mirror of
https://github.com/grafana/grafana.git
synced 2025-09-17 12:04:15 +08:00
Docs: Plugins doc reorganization, part 1 (#69864)
* Initial commit * Prettier fixes * Doc-validator fixes part 1 * Doc-validator fixes part 2 * More doc-validator fixes * More doc-validator fixes * Test * link test * Linnk test * Link test * More fixes * More fixes * Doc-validator fixes * Doc-validator fixes * fix broken link * Fix * Testing * Doc fixes * Link fixes * Fix links * Update docs/sources/developers/plugins/create-a-grafana-plugin/_index.md Co-authored-by: David Harris <david.harris@grafana.com> * Testing * Testing * Testing * Testing * Doc-validator fixes * Doc-validator fixes * Doc-validator fixes * Fix broken links for plugins reorganization project * Prettier fixes * Prettier fixes * Incorporate reviewer feedback * Link fixes * Link fixes * Link fixes * Link fix * Deleted space * Codeowners fix * Change grafana.com links to absolute URLs for Hugo --------- Co-authored-by: David Harris <david.harris@grafana.com>
This commit is contained in:
@ -1,27 +1,18 @@
|
||||
---
|
||||
description: How-to topics for plugin development
|
||||
title: Create a plugin
|
||||
title: Create a Grafana plugin
|
||||
menuTitle: Create a plugin
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- documentation
|
||||
description: An index of how-to topics for Grafana plugin development.
|
||||
weight: 300
|
||||
---
|
||||
|
||||
# Create a Grafana plugin
|
||||
|
||||
This section contains how-to topics for developing Grafana plugins.
|
||||
This section contains how-to topics for developing and extending Grafana plugins with more advanced capabilities.
|
||||
|
||||
- [Build a Grafana plugin](https://grafana.github.io/plugin-tools/docs/creating-a-plugin)
|
||||
- [Build a panel plugin](https://grafana.com/tutorials/build-a-panel-plugin/)
|
||||
- [Build a data source plugin](https://grafana.com/tutorials/build-a-data-source-plugin/)
|
||||
- [Build a data source backend plugin](https://grafana.com/tutorials/build-a-data-source-backend-plugin/)
|
||||
- [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">}})
|
||||
- Extend a Grafana plugin
|
||||
- [Add annotations]({{< relref "add-support-for-annotations.md">}})
|
||||
- [Add anonymous usage reporting]({{< relref "add-anonymous-usage-reporting.md">}})
|
||||
- [Add authentication for a data source plugin]({{< relref "add-authentication-for-data-source-plugins.md">}})
|
||||
- [Add Explore queries]({{< relref "add-support-for-explore-queries.md">}})
|
||||
- [Add query editor help]({{< relref "add-query-editor-help.md">}})
|
||||
- [Add variables]({{< relref "add-support-for-variables.md">}})
|
||||
- [Create panel option editors]({{< relref "custom-panel-option-editors.md">}})
|
||||
- [Sign a plugin]({{< relref "sign-a-plugin.md">}})
|
||||
- [Automate development with CI](https://grafana.github.io/plugin-tools/docs/ci)
|
||||
- [Create nested plugins](https://grafana.github.io/plugin-tools/docs/nested-plugins)
|
||||
- [Extend configurations](https://grafana.github.io/plugin-tools/docs/advanced-configuration)
|
||||
- [Develop a plugin]({{< relref "./develop-a-plugin" >}})
|
||||
- [Extend a plugin]({{< relref "./extend-a-plugin" >}})
|
||||
|
@ -0,0 +1,28 @@
|
||||
---
|
||||
title: Develop a Grafana plugin
|
||||
menuTitle: Develop a plugin
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- development
|
||||
- documentation
|
||||
description: An index of how-to topics for Grafana plugin development.
|
||||
weight: 100
|
||||
---
|
||||
|
||||
# Develop a Grafana plugin
|
||||
|
||||
This section contains how-to topics for developing Grafana plugins:
|
||||
|
||||
- [Build a panel plugin]({{< relref "./build-a-panel-plugin.md" >}})
|
||||
- [Build a panel plugin with d3.js]({{< relref "./build-a-panel-plugin-with-d3.md" >}})
|
||||
- [Build a data source plugin]({{< relref "./build-a-data-source-plugin.md" >}})
|
||||
- [Build a data source backend plugin]({{< relref "./build-a-data-source-backend-plugin.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" >}})
|
||||
- [Work with data frames]({{< relref "./working-with-data-frames.md" >}})
|
||||
|
||||
Additional resources:
|
||||
|
||||
- [Build a Grafana plugin with the create-plugin tool](https://grafana.github.io/plugin-tools/docs/creating-a-plugin)
|
@ -0,0 +1,184 @@
|
||||
---
|
||||
title: Build a data source backend plugin
|
||||
description: Create a backend for your data source plugin.
|
||||
weight: 400
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- backend
|
||||
- backend data source
|
||||
- datasource
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
Grafana supports a wide range of data sources, including Prometheus, MySQL, and even Datadog. There's a good chance you can already visualize metrics from the systems you have set up. In some cases, though, you already have an in-house metrics solution that you’d like to add to your Grafana dashboards. This tutorial teaches you to build a support for your data source.
|
||||
|
||||
For more information about backend plugins, refer to the documentation on [Backend plugins](/docs/grafana/latest/developers/plugins/backend/).
|
||||
|
||||
In this tutorial, you'll:
|
||||
|
||||
- Build a backend for your data source
|
||||
- Implement a health check for your data source
|
||||
- Enable Grafana Alerting for your data source
|
||||
|
||||
{{% class "prerequisite-section" %}}
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
- Knowledge about how data sources are implemented in the frontend.
|
||||
- Grafana 7.0
|
||||
- Go ([Version](https://github.com/grafana/plugin-tools/blob/main/packages/create-plugin/templates/backend/go.mod#L3))
|
||||
- [Mage](https://magefile.org/)
|
||||
- NodeJS ([Version](https://github.com/grafana/plugin-tools/blob/main/packages/create-plugin/templates/common/package.json#L66))
|
||||
- yarn
|
||||
{{% /class %}}
|
||||
|
||||
## Set up your environment
|
||||
|
||||
{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}}
|
||||
|
||||
## Create a new plugin
|
||||
|
||||
To build a backend for your data source plugin, Grafana requires a binary that it can execute when it loads the plugin during start-up. In this guide, we will build a binary using the [Grafana plugin SDK for Go]({{< relref "../../introduction-to-plugin-development/backend/grafana-plugin-sdk-for-go" >}}).
|
||||
|
||||
The easiest way to get started is to use the Grafana [create-plugin tool](https://www.npmjs.com/package/@grafana/create-plugin). Navigate to the plugin folder that you configured in step 1 and type:
|
||||
|
||||
```
|
||||
npx @grafana/create-plugin@latest
|
||||
```
|
||||
|
||||
Follow the steps and select **datasource** as your plugin type and answer **yes** when prompted to create a backend for your plugin.
|
||||
|
||||
```bash
|
||||
cd my-plugin
|
||||
```
|
||||
|
||||
Install frontend dependencies and build frontend parts of the plugin to _dist_ directory:
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
yarn build
|
||||
```
|
||||
|
||||
Run the following to update [Grafana plugin SDK for Go]({{< relref "../../introduction-to-plugin-development/backend/grafana-plugin-sdk-for-go" >}}) dependency to the latest minor version:
|
||||
|
||||
```bash
|
||||
go get -u github.com/grafana/grafana-plugin-sdk-go
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
Build backend plugin binaries for Linux, Windows and Darwin to _dist_ directory:
|
||||
|
||||
```bash
|
||||
mage -v
|
||||
```
|
||||
|
||||
Now, let's verify that the plugin you've built so far can be used in Grafana when creating a new data source:
|
||||
|
||||
1. Restart your Grafana instance.
|
||||
1. Open Grafana in your web browser.
|
||||
1. Navigate via the side-menu to **Configuration** -> **Data Sources**.
|
||||
1. Click **Add data source**.
|
||||
1. Find your newly created plugin and select it.
|
||||
1. Enter a name and then click **Save & Test** (ignore any errors reported for now).
|
||||
|
||||
You now have a new data source instance of your plugin that is ready to use in a dashboard:
|
||||
|
||||
1. Navigate via the side-menu to **Create** -> **Dashboard**.
|
||||
1. Click **Add new panel**.
|
||||
1. In the query tab, select the data source you just created.
|
||||
1. A line graph is rendered with one series consisting of two data points.
|
||||
1. Save the dashboard.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
#### Grafana doesn't load my plugin
|
||||
|
||||
By default, Grafana requires backend plugins to be signed. To load unsigned backend plugins, you need to
|
||||
configure Grafana to [allow unsigned plugins](/docs/grafana/latest/plugins/plugin-signature-verification/#allow-unsigned-plugins).
|
||||
For more information, refer to [Plugin signature verification](/docs/grafana/latest/plugins/plugin-signature-verification/#backend-plugins).
|
||||
|
||||
## Anatomy of a backend plugin
|
||||
|
||||
The folders and files used to build the backend for the data source are:
|
||||
|
||||
| file/folder | description |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `Magefile.go` | It’s not a requirement to use mage build files, but we strongly recommend using it so that you can use the build targets provided by the plugin SDK. |
|
||||
| `/go.mod ` | Go modules dependencies, [reference](https://golang.org/cmd/go/#hdr-The_go_mod_file) |
|
||||
| `/src/plugin.json` | A JSON file describing the backend plugin |
|
||||
| `/pkg/main.go` | Starting point of the plugin binary. |
|
||||
|
||||
#### plugin.json
|
||||
|
||||
The [plugin.json](/docs/grafana/latest/developers/plugins/metadata/) file is required for all plugins. When building a backend plugin these properties are important:
|
||||
|
||||
| property | description |
|
||||
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| backend | Should be set to `true` for backend plugins. This tells Grafana that it should start a binary when loading the plugin. |
|
||||
| executable | This is the name of the executable that Grafana expects to start, see [plugin.json reference](/docs/grafana/latest/developers/plugins/metadata/) for details. |
|
||||
| alerting | Should be set to `true` if your backend datasource supports alerting. |
|
||||
|
||||
In the next step we will look at the query endpoint!
|
||||
|
||||
## Implement data queries
|
||||
|
||||
We begin by opening the file `/pkg/plugin/plugin.go`. In this file you will see the `SampleDatasource` struct which implements the [backend.QueryDataHandler](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend?tab=doc#QueryDataHandler) interface. The `QueryData` method on this struct is where the data fetching happens for a data source plugin.
|
||||
|
||||
Each request contains multiple queries to reduce traffic between Grafana and plugins. So you need to loop over the slice of queries, process each query, and then return the results of all queries.
|
||||
|
||||
In the tutorial we have extracted a method named `query` to take care of each query model. Since each plugin has their own unique query model, Grafana sends it to the backend plugin as JSON. Therefore the plugin needs to `Unmarshal` the query model into something easier to work with.
|
||||
|
||||
As you can see the sample only returns static numbers. Try to extend the plugin to return other types of data.
|
||||
|
||||
You can read more about how to [build data frames in our docs](/docs/grafana/latest/developers/plugins/data-frames/).
|
||||
|
||||
## Add support for health checks
|
||||
|
||||
Implementing the health check handler allows Grafana to verify that a data source has been configured correctly.
|
||||
|
||||
When editing a data source in Grafana's UI, you can **Save & Test** to verify that it works as expected.
|
||||
|
||||
In this sample data source, there is a 50% chance that the health check will be successful. Make sure to return appropriate error messages to the users, informing them about what is misconfigured in the data source.
|
||||
|
||||
Open `/pkg/plugin/plugin.go`. In this file you'll see that the `SampleDatasource` struct also implements the [backend.CheckHealthHandler](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend?tab=doc#CheckHealthHandler) interface. Navigate to the `CheckHealth` method to see how the health check for this sample plugin is implemented.
|
||||
|
||||
## Add authentication
|
||||
|
||||
Implementing authentication allows your plugin to access protected resources like databases or APIs. To learn more about how to authenticate using a backend plugin, refer to [our documentation]({{< relref "../extend-a-plugin/add-authentication-for-data-source-plugins/#authenticate-using-a-backend-plugin" >}}).
|
||||
|
||||
## Enable Grafana Alerting
|
||||
|
||||
1. Open _src/plugin.json_.
|
||||
1. Add the top level `backend` property with a value of `true` to specify that your plugin supports Grafana Alerting, e.g.
|
||||
```json
|
||||
{
|
||||
...
|
||||
"backend": true,
|
||||
"executable": "gpx_simple_datasource_backend",
|
||||
"alerting": true,
|
||||
"info": {
|
||||
...
|
||||
}
|
||||
```
|
||||
1. Rebuild frontend parts of the plugin to _dist_ directory:
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
1. Restart your Grafana instance.
|
||||
1. Open Grafana in your web browser.
|
||||
1. Open the dashboard you created earlier in the _Create a new plugin_ step.
|
||||
1. Edit the existing panel.
|
||||
1. Click on the _Alert_ tab.
|
||||
1. Click on _Create Alert_ button.
|
||||
1. Edit condition and specify _IS ABOVE 10_. Change _Evaluate every_ to _10s_ and clear the _For_ field to make the alert rule evaluate quickly.
|
||||
1. Save the dashboard.
|
||||
1. After some time the alert rule evaluates and transitions into _Alerting_ state.
|
||||
|
||||
## Summary
|
||||
|
||||
In this tutorial you created a backend for your data source plugin.
|
@ -0,0 +1,376 @@
|
||||
---
|
||||
title: Build a data source plugin
|
||||
description: Create a plugin to add support for your own data sources.
|
||||
weight: 300
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- data source
|
||||
- datasource
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
Grafana supports a wide range of data sources, including Prometheus, MySQL, and even Datadog. There's a good chance you can already visualize metrics from the systems you have set up. In some cases, though, you already have an in-house metrics solution that you’d like to add to your Grafana dashboards. This tutorial teaches you to build a support for your data source.
|
||||
|
||||
In this tutorial, you'll:
|
||||
|
||||
- Build a data source to visualize a sine wave
|
||||
- Construct queries using the query editor
|
||||
- Configure your data source using the config editor
|
||||
|
||||
{{% class "prerequisite-section" %}}
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Grafana >=7.0
|
||||
- NodeJS >=14
|
||||
- yarn
|
||||
{{% /class %}}
|
||||
|
||||
## Set up your environment
|
||||
|
||||
{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}}
|
||||
|
||||
## Create a new plugin
|
||||
|
||||
{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" version="latest" >}}
|
||||
|
||||
To learn how to create a backend data source plugin, see [Build a data source backend plugin]({{< relref "./build-a-data-source-backend-plugin.md" >}})
|
||||
|
||||
## Anatomy of a plugin
|
||||
|
||||
{{< docs/shared lookup="tutorials/plugin-anatomy.md" source="grafana" version="latest" >}}
|
||||
|
||||
## Data source plugins
|
||||
|
||||
A data source in Grafana must extend the `DataSourceApi` interface, which requires you to define two methods: `query` and `testDatasource`.
|
||||
|
||||
### The `query` method
|
||||
|
||||
The `query` method is the heart of any data source plugin. It accepts a query from the user, retrieves the data from an external database, and returns the data in a format that Grafana recognizes.
|
||||
|
||||
```
|
||||
async query(options: DataQueryRequest<MyQuery>): Promise<DataQueryResponse>
|
||||
```
|
||||
|
||||
The `options` object contains the queries, or _targets_, that the user made, along with context information, like the current time interval. Use this information to query an external database.
|
||||
|
||||
> The term _target_ originates from Graphite, and the earlier days of Grafana when Graphite was the only supported data source. As Grafana gained support for more data sources, the term "target" became synonymous with any type of query.
|
||||
|
||||
### Test your data source
|
||||
|
||||
`testDatasource` implements a health check for your data source. For example, Grafana calls this method whenever the user clicks the **Save & Test** button, after changing the connection settings.
|
||||
|
||||
```
|
||||
async testDatasource()
|
||||
```
|
||||
|
||||
## Data frames
|
||||
|
||||
Nowadays there are countless different databases, each with their own ways of querying data. To be able to support all the different data formats, Grafana consolidates the data into a unified data structure called _data frames_.
|
||||
|
||||
Let's see how to create and return a data frame from the `query` method. In this step, you'll change the code in the starter plugin to return a [sine wave](https://en.wikipedia.org/wiki/Sine_wave).
|
||||
|
||||
1. In the current `query` method, remove the code inside the `map` function.
|
||||
|
||||
The `query` method now look like this:
|
||||
|
||||
```ts
|
||||
async query(options: DataQueryRequest<MyQuery>): Promise<DataQueryResponse> {
|
||||
const { range } = options;
|
||||
const from = range!.from.valueOf();
|
||||
const to = range!.to.valueOf();
|
||||
|
||||
const data = options.targets.map(target => {
|
||||
// Your code goes here.
|
||||
});
|
||||
|
||||
return { data };
|
||||
}
|
||||
```
|
||||
|
||||
1. In the `map` function, use the `lodash/defaults` package to set default values for query properties that haven't been set:
|
||||
|
||||
```ts
|
||||
const query = defaults(target, defaultQuery);
|
||||
```
|
||||
|
||||
1. Create a default query at the top of datasource.ts:
|
||||
|
||||
```ts
|
||||
export const defaultQuery: Partial<MyQuery> = {
|
||||
constant: 6.5,
|
||||
};
|
||||
```
|
||||
|
||||
1. Create a data frame with a time field and a number field:
|
||||
|
||||
```ts
|
||||
const frame = new MutableDataFrame({
|
||||
refId: query.refId,
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time },
|
||||
{ name: 'value', type: FieldType.number },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
`refId` needs to be set to tell Grafana which query that generated this date frame.
|
||||
|
||||
Next, we'll add the actual values to the data frame. Don't worry about the math used to calculate the values.
|
||||
|
||||
1. Create a couple of helper variables:
|
||||
|
||||
```ts
|
||||
// duration of the time range, in milliseconds.
|
||||
const duration = to - from;
|
||||
|
||||
// step determines how close in time (ms) the points will be to each other.
|
||||
const step = duration / 1000;
|
||||
```
|
||||
|
||||
1. Add the values to the data frame:
|
||||
|
||||
```ts
|
||||
for (let t = 0; t < duration; t += step) {
|
||||
frame.add({ time: from + t, value: Math.sin((2 * Math.PI * t) / duration) });
|
||||
}
|
||||
```
|
||||
|
||||
The `frame.add()` accepts an object where the keys corresponds to the name of each field in the data frame.
|
||||
|
||||
1. Return the data frame:
|
||||
|
||||
```ts
|
||||
return frame;
|
||||
```
|
||||
|
||||
1. Rebuild the plugin and try it out.
|
||||
|
||||
Your data source is now sending data frames that Grafana can visualize. Next, we'll look at how you can control the frequency of the sine wave by defining a _query_.
|
||||
|
||||
> In this example, we're generating timestamps from the current time range. This means that you'll get the same graph no matter what time range you're using. In practice, you'd instead use the timestamps returned by your database.
|
||||
|
||||
## Define a query
|
||||
|
||||
Most data sources offer a way to query specific data. MySQL and PostgreSQL use SQL, while Prometheus has its own query language, called _PromQL_. No matter what query language your databases are using, Grafana lets you build support for it.
|
||||
|
||||
Add support for custom queries to your data source, by implementing your own _query editor_, a React component that enables users to build their own queries, through a user-friendly graphical interface.
|
||||
|
||||
A query editor can be as simple as a text field where the user edits the raw query text, or it can provide a more user-friendly form with drop-down menus and switches, that later gets converted into the raw query text before it gets sent off to the database.
|
||||
|
||||
### Define the query model
|
||||
|
||||
The first step in designing your query editor is to define its _query model_. The query model defines the user input to your data source.
|
||||
|
||||
We want to be able to control the frequency of the sine wave, so let's add another property.
|
||||
|
||||
1. Add a new number property called `frequency` to the query model:
|
||||
|
||||
**src/types.ts**
|
||||
|
||||
```ts
|
||||
export interface MyQuery extends DataQuery {
|
||||
queryText?: string;
|
||||
constant: number;
|
||||
frequency: number;
|
||||
}
|
||||
```
|
||||
|
||||
1. Set a default value to the new `frequency` property:
|
||||
|
||||
```ts
|
||||
export const defaultQuery: Partial<MyQuery> = {
|
||||
constant: 6.5,
|
||||
frequency: 1.0,
|
||||
};
|
||||
```
|
||||
|
||||
### Bind the model to a form
|
||||
|
||||
Now that you've defined the query model you wish to support, the next step is to bind the model to a form. The `FormField` is a text field component from `grafana/ui` that lets you register a listener which will be invoked whenever the form field value changes.
|
||||
|
||||
1. Define the `frequency` from the `query` object and add a new form field to the query editor to control the new frequency property in the `render` method.
|
||||
|
||||
**QueryEditor.tsx**
|
||||
|
||||
```ts
|
||||
const { queryText, constant, frequency } = query;
|
||||
```
|
||||
|
||||
```ts
|
||||
<InlineField label="Frequency" labelWidth={16}>
|
||||
<Input onChange={onFrequencyChange} value={frequency} />
|
||||
</InlineField>
|
||||
```
|
||||
|
||||
1. Add a event listener for the new property.
|
||||
|
||||
```ts
|
||||
const onFrequencyChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange({ ...query, frequency: parseFloat(event.target.value) });
|
||||
// executes the query
|
||||
onRunQuery();
|
||||
};
|
||||
```
|
||||
|
||||
The registered listener, `onFrequencyChange`, calls `onChange` to update the current query with the value from the form field.
|
||||
|
||||
`onRunQuery();` tells Grafana to run the query after each change. For fast queries, this is recommended to provide a more responsive experience.
|
||||
|
||||
### Use the property
|
||||
|
||||
The new query model is now ready to use in our `query` method.
|
||||
|
||||
1. In the `query` method, use the `frequency` property to adjust our equation.
|
||||
|
||||
```ts
|
||||
frame.add({ time: from + t, value: Math.sin((2 * Math.PI * query.frequency * t) / duration) });
|
||||
```
|
||||
|
||||
## Configure your data source
|
||||
|
||||
To access a specific data source, you often need to configure things like hostname, credentials, or authentication method. A _config editor_ lets your users configure your data source plugin to fit their needs.
|
||||
|
||||
The config editor looks similar to the query editor, in that it defines a model and binds it to a form.
|
||||
|
||||
Since we're not actually connecting to an external database in our sine wave example, we don't really need many options. To show you how you can add an option however, we're going to add the _wave resolution_ as an option.
|
||||
|
||||
The resolution controls how close in time the data points are to each other. A higher resolution means more points closer together, at the cost of more data being processed.
|
||||
|
||||
### Define the options model
|
||||
|
||||
1. Add a new number property called `resolution` to the options model.
|
||||
|
||||
**types.ts**
|
||||
|
||||
```ts
|
||||
export interface MyDataSourceOptions extends DataSourceJsonData {
|
||||
path?: string;
|
||||
resolution?: number;
|
||||
}
|
||||
```
|
||||
|
||||
### Bind the model to a form
|
||||
|
||||
Just like query editor, the form field in the config editor calls the registered listener whenever the value changes.
|
||||
|
||||
1. Add a new form field to the query editor to control the new resolution option.
|
||||
|
||||
**ConfigEditor.tsx**
|
||||
|
||||
```ts
|
||||
<InlineField label="Resolution" labelWidth={12}>
|
||||
<Input onChange={onResolutionChange} value={jsonData.resolution || ''} placeholder="Enter a number" width={40} />
|
||||
</InlineField>
|
||||
```
|
||||
|
||||
1. Add a event listener for the new option.
|
||||
|
||||
```ts
|
||||
const onResolutionChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const jsonData = {
|
||||
...options.jsonData,
|
||||
resolution: parseFloat(event.target.value),
|
||||
};
|
||||
onOptionsChange({ ...options, jsonData });
|
||||
};
|
||||
```
|
||||
|
||||
The `onResolutionChange` listener calls `onOptionsChange` to update the current options with the value from the form field.
|
||||
|
||||
### Use the option
|
||||
|
||||
1. Create a property called `resolution` to the `DataSource` class.
|
||||
|
||||
```ts
|
||||
export class DataSource extends DataSourceApi<MyQuery, MyDataSourceOptions> {
|
||||
resolution: number;
|
||||
|
||||
constructor(instanceSettings: DataSourceInstanceSettings<MyDataSourceOptions>) {
|
||||
super(instanceSettings);
|
||||
|
||||
this.resolution = instanceSettings.jsonData.resolution || 1000.0;
|
||||
}
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
1. In the `query` method, use the `resolution` property to calculate the step size.
|
||||
|
||||
**src/datasource.ts**
|
||||
|
||||
```ts
|
||||
const step = duration / this.resolution;
|
||||
```
|
||||
|
||||
## Get data from an external API
|
||||
|
||||
So far, you've generated the data returned by the data source. A more realistic use case would be to fetch data from an external API.
|
||||
|
||||
While you can use something like [axios](https://github.com/axios/axios) or the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to make requests, we recommend using the [`getBackendSrv` function](https://github.com/grafana/grafana/blob/main/packages/grafana-runtime/src/services/backendSrv.ts) from the [`grafana-runtime` package](https://github.com/grafana/grafana/tree/main/packages/grafana-runtime).
|
||||
|
||||
The main advantage of `getBackendSrv` is that it proxies requests through the Grafana server rather making the request from the browser. This is strongly recommended when making authenticated requests to an external API. For more information on authenticating external requests, refer to [Add authentication for data source plugins]({{< relref "../extend-a-plugin/add-authentication-for-data-source-plugins.md" >}}).
|
||||
|
||||
1. Import `getBackendSrv`.
|
||||
|
||||
**src/datasource.ts**
|
||||
|
||||
```ts
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
```
|
||||
|
||||
1. Create a helper method `doRequest` and use the `datasourceRequest` method to make a request to your API. Replace `https://api.example.com/metrics` to point to your own API endpoint.
|
||||
|
||||
```ts
|
||||
async doRequest(query: MyQuery) {
|
||||
const result = await getBackendSrv().datasourceRequest({
|
||||
method: "GET",
|
||||
url: "https://api.example.com/metrics",
|
||||
params: query,
|
||||
})
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
1. Make a request for each query. `Promises.all` waits for all requests to finish before returning the data.
|
||||
|
||||
```ts
|
||||
async query(options: DataQueryRequest<MyQuery>): Promise<DataQueryResponse> {
|
||||
const promises = options.targets.map((query) =>
|
||||
this.doRequest(query).then((response) => {
|
||||
const frame = new MutableDataFrame({
|
||||
refId: query.refId,
|
||||
fields: [
|
||||
{ name: "Time", type: FieldType.time },
|
||||
{ name: "Value", type: FieldType.number },
|
||||
],
|
||||
});
|
||||
|
||||
response.data.forEach((point: any) => {
|
||||
frame.appendRow([point.time, point.value]);
|
||||
});
|
||||
|
||||
return frame;
|
||||
})
|
||||
);
|
||||
|
||||
return Promise.all(promises).then((data) => ({ data }));
|
||||
}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
In this tutorial you built a complete data source plugin for Grafana that uses a query editor to control what data to visualize. You've added a data source option, commonly used to set connection options and more.
|
||||
|
||||
### Learn more
|
||||
|
||||
Learn how you can improve your plugin even further, by reading our advanced guides:
|
||||
|
||||
- [Add support for variables](/docs/grafana/latest/developers/plugins/add-support-for-variables/)
|
||||
- [Add support for annotations](/docs/grafana/latest/developers/plugins/add-support-for-annotations/)
|
||||
- [Add support for Explore queries](/docs/grafana/latest/developers/plugins/add-support-for-explore-queries/)
|
||||
- [Build a logs data source](/docs/grafana/latest/developers/plugins/build-a-logs-data-source-plugin/)
|
@ -0,0 +1,159 @@
|
||||
---
|
||||
title: Build a logs data source plugin
|
||||
description: How to build a logs data source plugin.
|
||||
aliases:
|
||||
- ../../../plugins/build-a-logs-data-source-plugin/
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- logs
|
||||
- logs data source
|
||||
- datasource
|
||||
weight: 500
|
||||
---
|
||||
|
||||
# Build a logs data source plugin
|
||||
|
||||
Grafana data source plugins support metrics, logs, and other data types. The steps to build a logs data source plugin are largely the same as for a metrics data source, but there are a few differences which we will explain in this guide.
|
||||
|
||||
## Before you begin
|
||||
|
||||
This guide assumes that you're already familiar with how to [Build a data source plugin]({{< relref "./build-a-data-source-plugin" >}}) for metrics. We recommend that you review this material before continuing.
|
||||
|
||||
## Add logs support to your data source
|
||||
|
||||
To add logs support to an existing data source, you need to:
|
||||
|
||||
1. Enable logs support
|
||||
1. Construct the log data
|
||||
|
||||
When these steps are done, then you can improve the user experience with one or more [optional features](#enhance-your-logs-data-source-plugin-with-optional-features).
|
||||
|
||||
### Step 1: 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
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Construct the log data
|
||||
|
||||
As it does with metrics data, Grafana expects your plugin to return log data as a [data frame]({{< relref "../../introduction-to-plugin-development/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" >}}) or by adding a [Logs panel]({{< relref "../../../../panels-visualizations/visualizations/logs" >}}).
|
||||
|
||||
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.
|
||||
|
||||
## Enhance your logs data source plugin with optional features
|
||||
|
||||
Add visualization type hints, labels, and other optional features to logs.
|
||||
|
||||
### Add a preferred visualization type hint to the data frame
|
||||
|
||||
To make sure Grafana recognizes data as logs and shows logs visualization automatically in Explore, set `meta.preferredVisualisationType` to `'logs'` in the returned data frame. See [Selecting preferred visualization section]({{< relref "../extend-a-plugin/add-support-for-explore-queries#select-a-preferred-visualization-type" >}})
|
||||
|
||||
**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
|
||||
|
||||
Many log systems let you query logs based on metadata, or _labels_, to help filter log lines.
|
||||
|
||||
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
|
||||
|
||||
Add additional information about each log line by supplying 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. Grafana treats subsequent text fields as detected fields.
|
||||
|
||||
Any number of custom fields can be added to your data frame; Grafana comes with two dedicated fields: `levels` and `id`.
|
||||
|
||||
#### 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' });
|
||||
```
|
||||
|
||||
#### 'id' for assigning unique identifiers to 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' });
|
||||
```
|
@ -0,0 +1,236 @@
|
||||
---
|
||||
title: Build a panel plugin with D3.js
|
||||
description: how to use D3.js in your panel plugins.
|
||||
weight: 200
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- d3js
|
||||
- d3
|
||||
- panel
|
||||
- panel plugin
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
Panels are the building blocks of Grafana, and allow you to visualize data in different ways. This tutorial gives you a hands-on walkthrough of creating your own panel using [D3.js](https://d3js.org/).
|
||||
|
||||
For more information about panels, refer to the documentation on [Panels](/docs/grafana/latest/features/panels/panels/).
|
||||
|
||||
In this tutorial, you'll:
|
||||
|
||||
- Build a simple panel plugin to visualize a bar chart.
|
||||
- Learn how to use D3.js to build a panel using data-driven transformations.
|
||||
|
||||
{{% class "prerequisite-section" %}}
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Grafana 7.0
|
||||
- NodeJS 12.x
|
||||
- yarn
|
||||
{{% /class %}}
|
||||
|
||||
## Set up your environment
|
||||
|
||||
{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}}
|
||||
|
||||
## Create a new plugin
|
||||
|
||||
{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" version="latest" >}}
|
||||
|
||||
## Data-driven documents
|
||||
|
||||
[D3.js](https://d3js.org/) is a JavaScript library for manipulating documents based on data. It lets you transform arbitrary data into HTML, and is commonly used for creating visualizations.
|
||||
|
||||
Wait a minute. Manipulating documents based on data? That's sounds an awful lot like React. In fact, much of what you can accomplish with D3 you can already do with React. So before we start looking at D3, let's see how you can create an SVG from data, using only React.
|
||||
|
||||
In **SimplePanel.tsx**, change `SimplePanel` to return an `svg` with a `rect` element.
|
||||
|
||||
```ts
|
||||
export const SimplePanel = ({ options, data, width, height }: Props) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<svg width={width} height={height}>
|
||||
<rect x={0} y={0} width={10} height={10} fill={theme.palette.greenBase} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
One single rectangle might not be very exciting, so let's see how you can create rectangles from data.
|
||||
|
||||
1. Create some data that we can visualize.
|
||||
|
||||
```ts
|
||||
const values = [4, 8, 15, 16, 23, 42];
|
||||
```
|
||||
|
||||
1. Calculate the height of each bar based on the height of the panel.
|
||||
|
||||
```ts
|
||||
const barHeight = height / values.length;
|
||||
```
|
||||
|
||||
1. Inside a SVG group, `g`, create a `rect` element for every value in the dataset. Each rectangle uses the value as its width.
|
||||
|
||||
```ts
|
||||
return (
|
||||
<svg width={width} height={height}>
|
||||
<g>
|
||||
{values.map((value, i) => (
|
||||
<rect x={0} y={i * barHeight} width={value} height={barHeight - 1} fill={theme.palette.greenBase} />
|
||||
))}
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
```
|
||||
|
||||
1. Rebuild the plugin and reload your browser to see the changes you've made.
|
||||
|
||||
As you can see, React is perfectly capable of dynamically creating HTML elements. In fact, creating elements using React is often faster than creating them using D3.
|
||||
|
||||
So why would you use even use D3? In the next step, we'll see how you can take advantage of D3's data transformations.
|
||||
|
||||
## Transform data using D3.js
|
||||
|
||||
In this step, you'll see how you can transform data using D3 before rendering it using React.
|
||||
|
||||
D3 is already bundled with Grafana, and you can access it by importing the `d3` package. However, we're going to need the type definitions while developing.
|
||||
|
||||
1. Install the D3 type definitions:
|
||||
|
||||
```bash
|
||||
yarn add --dev @types/d3
|
||||
```
|
||||
|
||||
1. Import `d3` in **SimplePanel.tsx**.
|
||||
|
||||
```ts
|
||||
import * as d3 from 'd3';
|
||||
```
|
||||
|
||||
In the previous step, we had to define the width of each bar in pixels. Instead, let's use _scales_ from the D3 library to make the width of each bar depend on the width of the panel.
|
||||
|
||||
Scales are functions that map a range of values to another range of values. In this case, we want to map the values in our datasets to a position within our panel.
|
||||
|
||||
1. Create a scale to map a value between 0 and the maximum value in the dataset, to a value between 0 and the width of the panel. We'll be using this to calculate the width of the bar.
|
||||
|
||||
```ts
|
||||
const scale = d3
|
||||
.scaleLinear()
|
||||
.domain([0, d3.max(values) || 0.0])
|
||||
.range([0, width]);
|
||||
```
|
||||
|
||||
1. Pass the value to the scale function to calculate the width of the bar in pixels.
|
||||
|
||||
```ts
|
||||
return (
|
||||
<svg width={width} height={height}>
|
||||
<g>
|
||||
{values.map((value, i) => (
|
||||
<rect x={0} y={i * barHeight} width={scale(value)} height={barHeight - 1} fill={theme.palette.greenBase} />
|
||||
))}
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
```
|
||||
|
||||
As you can see, even if we're using React to render the actual elements, the D3 library contains useful tools that you can use to transform your data before rendering it.
|
||||
|
||||
## Add an axis
|
||||
|
||||
Another useful tool in the D3 toolbox is the ability to generate _axes_. Adding axes to our chart makes it easier for the user to understand the differences between each bar.
|
||||
|
||||
Let's see how you can use D3 to add a horizontal axis to your bar chart.
|
||||
|
||||
1. Create a D3 axis. Notice that by using the same scale as before, we make sure that the bar width aligns with the ticks on the axis.
|
||||
|
||||
```ts
|
||||
const axis = d3.axisBottom(scale);
|
||||
```
|
||||
|
||||
1. Generate the axis. While D3 needs to generate the elements for the axis, we can encapsulate it by generating them within an anonymous function which we pass as a `ref` to a group element `g`.
|
||||
|
||||
```ts
|
||||
<g
|
||||
ref={(node) => {
|
||||
d3.select(node).call(axis as any);
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
By default, the axis renders at the top of the SVG element. We'd like to move it to the bottom, but to do that, we first need to make room for it by decreasing the height of each bar.
|
||||
|
||||
1. Calculate the new bar height based on the padded height.
|
||||
|
||||
```ts
|
||||
const padding = 20;
|
||||
const chartHeight = height - padding;
|
||||
const barHeight = chartHeight / values.length;
|
||||
```
|
||||
|
||||
1. Translate the axis by adding a transform to the `g` element.
|
||||
|
||||
```ts
|
||||
<g
|
||||
transform={`translate(0, ${chartHeight})`}
|
||||
ref={(node) => {
|
||||
d3.select(node).call(axis as any);
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
Congrats! You've created a simple and responsive bar chart.
|
||||
|
||||
## Complete example
|
||||
|
||||
```ts
|
||||
import React from 'react';
|
||||
import { PanelProps } from '@grafana/data';
|
||||
import { SimpleOptions } from 'types';
|
||||
import { useTheme } from '@grafana/ui';
|
||||
import * as d3 from 'd3';
|
||||
|
||||
interface Props extends PanelProps<SimpleOptions> {}
|
||||
|
||||
export const SimplePanel = ({ options, data, width, height }: Props) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const values = [4, 8, 15, 16, 23, 42];
|
||||
|
||||
const scale = d3
|
||||
.scaleLinear()
|
||||
.domain([0, d3.max(values) || 0.0])
|
||||
.range([0, width]);
|
||||
|
||||
const axis = d3.axisBottom(scale);
|
||||
|
||||
const padding = 20;
|
||||
const chartHeight = height - padding;
|
||||
const barHeight = chartHeight / values.length;
|
||||
|
||||
return (
|
||||
<svg width={width} height={height}>
|
||||
<g>
|
||||
{values.map((value, i) => (
|
||||
<rect x={0} y={i * barHeight} width={scale(value)} height={barHeight - 1} fill={theme.palette.greenBase} />
|
||||
))}
|
||||
</g>
|
||||
<g
|
||||
transform={`translate(0, ${chartHeight})`}
|
||||
ref={(node) => {
|
||||
d3.select(node).call(axis as any);
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
In this tutorial you built a panel plugin with D3.js.
|
@ -0,0 +1,260 @@
|
||||
---
|
||||
title: Build a panel plugin
|
||||
description: Learn how to create a custom visualization for your dashboards.
|
||||
weight: 100
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- visualization
|
||||
- custom visualization
|
||||
- dashboard
|
||||
- dashboards
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
Panels are the building blocks of Grafana. They allow you to visualize data in different ways. While Grafana has several types of panels already built-in, you can also build your own panel, to add support for other visualizations.
|
||||
|
||||
For more information about panels, refer to the documentation on [Panels](/docs/grafana/latest/panels/).
|
||||
|
||||
{{% class "prerequisite-section" %}}
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Grafana >=7.0
|
||||
- NodeJS >=14
|
||||
- yarn
|
||||
{{% /class %}}
|
||||
|
||||
## Set up your environment
|
||||
|
||||
{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}}
|
||||
|
||||
## Create a new plugin
|
||||
|
||||
{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" version="latest" >}}
|
||||
|
||||
## Anatomy of a plugin
|
||||
|
||||
{{< docs/shared lookup="tutorials/plugin-anatomy.md" source="grafana" version="latest" >}}
|
||||
|
||||
## Panel plugins
|
||||
|
||||
Since Grafana 6.x, panels are [ReactJS components](https://reactjs.org/docs/components-and-props.html).
|
||||
|
||||
Prior to Grafana 6.0, plugins were written in [AngularJS](https://angular.io/). Even though we still support plugins written in AngularJS, we highly recommend that you write new plugins using ReactJS.
|
||||
|
||||
### Panel properties
|
||||
|
||||
The [PanelProps](https://github.com/grafana/grafana/blob/747b546c260f9a448e2cb56319f796d0301f4bb9/packages/grafana-data/src/types/panel.ts#L27-L40) interface exposes runtime information about the panel, such as panel dimensions, and the current time range.
|
||||
|
||||
You can access the panel properties through `props`, as seen in your plugin.
|
||||
|
||||
**src/SimplePanel.tsx**
|
||||
|
||||
```js
|
||||
const { options, data, width, height } = props;
|
||||
```
|
||||
|
||||
### Development workflow
|
||||
|
||||
Next, you'll learn the basic workflow of making a change to your panel, building it, and reloading Grafana to reflect the changes you made.
|
||||
|
||||
First, you need to add your panel to a dashboard:
|
||||
|
||||
1. Open Grafana in your browser.
|
||||
1. Create a new dashboard, and add a new panel.
|
||||
1. Select your panel from the list of visualization types.
|
||||
1. Save the dashboard.
|
||||
|
||||
Now that you can view your panel, try making a change to the panel plugin:
|
||||
|
||||
1. In `SimplePanel.tsx`, change the fill color of the circle.
|
||||
1. Run `yarn dev` to build the plugin.
|
||||
1. In the browser, reload Grafana with the new changes.
|
||||
|
||||
## Add panel options
|
||||
|
||||
Sometimes you want to offer the users of your panel an option to configure the behavior of your plugin. By configuring _panel options_ for your plugin, your panel will be able to accept user input.
|
||||
|
||||
In the previous step, you changed the fill color of the circle in the code. Let's change the code so that the plugin user can configure the color from the panel editor.
|
||||
|
||||
#### Add an option
|
||||
|
||||
Panel options are defined in a _panel options object_. `SimpleOptions` is an interface that describes the options object.
|
||||
|
||||
1. In `types.ts`, add a `CircleColor` type to hold the colors the users can choose from:
|
||||
|
||||
```
|
||||
type CircleColor = 'red' | 'green' | 'blue';
|
||||
```
|
||||
|
||||
1. In the `SimpleOptions` interface, add a new option called `color`:
|
||||
|
||||
```
|
||||
color: CircleColor;
|
||||
```
|
||||
|
||||
Here's the updated options definition:
|
||||
|
||||
**src/types.ts**
|
||||
|
||||
```ts
|
||||
type SeriesSize = 'sm' | 'md' | 'lg';
|
||||
type CircleColor = 'red' | 'green' | 'blue';
|
||||
|
||||
// interface defining panel options type
|
||||
export interface SimpleOptions {
|
||||
text: string;
|
||||
showSeriesCount: boolean;
|
||||
seriesCountSize: SeriesSize;
|
||||
color: CircleColor;
|
||||
}
|
||||
```
|
||||
|
||||
#### Add an option control
|
||||
|
||||
To change the option from the panel editor, you need to bind the `color` option to an _option control_.
|
||||
|
||||
Grafana supports a range of option controls, such as text inputs, switches, and radio groups.
|
||||
|
||||
Let's create a radio control and bind it to the `color` option.
|
||||
|
||||
1. In `src/module.ts`, add the control at the end of the builder:
|
||||
|
||||
```ts
|
||||
.addRadio({
|
||||
path: 'color',
|
||||
name: 'Circle color',
|
||||
defaultValue: 'red',
|
||||
settings: {
|
||||
options: [
|
||||
{
|
||||
value: 'red',
|
||||
label: 'Red',
|
||||
},
|
||||
{
|
||||
value: 'green',
|
||||
label: 'Green',
|
||||
},
|
||||
{
|
||||
value: 'blue',
|
||||
label: 'Blue',
|
||||
},
|
||||
],
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The `path` is used to bind the control to an option. You can bind a control to nested option by specifying the full path within a options object, for example `colors.background`.
|
||||
|
||||
Grafana builds an options editor for you and displays it in the panel editor sidebar in the **Display** section.
|
||||
|
||||
#### Use the new option
|
||||
|
||||
You're almost done. You've added a new option and a corresponding control to change the value. But the plugin isn't using the option yet. Let's change that.
|
||||
|
||||
1. To convert option value to the colors used by the current theme, add a `switch` statement right before the `return` statement in `SimplePanel.tsx`.
|
||||
|
||||
**src/SimplePanel.tsx**
|
||||
|
||||
```ts
|
||||
let color: string;
|
||||
switch (options.color) {
|
||||
case 'red':
|
||||
color = theme.palette.redBase;
|
||||
break;
|
||||
case 'green':
|
||||
color = theme.palette.greenBase;
|
||||
break;
|
||||
case 'blue':
|
||||
color = theme.palette.blue95;
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
1. Configure the circle to use the color.
|
||||
|
||||
```ts
|
||||
<g>
|
||||
<circle style={{ fill: color }} r={100} />
|
||||
</g>
|
||||
```
|
||||
|
||||
Now, when you change the color in the panel editor, the fill color of the circle changes as well.
|
||||
|
||||
## Create dynamic panels using data frames
|
||||
|
||||
Most panels visualize dynamic data from a Grafana data source. In this step, you'll create one circle per series, each with a radius equal to the last value in the series.
|
||||
|
||||
> To use data from queries in your panel, you need to set up a data source. If you don't have one available, you can use the [TestData](/docs/grafana/latest/features/datasources/testdata) data source while developing.
|
||||
|
||||
The results from a data source query within your panel are available in the `data` property inside your panel component.
|
||||
|
||||
```ts
|
||||
const { data } = props;
|
||||
```
|
||||
|
||||
`data.series` contains the series returned from a data source query. Each series is represented as a data structure called _data frame_. A data frame resembles a table, where data is stored by columns, or _fields_, instead of rows. Every value in a field share the same data type, such as string, number, or time.
|
||||
|
||||
Here's an example of a data frame with a time field, `Time`, and a number field, `Value`:
|
||||
|
||||
| Time | Value |
|
||||
| ------------- | ----- |
|
||||
| 1589189388597 | 32.4 |
|
||||
| 1589189406480 | 27.2 |
|
||||
| 1589189513721 | 15.0 |
|
||||
|
||||
Let's see how you can retrieve data from a data frame and use it in your visualization.
|
||||
|
||||
1. Get the last value of each field of type `number`, by adding the following to `SimplePanel.tsx`, before the `return` statement:
|
||||
|
||||
```ts
|
||||
const radii = data.series
|
||||
.map((series) => series.fields.find((field) => field.type === 'number'))
|
||||
.map((field) => field?.values.get(field.values.length - 1));
|
||||
```
|
||||
|
||||
`radii` will contain the last values in each of the series that are returned from a data source query. You'll use these to set the radius for each circle.
|
||||
|
||||
1. Change the `svg` element to the following:
|
||||
|
||||
```ts
|
||||
<svg
|
||||
className={styles.svg}
|
||||
width={width}
|
||||
height={height}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
viewBox={`0 -${height / 2} ${width} ${height}`}
|
||||
>
|
||||
<g fill={color}>
|
||||
{radii.map((radius, index) => {
|
||||
const step = width / radii.length;
|
||||
return <circle r={radius} transform={`translate(${index * step + step / 2}, 0)`} />;
|
||||
})}
|
||||
</g>
|
||||
</svg>
|
||||
```
|
||||
|
||||
Note how we're creating a `<circle>` element for each value in `radii`:
|
||||
|
||||
```ts
|
||||
{
|
||||
radii.map((radius, index) => {
|
||||
const step = width / radii.length;
|
||||
return <circle r={radius} transform={`translate(${index * step + step / 2}, 0)`} />;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
We use the `transform` here to distribute the circle horizontally within the panel.
|
||||
|
||||
1. Rebuild your plugin and try it out by adding multiple queries to the panel. Refresh the dashboard.
|
||||
|
||||
If you want to know more about data frames, check out our introduction to [Data frames](/docs/grafana/latest/developers/plugins/data-frames/).
|
||||
|
||||
## Summary
|
||||
|
||||
In this tutorial you learned how to create a custom visualization for your dashboards.
|
@ -0,0 +1,162 @@
|
||||
---
|
||||
title: Build a streaming data source plugin
|
||||
aliases:
|
||||
- ../../../plugins/build-a-streaming-data-source-plugin/
|
||||
description: How to build a streaming data source plugin.
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- streaming
|
||||
- streaming data source
|
||||
- datasource
|
||||
weight: 600
|
||||
---
|
||||
|
||||
# Build a streaming data source plugin
|
||||
|
||||
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. Adding streaming to a plugin helps reduce queries so your dashboard is only updated when new data becomes available.
|
||||
|
||||
## Before you begin
|
||||
|
||||
This guide assumes that you're already familiar with how to [Build a data source plugin]({{< relref "./build-a-data-source-plugin" >}})
|
||||
|
||||
Grafana uses [RxJS](https://rxjs.dev/) to continuously send data from a data source to a panel visualization.
|
||||
|
||||
> **Note:** To learn more about RxJs, refer to the [RxJS documentation](https://rxjs.dev/guide/overview).
|
||||
|
||||
## Add streaming to your data source
|
||||
|
||||
Enable streaming for your data source plugin to update your dashboard when 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.
|
||||
|
||||
### Step 1: Edit the `plugin.json` file
|
||||
|
||||
Enable streaming for your data source in the `plugin.json` file.
|
||||
|
||||
```json
|
||||
{
|
||||
"streaming": true
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Change the signature of the `query` method
|
||||
|
||||
Modify 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> {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Create an `Observable` instance for each query
|
||||
|
||||
Create an `Observable` instance 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);
|
||||
```
|
||||
|
||||
### Step 4: Create a `CircularDataFrame` instance
|
||||
|
||||
In the `subscribe` function, create a `CircularDataFrame` instance.
|
||||
|
||||
```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.
|
||||
|
||||
### Step 5: Send the updated data frame
|
||||
|
||||
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. In the example above, data is being received every 500 milliseconds.
|
||||
|
||||
### Example code for 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 it, or _backfill_ it, to the data frame before the first call to `subscriber.next()`.
|
||||
|
||||
For another example of a streaming plugin, refer to the [streaming websocket example](https://github.com/grafana/grafana-plugin-examples/tree/main/examples/datasource-streaming-websocket) on GitHub.
|
@ -0,0 +1,207 @@
|
||||
---
|
||||
title: Build an app plugin
|
||||
description: Learn at how to create an app for Grafana.
|
||||
weight: 700
|
||||
draft: true
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- app
|
||||
- app plugin
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
App plugins are Grafana plugins that can bundle data source and panel plugins within one package. They also let you create _custom pages_ within Grafana. Custom pages enable the plugin author to include things like documentation, sign-up forms, or to control other services over HTTP.
|
||||
|
||||
Data source and panel plugins will show up like normal plugins. The app pages will be available in the main menu.
|
||||
|
||||
{{% class "prerequisite-section" %}}
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Grafana 7.0
|
||||
- NodeJS 12.x
|
||||
- yarn
|
||||
{{% /class %}}
|
||||
|
||||
## Set up your environment
|
||||
|
||||
{{< docs/shared lookup="tutorials/set-up-environment.md" source="grafana" version="latest" >}}
|
||||
|
||||
## Create a new plugin
|
||||
|
||||
{{< docs/shared lookup="tutorials/create-plugin.md" source="grafana" version="latest" >}}
|
||||
|
||||
## Anatomy of a plugin
|
||||
|
||||
{{< docs/shared lookup="tutorials/plugin-anatomy.md" source="grafana" version="latest" >}}
|
||||
|
||||
## App plugins
|
||||
|
||||
App plugins let you bundle resources such as dashboards, panels, and data sources into a single plugin.
|
||||
|
||||
Any resource you want to include needs to be added to the `includes` property in the `plugin.json` file. To add a resource to your app plugin, you need to include it to the `plugin.json`.
|
||||
|
||||
Plugins that are included in an app plugin are available like any other plugin.
|
||||
|
||||
Dashboards and pages can be added to the app menu by setting `addToNav` to `true`.
|
||||
|
||||
By setting `"defaultNav": true`, users can navigate to the dashboard by clicking the app icon in the side menu.
|
||||
|
||||
## Add a custom page
|
||||
|
||||
App plugins let you extend the Grafana user interface through the use of _custom pages_.
|
||||
|
||||
Any requests sent to `/a/<plugin-id>`, e.g. `/a/myorgid-simple-app/`, are routed to the _root page_ of the app plugin. The root page is a React component that returns the content for a given route.
|
||||
|
||||
While you're free to implement your own routing, in this tutorial you'll use a tab-based navigation page that you can use by calling `onNavChange`.
|
||||
|
||||
Let's add a tab for managing server instances.
|
||||
|
||||
1. In the `src/pages` directory, add a new file called `Instances.tsx`. This component contains the content for the new tab.
|
||||
|
||||
```ts
|
||||
import { AppRootProps } from '@grafana/data';
|
||||
import React from 'react';
|
||||
|
||||
export const Instances = ({ query, path, meta }: AppRootProps) => {
|
||||
return <p>Hello</p>;
|
||||
};
|
||||
```
|
||||
|
||||
1. Register the page by adding it to the `pages` array in `src/pages/index.ts`.
|
||||
|
||||
**index.ts**
|
||||
|
||||
```ts
|
||||
import { Instances } from './Instances';
|
||||
```
|
||||
|
||||
```ts
|
||||
{
|
||||
component: Instances,
|
||||
icon: 'file-alt',
|
||||
id: 'instances',
|
||||
text: 'Instances',
|
||||
}
|
||||
```
|
||||
|
||||
1. Add the page to the app menu, by including it in `plugin.json`. This will be the main view of the app, so we'll set `defaultNav` to let users quickly get to it by clicking the app icon in the side menu.
|
||||
|
||||
**plugin.json**
|
||||
|
||||
```json
|
||||
"includes": [
|
||||
{
|
||||
"type": "page",
|
||||
"name": "Instances",
|
||||
"path": "/a/myorgid-simple-app?tab=instances",
|
||||
"role": "Viewer",
|
||||
"addToNav": true,
|
||||
"defaultNav": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
> **Note:** While `page` includes typically reference pages created by the app, you can set `path` to any URL, internal or external. Try setting `path` to `https://grafana.com`.
|
||||
|
||||
## Configure the app
|
||||
|
||||
Let's add a new configuration page where users are able to configure default zone and regions for any instances they create.
|
||||
|
||||
1. In `module.ts`, add new configuration page using the `addConfigPage` method. `body` is the React component that renders the page content.
|
||||
|
||||
**module.ts**
|
||||
|
||||
```ts
|
||||
.addConfigPage({
|
||||
title: 'Defaults',
|
||||
icon: 'fa fa-info',
|
||||
body: DefaultsConfigPage,
|
||||
id: 'defaults',
|
||||
})
|
||||
```
|
||||
|
||||
## Add a dashboard
|
||||
|
||||
#### Include a dashboard in your app
|
||||
|
||||
1. In `src/`, create a new directory called `dashboards`.
|
||||
1. Create a file called `overview.json` in the `dashboards` directory.
|
||||
1. Copy the JSON definition for the dashboard you want to include and paste it into `overview.json`. If you don't have one available, you can find a sample dashboard at the end of this step.
|
||||
1. In `plugin.json`, add the following object to the `includes` property.
|
||||
|
||||
- The `name` of the dashboard needs to be the same as the `title` in the dashboard JSON model.
|
||||
- `path` points out the file that contains the dashboard definition, relative to the `plugin.json` file.
|
||||
|
||||
```json
|
||||
"includes": [
|
||||
{
|
||||
"type": "dashboard",
|
||||
"name": "System overview",
|
||||
"path": "dashboards/overview.json",
|
||||
"addToNav": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
1. Save and restart Grafana to load the new changes.
|
||||
|
||||
## Bundle a plugin
|
||||
|
||||
An app plugin can contain panel and data source plugins that get installed along with the app plugin.
|
||||
|
||||
In this step, you'll add a data source to your app plugin. You can add panel plugins the same way by changing `datasource` to `panel`.
|
||||
|
||||
1. In `src/`, create a new directory called `datasources`.
|
||||
1. Create a new data source using Grafana create-plugin tool in a temporary directory.
|
||||
|
||||
```bash
|
||||
mkdir tmp
|
||||
cd tmp
|
||||
npx @grafana/create-plugin@latest
|
||||
```
|
||||
|
||||
1. Move the `src` directory in the data source plugin to `src/datasources`, and rename it to `my-datasource`.
|
||||
|
||||
```bash
|
||||
mv ./my-datasource/src ../src/datasources/my-datasource
|
||||
```
|
||||
|
||||
Any bundled plugins are built along with the app plugin. Grafana looks for any subdirectory containing a `plugin.json` file and attempts to load a plugin in that directory.
|
||||
|
||||
To let users know that your plugin bundles other plugins, you can optionally display it on the plugin configuration page. This is not done automatically, so you need to add it to the `plugin.json`.
|
||||
|
||||
1. Include the data source in the `plugin.json`. The `name` property is only used for displaying in the Grafana UI.
|
||||
|
||||
```json
|
||||
"includes": [
|
||||
{
|
||||
"type": "datasource",
|
||||
"name": "My data source"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Include external plugins
|
||||
|
||||
If you want to let users know that your app requires an existing plugin, you can add it as a dependency in `plugin.json`. Note that they'll still need to install it themselves.
|
||||
|
||||
```json
|
||||
"dependencies": {
|
||||
"plugins": [
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Worldmap Panel",
|
||||
"id": "grafana-worldmap-panel",
|
||||
"version": "^0.3.2"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
In this tutorial you learned how to create an app plugin.
|
@ -0,0 +1,131 @@
|
||||
---
|
||||
title: Work with data frames
|
||||
aliases:
|
||||
- ../../../plugins/working-with-data-frames/
|
||||
description: How to work with data frames.
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- data frames
|
||||
- dataframes
|
||||
weight: 900
|
||||
---
|
||||
|
||||
# Work with data frames
|
||||
|
||||
The [data frame]({{< relref "../../introduction-to-plugin-development/data-frames" >}}) is a columnar data structure that allows for 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 want to migrate an existing plugin to use the data frame format, refer to [Migrate to data frames]({{< relref "../../migration-guide/v6.x-v7.x#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 do this.
|
||||
|
||||
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, to create data frames like this, your data must already be 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`. In this case, `toDataFrame` tries to guess the schema based on the types and names of the objects in the array. To create complex data frames this way, 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
|
||||
function SimplePanel({ data: Props }) {
|
||||
const frame = data.series[0];
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Before you start reading the data, think about what data you expect. For example, to visualize a time series you 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[i], y?.values[i], size?.values[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.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);
|
||||
```
|
@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Extend a Grafana plugin
|
||||
menuTitle: Extend a plugin
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- development
|
||||
- extension
|
||||
- documentation
|
||||
description: An index of how-to topics for extending or enhancing Grafana plugins.
|
||||
weight: 200
|
||||
---
|
||||
|
||||
# Extend a Grafana plugin
|
||||
|
||||
This section contains how-to topics for extending or enhancing Grafana plugins:
|
||||
|
||||
- [Enable annotations]({{< relref "./add-support-for-annotations.md" >}})
|
||||
- [Add anonymous usage reporting]({{< relref "./add-anonymous-usage-reporting.md" >}})
|
||||
- [Add authentication for a data source plugin]({{< relref "./add-authentication-for-data-source-plugins.md" >}})
|
||||
- [Add distributed tracing for backend plugins]({{< relref "./add-distributed-tracing-for-backend-plugins.md" >}})
|
||||
- [Add features to Explore queries]({{< relref "./add-support-for-explore-queries.md" >}})
|
||||
- [Add query editor help]({{< relref "./add-query-editor-help.md" >}})
|
||||
- [Add support for variables]({{< relref "./add-support-for-variables.md" >}})
|
||||
- [Build a custom panel option editor]({{< relref "./custom-panel-option-editors.md" >}})
|
||||
- [Use extensions to add links to app plugins]({{< relref "./extend-the-grafana-ui-with-links.md" >}})
|
||||
- [Work with cross-plugin links]({{< relref "./cross-plugin-linking.md" >}})
|
||||
|
||||
Additional resources:
|
||||
|
||||
- [Automate development with CI](https://grafana.github.io/plugin-tools/docs/ci)
|
||||
- [Create nested plugins](https://grafana.github.io/plugin-tools/docs/nested-plugins)
|
||||
- [Extend configurations](https://grafana.github.io/plugin-tools/docs/advanced-configuration)
|
@ -0,0 +1,176 @@
|
||||
---
|
||||
title: Add anonymous usage reporting
|
||||
aliases:
|
||||
- ../../../plugins/add-anonymous-usage-reporting/
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- anonymous usage
|
||||
- reporting
|
||||
description: How to add anonymous usage tracking to your Grafana plugin.
|
||||
weight: 200
|
||||
---
|
||||
|
||||
# Add anonymous usage reporting
|
||||
|
||||
Add anonymous usage tracking to your plugin to send [reporting events]({{< relref "../../../../setup-grafana/configure-grafana#reporting_enabled" >}}) that describe how your plugin is being used to a tracking system configured by your Grafana server administrator.
|
||||
|
||||
## Event reporting
|
||||
|
||||
In this section, we show an example of tracking usage data from a query editor and receiving a report back from the analytics service.
|
||||
|
||||
### Sample query editor
|
||||
|
||||
Let's say you have a `QueryEditor` that looks similar to the example below. It has a `CodeEditor` field where you can write your query and a query type selector so you can select the kind of query result that you expect to return:
|
||||
|
||||
```ts
|
||||
import React, { ReactElement } from 'react';
|
||||
import { InlineFieldRow, InlineField, Select, CodeEditor } from '@grafana/ui';
|
||||
import type { EditorProps } from './types';
|
||||
|
||||
export function QueryEditor(props: EditorProps): ReactElement {
|
||||
const { datasource, query, onChange, onRunQuery } = props;
|
||||
const queryType = { value: query.value ?? 'timeseries' };
|
||||
const queryTypes = [
|
||||
{
|
||||
label: 'Timeseries',
|
||||
value: 'timeseries',
|
||||
},
|
||||
{
|
||||
label: 'Table',
|
||||
value: 'table',
|
||||
},
|
||||
];
|
||||
|
||||
const onChangeQueryType = (type: string) => {
|
||||
onChange({
|
||||
...query,
|
||||
queryType: type,
|
||||
});
|
||||
runQuery();
|
||||
};
|
||||
|
||||
const onChangeRawQuery = (rawQuery: string) => {
|
||||
onChange({
|
||||
...query,
|
||||
rawQuery: type,
|
||||
});
|
||||
runQuery();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<CodeEditor
|
||||
height="200px"
|
||||
showLineNumbers={true}
|
||||
language="sql"
|
||||
onBlur={onChangeRawQuery}
|
||||
value={query.rawQuery}
|
||||
/>
|
||||
</div>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Query type" grow>
|
||||
<Select options={queryTypes} onChange={onChangeQueryType} value={queryType} />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Track usage with `usePluginInteractionReporter`
|
||||
|
||||
Let's say that you want to track how the usage looks between time series and table queries.
|
||||
|
||||
What you want to do is to add the `usePluginInteractionReporter` to fetch a report function that takes two arguments:
|
||||
|
||||
- Required: An event name that begins with `grafana_plugin_`. It is used to identify the interaction being made.
|
||||
- Optional: Attached contextual data. In our example, that is the query type.
|
||||
|
||||
```ts
|
||||
import React, { ReactElement } from 'react';
|
||||
import { InlineFieldRow, InlineField, Select, CodeEditor } from '@grafana/ui';
|
||||
import { usePluginInteractionReporter } from '@grafana/runtime';
|
||||
import type { EditorProps } from './types';
|
||||
|
||||
export function QueryEditor(props: EditorProps): ReactElement {
|
||||
const { datasource, query, onChange, onRunQuery } = props;
|
||||
const report = usePluginInteractionReporter();
|
||||
|
||||
const queryType = { value: query.value ?? 'timeseries' };
|
||||
const queryTypes = [
|
||||
{
|
||||
label: 'Timeseries',
|
||||
value: 'timeseries',
|
||||
},
|
||||
{
|
||||
label: 'Table',
|
||||
value: 'table',
|
||||
},
|
||||
];
|
||||
|
||||
const onChangeQueryType = (type: string) => {
|
||||
onChange({
|
||||
...query,
|
||||
queryType: type,
|
||||
});
|
||||
runQuery();
|
||||
};
|
||||
|
||||
const onChangeRawQuery = (rawQuery: string) => {
|
||||
onChange({
|
||||
...query,
|
||||
rawQuery: type,
|
||||
});
|
||||
|
||||
report('grafana_plugin_executed_query', {
|
||||
query_type: queryType.value,
|
||||
});
|
||||
|
||||
runQuery();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<CodeEditor
|
||||
height="200px"
|
||||
showLineNumbers={true}
|
||||
language="sql"
|
||||
onBlur={onChangeRawQuery}
|
||||
value={query.rawQuery}
|
||||
/>
|
||||
</div>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Query type" grow>
|
||||
<Select options={queryTypes} onChange={onChangeQueryType} value={queryType} />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Data returned from the analytics service
|
||||
|
||||
When you use `usePluginInteractionReporter`, the report function that is handed back to you automatically attaches contextual data about the plugin you are tracking to the events.
|
||||
|
||||
In our example, the following information is sent to the analytics service configured by the Grafana server administrator:
|
||||
|
||||
```ts
|
||||
{
|
||||
type: 'interaction',
|
||||
payload: {
|
||||
interactionName: 'grafana_plugin_executed_query',
|
||||
grafana_version: '9.2.1',
|
||||
plugin_type: 'datasource',
|
||||
plugin_version: '1.0.0',
|
||||
plugin_id: 'grafana-example-datasource',
|
||||
plugin_name: 'Example',
|
||||
datasource_uid: 'qeSI8VV7z', // will only be added for datasources
|
||||
query_type: 'timeseries'
|
||||
}
|
||||
}
|
||||
```
|
@ -0,0 +1,470 @@
|
||||
---
|
||||
title: Add authentication for data source plugins
|
||||
aliases:
|
||||
- ../../../plugins/add-authentication-for-data-source-plugins/
|
||||
description: How to add authentication for data source plugins.
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- authentication
|
||||
- data source
|
||||
- datasource
|
||||
aliases:
|
||||
- ../../plugins/developing/auth-for-datasources/
|
||||
- /docs/grafana/next/developers/plugins/authentication/
|
||||
weight: 300
|
||||
---
|
||||
|
||||
# Add authentication for data source plugins
|
||||
|
||||
Grafana plugins can perform authenticated requests against a third-party API by using the _data source proxy_ or through a custom a _backend plugin_.
|
||||
|
||||
## Choose an authentication method
|
||||
|
||||
Configure your data source plugin to authenticate against a third-party API in one of either of two ways:
|
||||
|
||||
- Use the [_data source proxy_](#authenticate-using-the-data-source-proxy) method, or
|
||||
- Build a [_backend plugin_](#authenticate-using-a-backend-plugin).
|
||||
|
||||
| Case | Use |
|
||||
| ----------------------------------------------------------------------------------------------- | ------------------------------- |
|
||||
| Do you need to authenticate your plugin using Basic Auth or API keys? | Use the data source proxy. |
|
||||
| Does your API support OAuth 2.0 using client credentials? | Use the data source proxy. |
|
||||
| Does your API use a custom authentication method that isn't supported by the data source proxy? | Use a backend plugin. |
|
||||
| Does your API communicate over a protocol other than HTTP? | Build and use a backend plugin. |
|
||||
| Does your plugin require alerting support? | Build and use a backend plugin. |
|
||||
|
||||
## Encrypt data source configuration
|
||||
|
||||
Data source plugins have two ways of storing custom configuration: `jsonData` and `secureJsonData`.
|
||||
|
||||
Users with the Viewer role can access data source configuration such as the contents of `jsonData` in cleartext. If you've enabled anonymous access, anyone who can access Grafana in their browser can see the contents of `jsonData`.
|
||||
|
||||
Users of [Grafana Enterprise](/products/enterprise/grafana/) can restrict access to data sources to specific users and teams. For more information, refer to [Data source permissions](/docs/grafana/latest/enterprise/datasource_permissions).
|
||||
|
||||
> **Important:** Do not use `jsonData` with sensitive data such as password, tokens, and API keys. If you need to store sensitive information, use `secureJsonData` instead.
|
||||
|
||||
> **Note:** You can see the settings that the current user has access to by entering `window.grafanaBootData` in the developer console of your browser.
|
||||
|
||||
### Store configuration in `secureJsonData`
|
||||
|
||||
If you need to store sensitive information, use `secureJsonData` instead of `jsonData`. Whenever the user saves the data source configuration, the secrets in `secureJsonData` are sent to the Grafana server and encrypted before they're stored.
|
||||
|
||||
Once you have encrypted the secure configuration, it can no longer be accessed from the browser. The only way to access secrets after they've been saved is by using the [_data source proxy_](#authenticate-using-the-data-source-proxy).
|
||||
|
||||
### Add secret configuration to your data source plugin
|
||||
|
||||
To demonstrate how you can add secrets to a data source plugin, let's add support for configuring an API key.
|
||||
|
||||
1. Create a new interface in `types.ts` to hold the API key:
|
||||
```ts
|
||||
export interface MySecureJsonData {
|
||||
apiKey?: string;
|
||||
}
|
||||
```
|
||||
1. Add type information to your `secureJsonData` object by updating the props for your `ConfigEditor` to accept the interface as a second type parameter. Access the value of the secret from the `options` prop inside your `ConfigEditor`:
|
||||
|
||||
```ts
|
||||
interface Props extends DataSourcePluginOptionsEditorProps<MyDataSourceOptions, MySecureJsonData> {}
|
||||
```
|
||||
|
||||
```ts
|
||||
const { secureJsonData, secureJsonFields } = options;
|
||||
const { apiKey } = secureJsonData;
|
||||
```
|
||||
|
||||
> **Note:** You can do this until the user saves the configuration; when the user saves the configuration, Grafana clears the value. After that, you can use `secureJsonFields` to determine whether the property has been configured.
|
||||
|
||||
1. To securely update the secret in your plugin's configuration editor, update the `secureJsonData` object using the `onOptionsChange` prop:
|
||||
|
||||
```ts
|
||||
const onAPIKeyChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
onOptionsChange({
|
||||
...options,
|
||||
secureJsonData: {
|
||||
apiKey: event.target.value,
|
||||
},
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
1. Define a component that can accept user input:
|
||||
|
||||
```ts
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={secureJsonFields?.apiKey ? 'configured' : ''}
|
||||
value={secureJsonData.apiKey ?? ''}
|
||||
onChange={onAPIKeyChange}
|
||||
/>
|
||||
```
|
||||
|
||||
1. Optional: If you want the user to be able to reset the API key, then you need to set the property to `false` in the `secureJsonFields` object:
|
||||
|
||||
```ts
|
||||
const onResetAPIKey = () => {
|
||||
onOptionsChange({
|
||||
...options,
|
||||
secureJsonFields: {
|
||||
...options.secureJsonFields,
|
||||
apiKey: false,
|
||||
},
|
||||
secureJsonData: {
|
||||
...options.secureJsonData,
|
||||
apiKey: '',
|
||||
},
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
Now that users can configure secrets, the next step is to see how we can add them to our requests.
|
||||
|
||||
## Authenticate using the data source proxy
|
||||
|
||||
Once the user has saved the configuration for a data source, the secret data source configuration will no longer be available in the browser. Encrypted secrets can only be accessed on the server. So how do you add them to your request?
|
||||
|
||||
The Grafana server comes with a proxy that lets you define templates for your requests: _proxy routes_. Grafana sends the proxy route to the server, decrypts the secrets along with other configuration, and adds them to the request before sending it.
|
||||
|
||||
> **Note:** Be sure not to confuse the data source proxy with the [auth proxy]({{< relref "../../../../setup-grafana/configure-security/configure-authentication/auth-proxy/index.md" >}}). The data source proxy is used to authenticate a data source, while the auth proxy is used to log into Grafana itself.
|
||||
|
||||
### Add a proxy route to your plugin
|
||||
|
||||
To forward requests through the Grafana proxy, you need to configure one or more _proxy routes_. A proxy route is a template for any outgoing request that is handled by the proxy. You can configure proxy routes in the [plugin.json]({{< relref "../../metadata.md" >}}) file.
|
||||
|
||||
1. Add the route to `plugin.json`:
|
||||
|
||||
```json
|
||||
"routes": [
|
||||
{
|
||||
"path": "example",
|
||||
"url": "https://api.example.com"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
> **Note:** You need to restart the Grafana server every time you make a change to your `plugin.json` file.
|
||||
|
||||
1. In the `DataSource`, extract the proxy URL from `instanceSettings` to a class property called `url`:
|
||||
|
||||
```ts
|
||||
export class DataSource extends DataSourceApi<MyQuery, MyDataSourceOptions> {
|
||||
url?: string;
|
||||
|
||||
constructor(instanceSettings: DataSourceInstanceSettings<MyDataSourceOptions>) {
|
||||
super(instanceSettings);
|
||||
|
||||
this.url = instanceSettings.url;
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
1. In the `query` method, make a request using `BackendSrv`. The first section of the URL path needs to match the `path` of your proxy route. The data source proxy replaces `this.url + routePath` with the `url` of the route. Based on our example, the URL for the request would be `https://api.example.com/v1/users`:
|
||||
|
||||
```ts
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
```
|
||||
|
||||
```ts
|
||||
const routePath = '/example';
|
||||
|
||||
getBackendSrv().datasourceRequest({
|
||||
url: this.url + routePath + '/v1/users',
|
||||
method: 'GET',
|
||||
});
|
||||
```
|
||||
|
||||
### Add a dynamic proxy route to your plugin
|
||||
|
||||
Grafana sends the proxy route to the server, where the data source proxy decrypts any sensitive data and interpolates the template variables with the decrypted data before making the request.
|
||||
|
||||
To add user-defined configuration to your routes:
|
||||
|
||||
- Use `.JsonData` for configuration stored in `jsonData`. For example, where `projectId` is the name of a property in the `jsonData` object:
|
||||
|
||||
```json
|
||||
"routes": [
|
||||
{
|
||||
"path": "example",
|
||||
"url": "https://api.example.com/projects/{{ .JsonData.projectId }}"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
- Use `.SecureJsonData` for sensitive data stored in `secureJsonData`. For example, where `password` is the name of a property in the `secureJsonData` object:
|
||||
|
||||
```json
|
||||
"routes": [
|
||||
{
|
||||
"path": "example",
|
||||
"url": "https://{{ .JsonData.username }}:{{ .SecureJsonData.password }}@api.example.com"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
In addition to adding the URL to the proxy route, you can also add headers, URL parameters, and a request body.
|
||||
|
||||
#### Add HTTP headers to a proxy route
|
||||
|
||||
Here's an example of adding `name` and `content` as HTTP headers:
|
||||
|
||||
```json
|
||||
"routes": [
|
||||
{
|
||||
"path": "example",
|
||||
"url": "https://api.example.com",
|
||||
"headers": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"content": "Bearer {{ .SecureJsonData.apiToken }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Add URL parameters to a proxy route
|
||||
|
||||
Here's an example of adding `name` and `content` as URL parameters:
|
||||
|
||||
```json
|
||||
"routes": [
|
||||
{
|
||||
"path": "example",
|
||||
"url": "http://api.example.com",
|
||||
"urlParams": [
|
||||
{
|
||||
"name": "apiKey",
|
||||
"content": "{{ .SecureJsonData.apiKey }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Add a request body to a proxy route
|
||||
|
||||
Here's an example of adding `username` and `password` to the request body:
|
||||
|
||||
```json
|
||||
"routes": [
|
||||
{
|
||||
"path": "example",
|
||||
"url": "http://api.example.com",
|
||||
"body": {
|
||||
"username": "{{ .JsonData.username }}",
|
||||
"password": "{{ .SecureJsonData.password }}"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Add an OAuth 2.0 proxy route to your plugin
|
||||
|
||||
Since your request to each route is made server-side with OAuth 2.0 authentication, only machine-to-machine requests are supported. In order words, if you need to use a different grant than client credentials, you need to implement it yourself.
|
||||
|
||||
To authenticate using OAuth 2.0, add a `tokenAuth` object to the proxy route definition. If necessary, Grafana performs a request to the URL defined in `tokenAuth` to retrieve a token before making the request to the URL in your proxy route. Grafana automatically renews the token when it expires.
|
||||
|
||||
Any parameters defined in `tokenAuth.params` are encoded as `application/x-www-form-urlencoded` and sent to the token URL.
|
||||
|
||||
```json
|
||||
{
|
||||
"routes": [
|
||||
{
|
||||
"path": "api",
|
||||
"url": "https://api.example.com/v1",
|
||||
"tokenAuth": {
|
||||
"url": "https://api.example.com/v1/oauth/token",
|
||||
"params": {
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": "{{ .SecureJsonData.clientId }}",
|
||||
"client_secret": "{{ .SecureJsonData.clientSecret }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Authenticate using a backend plugin
|
||||
|
||||
While the data source proxy supports the most common authentication methods for HTTP APIs, using proxy routes has a few limitations:
|
||||
|
||||
- Proxy routes only support HTTP or HTTPS.
|
||||
- Proxy routes don't support custom token authentication.
|
||||
|
||||
If any of these limitations apply to your plugin, you need to add a [backend plugin]({{< relref "../../introduction-to-plugin-development/backend" >}}). Because backend plugins run on the server, they can access decrypted secrets, which makes it easier to implement custom authentication methods.
|
||||
|
||||
The decrypted secrets are available from the `DecryptedSecureJSONData` field in the instance settings.
|
||||
|
||||
```go
|
||||
func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
instanceSettings := req.PluginContext.DataSourceInstanceSettings
|
||||
|
||||
if apiKey, exists := settings.DecryptedSecureJSONData["apiKey"]; exists {
|
||||
// Use the decrypted API key.
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Forward OAuth identity for the logged-in user
|
||||
|
||||
If your data source uses the same OAuth provider as Grafana itself, for example using [Generic OAuth Authentication]({{< relref "../../../../setup-grafana/configure-security/configure-authentication/generic-oauth" >}}), then your data source plugin can reuse the access token for the logged-in Grafana user.
|
||||
|
||||
To allow Grafana to pass the access token to the plugin, update the data source configuration and set the `jsonData.oauthPassThru` property to `true`. The [DataSourceHttpSettings](https://developers.grafana.com/ui/latest/index.html?path=/story/data-source-datasourcehttpsettings--basic) settings provide a toggle, the **Forward OAuth Identity** option, for this. You can also build an appropriate toggle to set `jsonData.oauthPassThru` in your data source configuration page UI.
|
||||
|
||||
When configured, Grafana can forward authorization HTTP headers such as `Authorization` or `X-ID-Token` to a backend data source. This information is available across the `QueryData`, `CallResource` and `CheckHealth` requests.
|
||||
|
||||
To get Grafana to forward the headers, create a HTTP client using the [Grafana plugin SDK for Go](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend/httpclient) and set the `ForwardHTTPHeaders` option to `true` (by default, it's set to `false`). This package exposes request information which can be subsequently forwarded downstream and/or used directly within the plugin.
|
||||
|
||||
```go
|
||||
func NewDatasource(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
opts, err := settings.HTTPClientOptions()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("http client options: %w", err)
|
||||
}
|
||||
|
||||
// Important: Reuse the same client for each query to avoid using all available connections on a host.
|
||||
|
||||
opts.ForwardHTTPHeaders = true
|
||||
|
||||
cl, err := httpclient.New(opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("httpclient new: %w", err)
|
||||
}
|
||||
return &Datasource{
|
||||
httpClient: cl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
// Necessary to keep the Context, since the injected middleware is configured there
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://some-url", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("new request with context: %w", err)
|
||||
}
|
||||
// Authorization header will be automatically injected if oauthPassThru is configured
|
||||
resp, err := ds.httpClient.Do(req)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
You can see a full working plugin example here: [datasource-http-backend](https://github.com/grafana/grafana-plugin-examples/tree/main/examples/datasource-http-backend).
|
||||
|
||||
### Extract a header from an HTTP request
|
||||
|
||||
If you need to access the HTTP header information directly, you can also extract that information from the request:
|
||||
|
||||
```go
|
||||
func (ds *dataSource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
token := strings.Fields(req.GetHTTPHeader(backend.OAuthIdentityTokenHeaderName))
|
||||
var (
|
||||
tokenType = token[0]
|
||||
accessToken = token[1]
|
||||
)
|
||||
idToken := req.GetHTTPHeader(backend.OAuthIdentityIDTokenHeaderName) // present if user's token includes an ID token
|
||||
|
||||
// ...
|
||||
return &backend.CheckHealthResult{Status: backend.HealthStatusOk}, nil
|
||||
}
|
||||
|
||||
func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
token := strings.Fields(req.GetHTTPHeader(backend.OAuthIdentityTokenHeaderName))
|
||||
var (
|
||||
tokenType = token[0]
|
||||
accessToken = token[1]
|
||||
)
|
||||
idToken := req.GetHTTPHeader(backend.OAuthIdentityIDTokenHeaderName)
|
||||
|
||||
for _, q := range req.Queries {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *dataSource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
token := req.GetHTTPHeader(backend.OAuthIdentityTokenHeaderName)
|
||||
idToken := req.GetHTTPHeader(backend.OAuthIdentityIDTokenHeaderName)
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Work with cookies
|
||||
|
||||
### Forward cookies for the logged-in user
|
||||
|
||||
Your data source plugin can forward cookies for the logged-in Grafana user to the data source. Use the [DataSourceHttpSettings](https://developers.grafana.com/ui/latest/index.html?path=/story/data-source-datasourcehttpsettings--basic) component on the data source's configuration page. It provides the **Allowed cookies** option, where you can specify the cookie names.
|
||||
|
||||
When configured, as with [authorization headers](#forward-oauth-identity-for-the-logged-in-user), these cookies are automatically injected if you use the SDK HTTP client.
|
||||
|
||||
### Extract cookies for the logged-in user
|
||||
|
||||
You can also extract the cookies in the `QueryData`, `CallResource` and `CheckHealth` requests if required.
|
||||
|
||||
**`QueryData`**
|
||||
|
||||
```go
|
||||
func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
cookies:= req.GetHTTPHeader(backend.CookiesHeaderName)
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**`CallResource`**
|
||||
|
||||
```go
|
||||
func (ds *dataSource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
cookies:= req.GetHTTPHeader(backend.CookiesHeaderName)
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**`CheckHealth`**
|
||||
|
||||
```go
|
||||
func (ds *dataSource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
cookies:= req.GetHTTPHeader(backend.CookiesHeaderName)
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Forward user header for the logged-in user
|
||||
|
||||
When `send_user_header` is enabled, Grafana passes the user header to the plugin using the `X-Grafana-User` header. You can forward this header as well as [authorization headers](#forward-oauth-identity-for-the-logged-in-user) or [configured cookies](#forward-cookies-for-the-logged-in-user).
|
||||
|
||||
**`QueryData`**
|
||||
|
||||
```go
|
||||
func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
u := req.GetHTTPHeader("X-Grafana-User")
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**`CallResource`**
|
||||
|
||||
```go
|
||||
func (ds *dataSource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
u := req.GetHTTPHeader("X-Grafana-User")
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**`CheckHealth`**
|
||||
|
||||
```go
|
||||
func (ds *dataSource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
u := req.GetHTTPHeader("X-Grafana-User")
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
@ -0,0 +1,122 @@
|
||||
---
|
||||
title: Add distributed tracing for backend plugins
|
||||
aliases:
|
||||
- ../../../plugins/add-distributed-tracing-for-backend-plugins/
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- distributed tracing
|
||||
- tracing
|
||||
- backend
|
||||
- back-end
|
||||
description: How to add distributed tracing for backend plugins.
|
||||
weight: 350
|
||||
---
|
||||
|
||||
# Add distributed tracing for backend plugins
|
||||
|
||||
> **Note:** This feature requires at least Grafana 9.5.0, and your plugin needs to be built at least with grafana-plugins-sdk-go v0.157.0. If you run a plugin with tracing features on an older version of Grafana, tracing is disabled.
|
||||
|
||||
Distributed tracing allows backend plugin developers to create custom spans in their plugins, and send them to the same endpoint and with the same propagation format as the main Grafana instance. The tracing context is also propagated from the Grafana instance to the plugin, so the plugin's spans will be correlated to the correct trace.
|
||||
|
||||
## Plugin configuration
|
||||
|
||||
Plugin tracing must be enabled manually on a per-plugin basis, by specifying `tracing = true` in the plugin's config section:
|
||||
|
||||
```ini
|
||||
[plugin.myorg-myplugin-datasource]
|
||||
tracing = true
|
||||
```
|
||||
|
||||
## OpenTelemetry configuration in Grafana
|
||||
|
||||
Grafana supports [OpenTelemetry](https://opentelemetry.io/) for distributed tracing. If Grafana is configured to use a deprecated tracing system (Jaeger or OpenTracing), then tracing is disabled in the plugin provided by the SDK and configured when calling `datasource.Manage | app.Manage`.
|
||||
|
||||
OpenTelemetry must be enabled and configured for the Grafana instance. Please refer to the [Grafana configuration documentation](
|
||||
{{< relref "../../../../setup-grafana/configure-grafana#tracingopentelemetry" >}}) for more information.
|
||||
|
||||
Refer to the [OpenTelemetry Go SDK](https://pkg.go.dev/go.opentelemetry.io/otel) for in-depth documentation about all the features provided by OpenTelemetry.
|
||||
|
||||
> **Note:** If tracing is disabled in Grafana, `backend.DefaultTracer()` returns a no-op tracer.
|
||||
|
||||
## Implement tracing in your plugin
|
||||
|
||||
> **Note:** Make sure you are using at least grafana-plugin-sdk-go v0.157.0. You can update with `go get -u github.com/grafana/grafana-plugin-sdk-go`.
|
||||
|
||||
### Configure a global tracer
|
||||
|
||||
When OpenTelemetry tracing is enabled on the main Grafana instance and tracing is enabled for a plugin, the OpenTelemetry endpoint address and propagation format is passed to the plugin during startup. These parameters are used to configure a global tracer.
|
||||
|
||||
1. Use `datasource.Manage` or `app.Manage` to run your plugin to automatically configure the global tracer. Specify any custom attributes for the default tracer using `CustomAttributes`:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
if err := datasource.Manage("MY_PLUGIN_ID", plugin.NewDatasource, datasource.ManageOpts{
|
||||
TracingOpts: tracing.Opts{
|
||||
// Optional custom attributes attached to the tracer's resource.
|
||||
// The tracer will already have some SDK and runtime ones pre-populated.
|
||||
CustomAttributes: []attribute.KeyValue{
|
||||
attribute.String("my_plugin.my_attribute", "custom value"),
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
log.DefaultLogger.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. Once you have configured tracing, use the global tracer like this:
|
||||
|
||||
```go
|
||||
tracing.DefaultTracer()
|
||||
```
|
||||
|
||||
This returns an [OpenTelemetry `trace.Tracer`](https://pkg.go.dev/go.opentelemetry.io/otel/trace#Tracer) for creating spans.
|
||||
|
||||
**Example:**
|
||||
|
||||
```go
|
||||
func (d *Datasource) query(ctx context.Context, pCtx backend.PluginContext, query backend.DataQuery) (backend.DataResponse, error) {
|
||||
ctx, span := tracing.DefaultTracer().Start(
|
||||
ctx,
|
||||
"query processing",
|
||||
trace.WithAttributes(
|
||||
attribute.String("query.ref_id", query.RefID),
|
||||
attribute.String("query.type", query.QueryType),
|
||||
attribute.Int64("query.max_data_points", query.MaxDataPoints),
|
||||
attribute.Int64("query.interval_ms", query.Interval.Milliseconds()),
|
||||
attribute.Int64("query.time_range.from", query.TimeRange.From.Unix()),
|
||||
attribute.Int64("query.time_range.to", query.TimeRange.To.Unix()),
|
||||
),
|
||||
)
|
||||
defer span.End()
|
||||
log.DefaultLogger.Debug("query", "traceID", trace.SpanContextFromContext(ctx).TraceID())
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Tracing gRPC calls
|
||||
|
||||
When tracing is enabled, a new span is created automatically for each gRPC call (`QueryData`, `CheckHealth`, etc.), both on Grafana's side and on the plugin's side. The plugin SDK also injects the trace context into the `context.Context` that is passed to those methods.
|
||||
|
||||
You can retrieve the [trace.SpanContext](https://pkg.go.dev/go.opentelemetry.io/otel/trace#SpanContext) with `tracing.SpanContextFromContext` by passing the original `context.Context` to it:
|
||||
|
||||
```go
|
||||
func (d *Datasource) query(ctx context.Context, pCtx backend.PluginContext, query backend.DataQuery) (backend.DataResponse, error) {
|
||||
spanCtx := trace.SpanContextFromContext(ctx)
|
||||
traceID := spanCtx.TraceID()
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Tracing HTTP requests
|
||||
|
||||
When tracing is enabled, a `TracingMiddleware` is also added to the default middleware stack to all HTTP clients created using the `httpclient.New` or `httpclient.NewProvider`, unless you specify custom middleware. This middleware creates spans for each outgoing HTTP request and provides some useful attributes and events related to the request's lifecycle.
|
||||
|
||||
## Plugin example
|
||||
|
||||
Refer to the [datasource-http-backend plugin example](https://github.com/grafana/grafana-plugin-examples/tree/main/examples/datasource-http-backend) for a complete example of a plugin with full distributed tracing support.
|
@ -0,0 +1,84 @@
|
||||
---
|
||||
title: Add query editor help
|
||||
aliases:
|
||||
- ../../../plugins/add-query-editor-help/
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- queries
|
||||
- query editor
|
||||
- query editor help
|
||||
description: How to add a help component to query editors in Grafana.
|
||||
weight: 500
|
||||
---
|
||||
|
||||
# Add query editor help
|
||||
|
||||
Query editors support the addition of a help component to display examples of potential queries. When the user clicks on one of the examples, the query editor is automatically updated. This helps the user to make faster queries.
|
||||
|
||||
1. In the `src` directory of your plugin, create a file `QueryEditorHelp.tsx` with the following content:
|
||||
|
||||
```ts
|
||||
import React from 'react';
|
||||
import { QueryEditorHelpProps } from '@grafana/data';
|
||||
|
||||
export default (props: QueryEditorHelpProps) => {
|
||||
return <h2>My cheat sheet</h2>;
|
||||
};
|
||||
```
|
||||
|
||||
1. Configure the plugin to use `QueryEditorHelp`:
|
||||
|
||||
```ts
|
||||
import QueryEditorHelp from './QueryEditorHelp';
|
||||
```
|
||||
|
||||
```ts
|
||||
export const plugin = new DataSourcePlugin<DataSource, MyQuery, MyDataSourceOptions>(DataSource)
|
||||
.setConfigEditor(ConfigEditor)
|
||||
.setQueryEditor(QueryEditor)
|
||||
.setQueryEditorHelp(QueryEditorHelp);
|
||||
```
|
||||
|
||||
1. Create a few examples of potential queries:
|
||||
|
||||
```ts
|
||||
import React from 'react';
|
||||
import { QueryEditorHelpProps, DataQuery } from '@grafana/data';
|
||||
|
||||
const examples = [
|
||||
{
|
||||
title: 'Addition',
|
||||
expression: '1 + 2',
|
||||
label: 'Add two integers',
|
||||
},
|
||||
{
|
||||
title: 'Subtraction',
|
||||
expression: '2 - 1',
|
||||
label: 'Subtract an integer from another',
|
||||
},
|
||||
];
|
||||
|
||||
export default (props: QueryEditorHelpProps) => {
|
||||
return (
|
||||
<div>
|
||||
<h2>Cheat Sheet</h2>
|
||||
{examples.map((item, index) => (
|
||||
<div className="cheat-sheet-item" key={index}>
|
||||
<div className="cheat-sheet-item__title">{item.title}</div>
|
||||
{item.expression ? (
|
||||
<div
|
||||
className="cheat-sheet-item__example"
|
||||
onClick={(e) => props.onClickExample({ refId: 'A', queryText: item.expression } as DataQuery)}
|
||||
>
|
||||
<code>{item.expression}</code>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="cheat-sheet-item__label">{item.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Enable annotations
|
||||
menuTitle: Enable annotations
|
||||
aliases:
|
||||
- ../../../plugins/add-support-for-annotations/
|
||||
description: Add support for annotations in your plugin.
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- annotations
|
||||
weight: 100
|
||||
---
|
||||
|
||||
# Enable annotations
|
||||
|
||||
You can add support to your plugin for annotations that will insert information into Grafana alerts. This guide explains how to add support for [annotations]({{< relref "../../../../dashboards/build-dashboards/annotate-visualizations#querying-other-data-sources" >}}) to a data source plugin.
|
||||
|
||||
## Support annotations in your data source plugin
|
||||
|
||||
To enable annotations, simply add two lines of code to your plugin. Grafana uses your default query editor for editing annotation queries.
|
||||
|
||||
1. Add `"annotations": true` to the [plugin.json]({{< relref "../../metadata.md" >}}) file to let Grafana know that your plugin supports annotations.
|
||||
|
||||
**In `plugin.json`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"annotations": true
|
||||
}
|
||||
```
|
||||
|
||||
2. In `datasource.ts`, override the `annotations` property from `DataSourceApi` (or `DataSourceWithBackend` for backend data sources). For the default behavior, set `annotations` to an empty object.
|
||||
|
||||
**In `datasource.ts`:**
|
||||
|
||||
```ts
|
||||
annotations: {
|
||||
}
|
||||
```
|
@ -0,0 +1,81 @@
|
||||
---
|
||||
title: Add features to Explore queries
|
||||
aliases:
|
||||
- ../../../plugins/add-support-for-explore-queries/
|
||||
description: Add features to Explore queries.
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- queries
|
||||
- explore queries
|
||||
- explore
|
||||
weight: 400
|
||||
---
|
||||
|
||||
# Add features to Explore queries
|
||||
|
||||
[Explore]({{< relref "../../../../explore" >}}) allows users can make ad-hoc queries without the use of a dashboard. This is useful when they want to troubleshoot or learn more about the data.
|
||||
|
||||
Your data source supports Explore by default and uses the existing query editor for the data source. This guide explains how to extend functionality for Explore queries in a data source plugin.
|
||||
|
||||
## Add an Explore-specific query editor
|
||||
|
||||
To extend Explore functionality for your data source, define an Explore-specific query editor.
|
||||
|
||||
1. Create a file `ExploreQueryEditor.tsx` in the `src` directory of your plugin, with content similar to this:
|
||||
|
||||
```ts
|
||||
import React from 'react';
|
||||
|
||||
import { QueryEditorProps } from '@grafana/data';
|
||||
import { QueryField } from '@grafana/ui';
|
||||
import { DataSource } from './DataSource';
|
||||
import { MyQuery, MyDataSourceOptions } from './types';
|
||||
|
||||
type Props = QueryEditorProps<DataSource, MyQuery, MyDataSourceOptions>;
|
||||
|
||||
export default (props: Props) => {
|
||||
return <h2>My Explore-specific query editor</h2>;
|
||||
};
|
||||
```
|
||||
|
||||
1. Modify your base query editor in `QueryEditor.tsx` to render the Explore-specific query editor. For example:
|
||||
|
||||
```ts
|
||||
// [...]
|
||||
import { CoreApp } from '@grafana/data';
|
||||
import ExploreQueryEditor from './ExploreQueryEditor';
|
||||
|
||||
type Props = QueryEditorProps<DataSource, MyQuery, MyDataSourceOptions>;
|
||||
|
||||
export default (props: Props) => {
|
||||
const { app } = props;
|
||||
|
||||
switch (app) {
|
||||
case CoreApp.Explore:
|
||||
return <ExploreQueryEditor {...props} />;
|
||||
default:
|
||||
return <div>My base query editor</div>;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Select a preferred visualization type
|
||||
|
||||
By default, Explore should select an appropriate and useful visualization for your data. It can figure out whether the returned data is time series data or logs or something else, and creates the right type of visualization.
|
||||
|
||||
However, if you want a custom visualization, you can add a hint to your returned data frame by setting the `meta' attribute to `preferredVisualisationType`.
|
||||
|
||||
Construct a data frame with specific metadata like this:
|
||||
|
||||
```
|
||||
const firstResult = new MutableDataFrame({
|
||||
fields: [...],
|
||||
meta: {
|
||||
preferredVisualisationType: 'logs',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
For possible options, refer to [PreferredVisualisationType](https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/types/data.ts#L25).
|
@ -0,0 +1,208 @@
|
||||
---
|
||||
title: Add support for variables
|
||||
aliases:
|
||||
- ../../../plugins/add-support-for-variables/
|
||||
description: Add support for variables.
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- queries
|
||||
- variables
|
||||
weight: 600
|
||||
---
|
||||
|
||||
# Add support for variables
|
||||
|
||||
Variables are placeholders for values, and you can use them to create templated queries, and dashboard or panel links. For more information on variables, refer to [Templates and variables]({{< relref "../../../../dashboards/variables" >}}).
|
||||
|
||||
In this guide, you'll see how you can turn a query string like this:
|
||||
|
||||
```sql
|
||||
SELECT * FROM services WHERE id = "$service"
|
||||
```
|
||||
|
||||
into
|
||||
|
||||
```sql
|
||||
SELECT * FROM services WHERE id = "auth-api"
|
||||
```
|
||||
|
||||
Grafana provides a couple of helper functions to interpolate variables in a string template. Let's see how you can use them in your plugin.
|
||||
|
||||
## Interpolate variables in panel plugins
|
||||
|
||||
For panels, the `replaceVariables` function is available in the `PanelProps`.
|
||||
|
||||
Add `replaceVariables` to the argument list, and pass a user-defined template string to it:
|
||||
|
||||
```ts
|
||||
export function SimplePanel({ options, data, width, height, replaceVariables }: Props) {
|
||||
const query = replaceVariables('Now displaying $service');
|
||||
|
||||
return <div>{query}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## Interpolate variables in data source plugins
|
||||
|
||||
For data sources, you need to use the `getTemplateSrv`, which returns an instance of `TemplateSrv`.
|
||||
|
||||
1. Import `getTemplateSrv` from the `runtime` package:
|
||||
|
||||
```ts
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
```
|
||||
|
||||
1. In your `query` method, call the `replace` method with a user-defined template string:
|
||||
|
||||
```ts
|
||||
async query(options: DataQueryRequest<MyQuery>): Promise<DataQueryResponse> {
|
||||
const query = getTemplateSrv().replace('SELECT * FROM services WHERE id = "$service"', options.scopedVars);
|
||||
|
||||
const data = makeDbQuery(query);
|
||||
|
||||
return { data };
|
||||
}
|
||||
```
|
||||
|
||||
## Format multi-value variables
|
||||
|
||||
When a user selects multiple values for a variable, the value of the interpolated variable depends on the [variable format]({{< relref "../../../../dashboards/variables/variable-syntax#advanced-variable-format-options" >}}).
|
||||
|
||||
A data source plugin can define the default format option when no format is specified by adding a third argument to the interpolation function.
|
||||
|
||||
Let's change the SQL query to use CSV format by default:
|
||||
|
||||
```ts
|
||||
getTemplateSrv().replace('SELECT * FROM services WHERE id IN ($service)', options.scopedVars, 'csv');
|
||||
```
|
||||
|
||||
Now, when users write `$service`, the query looks like this:
|
||||
|
||||
```sql
|
||||
SELECT * FROM services WHERE id IN (admin,auth,billing)
|
||||
```
|
||||
|
||||
For more information on the available variable formats, refer to [Advanced variable format options]({{< relref "../../../../dashboards/variables/variable-syntax/index.md#advanced-variable-format-options" >}}).
|
||||
|
||||
## Set a variable from your plugin
|
||||
|
||||
Not only can you read the value of a variable, you can also update the variable from your plugin. Use `locationService.partial(query, replace)`.
|
||||
|
||||
The following example shows how to update a variable called `service`.
|
||||
|
||||
- `query` contains the query parameters you want to update. The query parameters that control variables are prefixed with `var-`.
|
||||
- `replace: true` tells Grafana to update the current URL state rather than creating a new history entry.
|
||||
|
||||
```ts
|
||||
import { locationService } from '@grafana/runtime';
|
||||
```
|
||||
|
||||
```ts
|
||||
locationService.partial({ 'var-service': 'billing' }, true);
|
||||
```
|
||||
|
||||
> **Note:** Grafana queries your data source whenever you update a variable. Excessive updates to variables can slow down Grafana and lead to a poor user experience.
|
||||
|
||||
## Add support for query variables to your data source
|
||||
|
||||
A [query variable]({{< relref "../../../../dashboards/variables/add-template-variables#add-a-query-variable" >}}) is a type of variable that allows you to query a data source for the values. By adding support for query variables to your data source plugin, users can create dynamic dashboards based on data from your data source.
|
||||
|
||||
Let's start by defining a query model for the variable query:
|
||||
|
||||
```ts
|
||||
export interface MyVariableQuery {
|
||||
namespace: string;
|
||||
rawQuery: string;
|
||||
}
|
||||
```
|
||||
|
||||
For a data source to support query variables, override the `metricFindQuery` in your `DataSourceApi` class. The `metricFindQuery` function returns an array of `MetricFindValue` which has a single property, `text`:
|
||||
|
||||
```ts
|
||||
async metricFindQuery(query: MyVariableQuery, options?: any) {
|
||||
// Retrieve DataQueryResponse based on query.
|
||||
const response = await this.fetchMetricNames(query.namespace, query.rawQuery);
|
||||
|
||||
// Convert query results to a MetricFindValue[]
|
||||
const values = response.data.map(frame => ({ text: frame.name }));
|
||||
|
||||
return values;
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** By default, Grafana provides a basic query model and editor for simple text queries. If that's all you need, then leave the query type as `string`:
|
||||
|
||||
```ts
|
||||
async metricFindQuery(query: string, options?: any)
|
||||
```
|
||||
|
||||
Let's create a custom query editor to allow the user to edit the query model.
|
||||
|
||||
1. Create a `VariableQueryEditor` component:
|
||||
|
||||
```ts
|
||||
import React, { useState } from 'react';
|
||||
import { MyVariableQuery } from './types';
|
||||
|
||||
interface VariableQueryProps {
|
||||
query: MyVariableQuery;
|
||||
onChange: (query: MyVariableQuery, definition: string) => void;
|
||||
}
|
||||
|
||||
export const VariableQueryEditor = ({ onChange, query }: VariableQueryProps) => {
|
||||
const [state, setState] = useState(query);
|
||||
|
||||
const saveQuery = () => {
|
||||
onChange(state, `${state.query} (${state.namespace})`);
|
||||
};
|
||||
|
||||
const handleChange = (event: React.FormEvent<HTMLInputElement>) =>
|
||||
setState({
|
||||
...state,
|
||||
[event.currentTarget.name]: event.currentTarget.value,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="gf-form">
|
||||
<span className="gf-form-label width-10">Namespace</span>
|
||||
<input
|
||||
name="namespace"
|
||||
className="gf-form-input"
|
||||
onBlur={saveQuery}
|
||||
onChange={handleChange}
|
||||
value={state.namespace}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form">
|
||||
<span className="gf-form-label width-10">Query</span>
|
||||
<input
|
||||
name="rawQuery"
|
||||
className="gf-form-input"
|
||||
onBlur={saveQuery}
|
||||
onChange={handleChange}
|
||||
value={state.rawQuery}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Grafana saves the query model whenever one of the text fields loses focus (`onBlur`) and then previews the values returned by `metricFindQuery`.
|
||||
|
||||
The second argument to `onChange` allows you to set a text representation of the query that will appear next to the name of the variable in the variables list.
|
||||
|
||||
1. Configure your plugin to use the query editor:
|
||||
|
||||
```ts
|
||||
import { VariableQueryEditor } from './VariableQueryEditor';
|
||||
|
||||
export const plugin = new DataSourcePlugin<DataSource, MyQuery, MyDataSourceOptions>(DataSource)
|
||||
.setQueryEditor(QueryEditor)
|
||||
.setVariableQueryEditor(VariableQueryEditor);
|
||||
```
|
||||
|
||||
That's it! You can now try out the plugin by adding a [query variable]({{< relref "../../../../dashboards/variables/add-template-variables#add-a-query-variable" >}}) to your dashboard.
|
@ -0,0 +1,85 @@
|
||||
---
|
||||
title: Work with cross-plugin links
|
||||
aliases:
|
||||
- ../../../plugins/cross-plugin-linking/
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- links
|
||||
- cross-plugin links
|
||||
- extensions
|
||||
- extensions api
|
||||
description: Learn how to add plugin links to a Grafana app plugin.
|
||||
weight: 800
|
||||
---
|
||||
|
||||
# Work with cross-plugin links
|
||||
|
||||
With the Plugins extension API, app plugins can register extension points of their own to display other plugins links. This is called _cross-plugin linking_, and you can use it to create more immersive user experiences with installed plugins.
|
||||
|
||||
## Available extension points within plugins
|
||||
|
||||
An extension point is a location in another plugin's UI where your plugin can insert links. All extension point IDs within plugins should follow the naming convention `plugins/<plugin-id>/<extension-point-id>`.
|
||||
|
||||
## How to create an extension point within a plugin
|
||||
|
||||
Use the `getPluginExtensions` method in `@grafana/runtime` to create an extension point within your plugin. An extension point is a way to specify where in the plugin UI other plugins links are rendered.
|
||||
|
||||
{{% admonition type="note" %}}
|
||||
Creating an extension point in a plugin creates a public interface for other plugins to interact with. Changes to the extension point ID or its context could break any plugin that attempts to register a link inside your plugin.
|
||||
{{% /admonition %}}
|
||||
|
||||
The `getPluginExtensions` method takes an object consisting of the `extensionPointId`, which must begin `plugin/<pluginId>`, and any contextual information that you want to provide. The `getPluginExtensions` method returns a list of `extensionLinks` that your program can loop over:
|
||||
|
||||
```typescript
|
||||
import { getPluginExtensions } from '@grafana/runtime';
|
||||
import { isPluginExtensionLink } from '@grafana/data';
|
||||
import { LinkButton } from '@grafana/ui';
|
||||
|
||||
function AppExtensionPointExample() {
|
||||
const { extensions } = getPluginExtensions({
|
||||
extensionPointId: 'plugin/another-app-plugin/menu',
|
||||
context: {
|
||||
pluginId: 'another-app-plugin',
|
||||
},
|
||||
});
|
||||
|
||||
if (extensions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{extensions.map((extension) => {
|
||||
if (isPluginExtensionLink(extension)) {
|
||||
return (
|
||||
<LinkButton href={extension.path} title={extension.description} key={extension.key}>
|
||||
{extension.title}
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
The preceding example shows a component that renders `<LinkButton />` components for all link extensions that other plugins registered for the `plugin/another-app-plugin/menu` extension point ID. The context is passed as the second parameter to `getPluginExtensions`, which uses `Object.freeze` to make the context immutable before passing it to other plugins.
|
||||
|
||||
## Insert links into another plugin
|
||||
|
||||
Create links for other plugins in the same way you [extend the Grafana application UI]({{< relref "./extend-the-grafana-ui-with-links" >}}) with a link. Don't specify a `grafana/...` extension point. Instead, specify the plugin extension point `plugin/<pluginId>/<extensionPointId>`.
|
||||
|
||||
Given the preceding example, use a plugin link such as the following:
|
||||
|
||||
```typescript
|
||||
new AppPlugin().configureExtensionLink({
|
||||
title: 'Go to basic app',
|
||||
description: 'Will navigate the user to the basic app',
|
||||
extensionPointId: 'plugin/another-app-plugin/menu',
|
||||
path: '/a/myorg-basic-app/one',
|
||||
});
|
||||
```
|
@ -0,0 +1,132 @@
|
||||
---
|
||||
title: Build a custom panel option editor
|
||||
aliases:
|
||||
- ../../../plugins/custom-panel-option-editors/
|
||||
description: How to build a custom panel option editor.
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- custom panel option editor
|
||||
- customizing panel options
|
||||
- panel options
|
||||
weight: 700
|
||||
---
|
||||
|
||||
# Build a custom panel option editor
|
||||
|
||||
The Grafana plugin platform comes with a range of editors that allow your users to customize a panel. The standard editors cover the most common types of options, such as text input and boolean switches. If you don't find the editor you're looking for, you can build your own.
|
||||
|
||||
## Panel option editor basics
|
||||
|
||||
The simplest editor is a React component that accepts two props:
|
||||
|
||||
- **`value`**: the current value of the option
|
||||
- **`onChange`**: updates the option's value
|
||||
|
||||
The editor in the example below lets the user toggle a boolean value by clicking a button:
|
||||
|
||||
**SimpleEditor.tsx**
|
||||
|
||||
```ts
|
||||
import React from 'react';
|
||||
import { Button } from '@grafana/ui';
|
||||
import { StandardEditorProps } from '@grafana/data';
|
||||
|
||||
export const SimpleEditor = ({ value, onChange }: StandardEditorProps<boolean>) => {
|
||||
return <Button onClick={() => onChange(!value)}>{value ? 'Disable' : 'Enable'}</Button>;
|
||||
};
|
||||
```
|
||||
|
||||
To use a custom panel option editor, use the `addCustomEditor` on the `OptionsUIBuilder` object in your `module.ts` file and set the `editor` property to the name of your custom editor component.
|
||||
|
||||
**module.ts**
|
||||
|
||||
```ts
|
||||
export const plugin = new PanelPlugin<SimpleOptions>(SimplePanel).setPanelOptions((builder) => {
|
||||
return builder.addCustomEditor({
|
||||
id: 'label',
|
||||
path: 'label',
|
||||
name: 'Label',
|
||||
editor: SimpleEditor,
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Add settings to your panel option editor
|
||||
|
||||
You can use your custom editor to customize multiple possible settings. To add settings to your editor, set the second template variable of `StandardEditorProps` to an interface that contains the settings you want to configure. Access the editor settings through the `item` prop.
|
||||
|
||||
Here's an example of an editor that populates a drop-down with a range of numbers. The `Settings` interface defines the range of the `from` and `to` properties.
|
||||
|
||||
**SimpleEditor.tsx**
|
||||
|
||||
```ts
|
||||
interface Settings {
|
||||
from: number;
|
||||
to: number;
|
||||
}
|
||||
|
||||
type Props = StandardEditorProps<number, Settings>;
|
||||
|
||||
export const SimpleEditor = ({ item, value, onChange }: Props) => {
|
||||
const options: Array<SelectableValue<number>> = [];
|
||||
|
||||
// Default values
|
||||
const from = item.settings?.from ?? 1;
|
||||
const to = item.settings?.to ?? 10;
|
||||
|
||||
for (let i = from; i <= to; i++) {
|
||||
options.push({
|
||||
label: i.toString(),
|
||||
value: i,
|
||||
});
|
||||
}
|
||||
|
||||
return <Select options={options} value={value} onChange={(selectableValue) => onChange(selectableValue.value)} />;
|
||||
};
|
||||
```
|
||||
|
||||
You can now configure the editor for each option by configuring the `settings` property to call `addCustomEditor`:
|
||||
|
||||
```ts
|
||||
export const plugin = new PanelPlugin<SimpleOptions>(SimplePanel).setPanelOptions((builder) => {
|
||||
return builder.addCustomEditor({
|
||||
id: 'index',
|
||||
path: 'index',
|
||||
name: 'Index',
|
||||
editor: SimpleEditor,
|
||||
settings: {
|
||||
from: 1,
|
||||
to: 10,
|
||||
},
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Use query results in your panel option editor
|
||||
|
||||
Option editors can access the results from the last query. This lets you update your editor dynamically based on the data returned by the data source.
|
||||
|
||||
The editor context is available through the `context` prop. The data frames returned by the data source are available under `context.data`.
|
||||
|
||||
**SimpleEditor.tsx**
|
||||
|
||||
```ts
|
||||
export const SimpleEditor = ({ item, value, onChange, context }: StandardEditorProps<string>) => {
|
||||
const options: SelectableValue<string>[] = [];
|
||||
|
||||
if (context.data) {
|
||||
const frames = context.data;
|
||||
|
||||
for (let i = 0; i < frames.length; i++) {
|
||||
options.push({
|
||||
label: frames[i].name,
|
||||
value: frames[i].name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return <Select options={options} value={value} onChange={(selectableValue) => onChange(selectableValue.value)} />;
|
||||
};
|
||||
```
|
@ -0,0 +1,142 @@
|
||||
---
|
||||
title: Use extensions to add links to app plugins
|
||||
aliases:
|
||||
- ../../../plugins/extend-the-grafana-ui-with-links/
|
||||
keywords:
|
||||
- grafana
|
||||
- plugins
|
||||
- plugin
|
||||
- links
|
||||
- extensions
|
||||
- app plugins
|
||||
description: Learn how to add links to the Grafana user interface from an app plugin
|
||||
weight: 760
|
||||
---
|
||||
|
||||
# Use extensions to add links to app plugins
|
||||
|
||||
You can use the Plugin extensions API with your Grafana app plugins to add links to the Grafana UI. This feature lets you send users to your plugin's pages from other spots in the Grafana application.
|
||||
|
||||
## Before you begin
|
||||
|
||||
Be sure your plugin meets the following requirements before proceeding:
|
||||
|
||||
- It must be an app plugin.
|
||||
- It must be preloaded (by setting the [preload property]({{< relref "../../metadata.md" >}}) to `true` in the `plugin.json`
|
||||
- It must be installed and enabled.
|
||||
|
||||
## Available extension points within Grafana
|
||||
|
||||
An _extension point_ is a location within the Grafana UI where a plugin can insert links. The IDs of all extension points within Grafana start with `grafana/`. For example, you can use the following extension point ID:
|
||||
|
||||
- `grafana/dashboard/panel/menu`: extension point for all panel dropdown menus in dashboards
|
||||
|
||||
## Add a link extension within a Grafana dashboard panel menu
|
||||
|
||||
To add a link extension within a Grafana dashboard panel menu, complete the following steps:
|
||||
|
||||
1. Define the link extension in your plugin's `module.ts` file.
|
||||
|
||||
1. Define a new instance of the `AppPlugin` class by using the `configureExtensionLink` method. This method requires:
|
||||
- an object that describes your link extension, including a `title` property for the link text
|
||||
- an `extensionPointId` method that tells Grafana where the link should appear
|
||||
- a `path` for the user to go to your plugin
|
||||
|
||||
```typescript
|
||||
new AppPlugin().configureExtensionLink({
|
||||
title: 'Go to basic app',
|
||||
description: 'Will send the user to the basic app',
|
||||
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||
path: '/a/myorg-basic-app/one', // Must start with "/a/<PLUGIN_ID>/"
|
||||
});
|
||||
```
|
||||
|
||||
Your link will now appear in dashboard panel menus. When the user clicks the link, they will be sent to the path you defined earlier.
|
||||
|
||||
{{% admonition type="note" %}} Each plugin is limited to a maximum of two links per extension point.{{%
|
||||
/admonition %}}
|
||||
|
||||
## Add a link extension using context within Grafana
|
||||
|
||||
The above example works for simple cases. However, you may want to act on information from the app's panel from which the user is navigating.
|
||||
|
||||
To do this, use the `configure` property on the object that is passed to `configureExtensionLink()`. This property takes a function and returns an object that consists of a `title` property for the link text and a `path` to send the user to your plugin.
|
||||
|
||||
Alternatively, if you need to hide the link for certain scenarios, define the function to return _undefined_:
|
||||
|
||||
```typescript
|
||||
new AppPlugin().configureExtensionLink({
|
||||
title: 'Go to basic app',
|
||||
description: 'Will send the user to the basic app',
|
||||
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||
path: '/a/myorg-basic-app/one',
|
||||
configure: (context: PanelContext) => {
|
||||
switch (context?.pluginId) {
|
||||
case 'timeseries':
|
||||
return {
|
||||
title: 'Go to page one',
|
||||
description: 'hello',
|
||||
path: '/a/myorg-basic-app/one',
|
||||
};
|
||||
|
||||
case 'piechart':
|
||||
return {
|
||||
title: 'Go to page two',
|
||||
path: '/a/myorg-basic-app/two',
|
||||
};
|
||||
|
||||
// Returning undefined tells Grafana to hide the link
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The above example demonstrates how to return a different `path` based on which plugin the dashboard panel is using. If the clicked-upon panel is neither a time series nor a pie chart panel, then the `configure()` function returns _undefined_. When this happens, Grafana doesn't render the link.
|
||||
|
||||
{{% admonition type="note" %}} The context passed to the `configure()` function is bound by the `extensionPointId` into which you insert the link. Different extension points contain different contexts.{{%
|
||||
/admonition %}}
|
||||
|
||||
## Add an event handler to a link
|
||||
|
||||
Link extensions give you the means to direct users to a plugin page via href links within the Grafana UI. You can also use them to trigger `onClick` events to perform dynamic actions when clicked.
|
||||
|
||||
To add an event handler to a link in a panel menu, complete the following steps:
|
||||
|
||||
1. Define the link extension in the plugin's `module.ts` file.
|
||||
1. Create a new instance of the `AppPlugin` class, again using the `configureExtensionLink` method. This time, add an `onClick` property which takes a function. This function receives the click event and an object consisting of the `context` and an `openModal` function.
|
||||
|
||||
In the following example, we open a dialog.
|
||||
|
||||
```typescript
|
||||
new AppPlugin().configureExtensionLink({
|
||||
title: 'Go to basic app',
|
||||
description: 'Will send the user to the basic app',
|
||||
extensionPointId: 'grafana/dashboard/panel/menu',
|
||||
path: '/a/myorg-basic-app/one',
|
||||
onClick: (event, { context, openModal }) => {
|
||||
event.preventDefault();
|
||||
openModal({
|
||||
title: 'My plugin dialog',
|
||||
body: ({ onDismiss }) => <SampleModal onDismiss={onDismiss} pluginId={context?.pluginId} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
type Props = {
|
||||
onDismiss: () => void;
|
||||
pluginId?: string;
|
||||
};
|
||||
|
||||
const SampleModal = ({ onDismiss, pluginId }: Props) => {
|
||||
return (
|
||||
<VerticalGroup spacing="sm">
|
||||
<p>This dialog was opened via the plugin extensions API.</p>
|
||||
<p>The panel is using a {pluginId} plugin to display data.</p>
|
||||
</VerticalGroup>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
As you can see, the plugin extensions API enables you to insert links into the UI of Grafana applications that send users to plugin features or trigger actions based on where the user clicked. The plugins extension API can also be used for [cross-plugin linking]({{< relref "./cross-plugin-linking" >}}).
|
Reference in New Issue
Block a user