mirror of https://github.com/grafana/grafana
Docs: Remove old plugin docs, fix URLs to go to new dev portal (#75325)
* Remove old plugin docs, add redirects to new dev portal * Restore plugin.schema.json file * Update docs/sources/administration/plugin-management/index.md Co-authored-by: Jack Baldry <jack.baldry@grafana.com> * More updates * Cleanup links * Update docs/sources/alerting/fundamentals/evaluate-grafana-alerts.md Co-authored-by: Jack Baldry <jack.baldry@grafana.com> * fix codeowners * Change reference links to inline links to avoid 404s * Update docs/sources/datasources/_index.md Co-authored-by: Jack Baldry <jack.baldry@grafana.com> * Update docs/sources/datasources/mysql/_index.md Co-authored-by: Jack Baldry <jack.baldry@grafana.com> * Update docs/sources/datasources/postgres/_index.md Co-authored-by: Jack Baldry <jack.baldry@grafana.com> * Update docs/sources/fundamentals/timeseries-dimensions/index.md Co-authored-by: Jack Baldry <jack.baldry@grafana.com> * Update docs/sources/fundamentals/dashboards-overview/index.md Co-authored-by: Jack Baldry <jack.baldry@grafana.com> * Update docs/sources/fundamentals/dashboards-overview/index.md Co-authored-by: Jack Baldry <jack.baldry@grafana.com> * Update docs/sources/panels-visualizations/configure-standard-options/index.md Co-authored-by: Jack Baldry <jack.baldry@grafana.com> * Update docs/sources/panels-visualizations/query-transform-data/transform-data/index.md Co-authored-by: Jack Baldry <jack.baldry@grafana.com> * Remove grafana.com from 2 URLs * Fix 1 more reference link * Prettier fix --------- Co-authored-by: Jack Baldry <jack.baldry@grafana.com>pull/75721/head
parent
bcbfe75770
commit
0e2b741fc3
@ -1,58 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../plugins/developing/ |
||||
description: Resources for creating Grafana plugins |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
menuTitle: Plugin developer's guide |
||||
title: Grafana plugin developer's guide |
||||
weight: 200 |
||||
--- |
||||
|
||||
# Grafana plugin developer's guide |
||||
|
||||
You can extend Grafana's built-in capabilities with plugins. Plugins enable Grafana to accomplish specialized tasks, custom-tailored to your requirements. By making a plugin for your organization, you can connect Grafana to other data sources, ticketing tools, and CI/CD tooling. |
||||
|
||||
You can create plugins for private use or contribute them to the open source community by publishing to the [Grafana plugin catalog](/grafana/plugins/). This catalog has hundreds of other community and commercial plugins. |
||||
|
||||
If you are a Grafana plugin developer or want to become one, this plugin developer's guide contains the tutorials and reference materials to help you get started. |
||||
|
||||
## Plugin basics |
||||
|
||||
You can create several types of plugins, including: |
||||
|
||||
- **Panel plugins** - Visualize data and navigate between dashboards. |
||||
- **Data source plugins** - Link to new databases or other sources of data. |
||||
- **App plugins** - Create rich applications for custom out-of-the-box experiences. |
||||
|
||||
> **Note:** To learn more about the types of plugins you can build, refer to the [Plugin management]({{< relref "../../administration/plugin-management" >}}) documentation. |
||||
|
||||
## Contents of this developer's guide |
||||
|
||||
The following topics are covered in this guide: |
||||
|
||||
- **[Introduction to plugin development]({{< relref "./introduction-to-plugin-development" >}})** - Learn the fundamentals of Grafana plugin development: backend development, data frames, error handling, and more. |
||||
- **[Get started with plugins]({{< relref "./get-started-with-plugins" >}})** - Start developing Grafana plugins with the [create-plugin](https://www.npmjs.com/package/@grafana/create-plugin) tool. |
||||
- **[Create a Grafana plugin]({{< relref "./create-a-grafana-plugin/_index.md" >}})** - If you're familiar with plugin creation, use the tutorials for creating panel plugins, data source plugins, and more to deepen your knowledge. |
||||
- **[Publish a Grafana plugin]({{< relref "./publish-a-plugin" >}})** - Learn about publishing a plugin to the Grafana plugin catalog, including publishing criteria, packaging, and deployment. |
||||
- **[Work with legacy plugins]({{< relref "./legacy" >}})** - Learn how to upgrade from a previous version of a Grafana plugin, rewrite an old Angular plugin in React, or update to a newer version. |
||||
- **[Migrate a plugin]({{< relref "./migration-guide" >}})** - Consult these documents if you need to work with a plugin written in deprecated technology. |
||||
- **[Reference]({{< relref "./metadata.md" >}})** - Description of the `plugin.json` schema and plugin metadata. |
||||
|
||||
## Go further |
||||
|
||||
Learn more about additional tools and see plugin type examples. |
||||
|
||||
### User interface creation |
||||
|
||||
Explore the many UI components in our [Grafana UI library](https://developers.grafana.com/ui). |
||||
|
||||
### Plugin examples |
||||
|
||||
Grafana Labs provides a number of best practice example plugins for different use cases to help you quickly get started. Browse our [plugin examples](https://github.com/grafana/grafana-plugin-examples). |
||||
|
||||
### SDK |
||||
|
||||
Learn more about [Grafana Plugin SDK for Go]({{< relref "./introduction-to-plugin-development/backend/grafana-plugin-sdk-for-go" >}}). |
@ -1,22 +0,0 @@ |
||||
--- |
||||
description: An index of how-to topics for Grafana plugin development. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
menuTitle: Create a plugin |
||||
title: Create a Grafana plugin |
||||
weight: 300 |
||||
--- |
||||
|
||||
# Create a Grafana plugin |
||||
|
||||
This section contains how-to topics for developing and extending Grafana plugins with more advanced capabilities. |
||||
|
||||
- [Develop a plugin]({{< relref "./develop-a-plugin" >}}) |
||||
- [Extend a plugin]({{< relref "./extend-a-plugin" >}}) |
@ -1,32 +0,0 @@ |
||||
--- |
||||
description: An index of how-to topics for Grafana plugin development. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- development |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
menuTitle: Develop a plugin |
||||
title: Develop a Grafana plugin |
||||
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/get-started/). |
@ -1,198 +0,0 @@ |
||||
--- |
||||
description: Create a backend for your data source plugin. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- backend |
||||
- backend data source |
||||
- datasource |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Build a data source backend plugin |
||||
weight: 400 |
||||
--- |
||||
|
||||
## 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/) |
||||
- [LTS](https://nodejs.dev/en/about/releases/) version of Node.js |
||||
- 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 |
||||
|
||||
Ensure that Grafana has been started in development mode. If you are running Grafana from source, you'll need to add the following line to your `conf/custom.ini` file (if you don't have one already, go ahead and create this file before proceeding): |
||||
|
||||
```ini |
||||
app_mode = development |
||||
``` |
||||
|
||||
You can then start Grafana in development mode by running `make run & make run-frontend` in the Grafana repository root. |
||||
|
||||
If you are running Grafana from a binary or inside a Docker container, you can start it in development mode by setting the environment variable `GF_DEFAULT_APP_MODE` to `development`. |
||||
|
||||
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/datasource.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/datasource.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. |
@ -1,380 +0,0 @@ |
||||
--- |
||||
description: Create a plugin to add support for your own data sources. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- data source |
||||
- datasource |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Build a data source plugin |
||||
weight: 300 |
||||
--- |
||||
|
||||
## 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 |
||||
- [LTS](https://nodejs.dev/en/about/releases/) version of Node.js |
||||
- 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/) |
@ -1,685 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../../plugins/build-a-logs-data-source-plugin/ |
||||
description: How to build a logs data source plugin. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- logs |
||||
- logs data source |
||||
- datasource |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Build a logs data source plugin |
||||
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 frame |
||||
|
||||
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 frame |
||||
|
||||
### Logs data frame format |
||||
|
||||
The log data frame should include following fields: |
||||
|
||||
| Field name | Field type | Required field | Description | |
||||
| -------------- | ----------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
||||
| **timestamp** | `time` | required | Timestamp, non-nullable. | |
||||
| **body** | `string` | required | Content of the log line, non-nullable. | |
||||
| **severity** | `string` | optional | Severity/level of the log line. If no severity field is found, consumers/client will decide the log level. More information about log levels, refer to [Logs integration](https://grafana.com/docs/grafana/latest/explore/logs-integration/). | |
||||
| **id** | `string` | optional | Unique identifier of the log line. | |
||||
| **attributes** | `json raw message` (Go) or `other` (TypeScript) | optional | Additional attributes of the log line. Other systems may refer to this with different names, such as "Labels" in Loki. Represent its value as Record<string,any> type in JavaScript. | |
||||
|
||||
Logs data frame's `type` needs to be set to `type: DataFrameType.LogLines` in data frame's meta. |
||||
|
||||
**Example of constructing a logs data frame in Go:** |
||||
|
||||
```go |
||||
frame := data.NewFrame( |
||||
"logs", |
||||
data.NewField("timestamp", nil, []time.Time{time.UnixMilli(1645030244810), time.UnixMilli(1645030247027), time.UnixMilli(1645030247027)}), |
||||
data.NewField("body", nil, []string{"message one", "message two", "message three"}), |
||||
data.NewField("severity", nil, []string{"critical", "error", "warning"}), |
||||
data.NewField("id", nil, []string{"xxx-001", "xyz-002", "111-003"}), |
||||
data.NewField("attributes", nil, []json.RawMessage{[]byte(`{}`), []byte(`{"hello":"world"}`), []byte(`{"hello":"world", "foo": 123.45, "bar" :["yellow","red"], "baz" : { "name": "alice" }}`)}), |
||||
) |
||||
|
||||
frame.SetMeta(&data.FrameMeta{ |
||||
Type: data.FrameTypeLogLines, |
||||
}) |
||||
``` |
||||
|
||||
**Example of constructing a logs data frame in TypeScript:** |
||||
|
||||
```ts |
||||
import { createDataFrame, DataFrameType, FieldType } from '@grafana/data'; |
||||
|
||||
const result = createDataFrame({ |
||||
fields: [ |
||||
{ name: 'timestamp', type: FieldType.time, values: [1645030244810, 1645030247027, 1645030247027] }, |
||||
{ name: 'body', type: FieldType.string, values: ['message one', 'message two', 'message three'] }, |
||||
{ name: 'severity', type: FieldType.string, values: ['critical', 'error', 'warning'] }, |
||||
{ name: 'id', type: FieldType.string, values: ['xxx-001', 'xyz-002', '111-003'] }, |
||||
{ |
||||
name: 'attributes', |
||||
type: FieldType.other, |
||||
values: [{}, { hello: 'world' }, { hello: 'world', foo: 123.45, bar: ['yellow', 'red'], baz: { name: 'alice' } }], |
||||
}, |
||||
], |
||||
meta: { |
||||
type: DataFrameType.LogLines, |
||||
}, |
||||
}); |
||||
``` |
||||
|
||||
## Enhance your logs data source plugin with optional features |
||||
|
||||
You can use the following optional features to enhance your logs data source plugin. With optional features, you are going to enhance log querying experience in Explore. |
||||
|
||||
[Explore]({{< relref "../../../../explore" >}}) provides a useful interface for investigating incidents and troubleshooting logs. If the data source produces log results, we highly recommend implementing the following APIs to allow your users to get the most out of the logs UI and its features within Explore. |
||||
|
||||
The following steps show the process for adding support for Explore features in a data source plugin through a seamless integration. Implement these APIs to enhance the user experience and take advantage of Explore's powerful log investigation capabilities. |
||||
|
||||
### Show log results in Explore's Logs view |
||||
|
||||
To ensure that your log results are displayed in an interactive Logs view, you must add a `meta` attribute to `preferredVisualisationType` in your log result data frame. |
||||
|
||||
**Example in Go:** |
||||
|
||||
```go |
||||
frame.Meta = &data.FrameMeta{ |
||||
PreferredVisualization: "logs", |
||||
} |
||||
``` |
||||
|
||||
**Example in TypeScript:** |
||||
|
||||
```ts |
||||
import { createDataFrame } from '@grafana/data'; |
||||
|
||||
const result = createDataFrame({ |
||||
fields: [...], |
||||
meta: { |
||||
preferredVisualisationType: 'logs', |
||||
}, |
||||
}); |
||||
``` |
||||
|
||||
### Highlight searched words |
||||
|
||||
{{% admonition type="note" %}} This feature must be implemented in the data frame as a meta attribute. {{% |
||||
/admonition %}} |
||||
|
||||
The logs visualization can [highlight specific words or strings]({{< relref "../../../../explore/logs-integration/#highlight-searched-words" >}}) in log entries. This feature is typically used for highlighting search terms, making it easier for users to locate and focus on relevant information in the logs. For the highlighting to work, you must include search words in the data frame's `meta` information. |
||||
|
||||
**Example in Go:** |
||||
|
||||
```go |
||||
frame.Meta = &data.FrameMeta{ |
||||
Custom: map[string]interface{}{ |
||||
"searchWords": []string{"foo", "bar", "baz"} , |
||||
} |
||||
} |
||||
``` |
||||
|
||||
**Example in TypeScript:** |
||||
|
||||
```ts |
||||
import { createDataFrame } from '@grafana/data'; |
||||
|
||||
const result = createDataFrame({ |
||||
fields: [...], |
||||
meta: { |
||||
custom: { |
||||
searchWords: ["foo", "bar", "baz"], |
||||
} |
||||
}, |
||||
}); |
||||
``` |
||||
|
||||
### Log result `meta` information |
||||
|
||||
{{% admonition type="note" %}} This feature must be implemented in the data frame as a meta attribute, or in the data frame as a field. {{% |
||||
/admonition %}} |
||||
|
||||
[Log result meta information]({{< relref "../../../../explore/logs-integration/#log-result-meta-information" >}}) can be used to communicate information about logs results to the user. The following information can be shared with the user: |
||||
|
||||
- **Count of received logs vs limit** - Displays the count of received logs compared to the specified limit. Data frames should set a limit with a meta attribute for the number of requested log lines. |
||||
- **Error**: Displays possible errors in your log results. Data frames should to have an `error` in the `meta` attribute. |
||||
- **Common labels**: Displays attributes present in the `attributes` data frame field that are the same for all displayed log lines. This feature is supported for data sources that produce log data frames with an attributes field. Refer to [Logs data frame format](#logs-data-frame-format) for more information. |
||||
|
||||
**Example in Go:** |
||||
|
||||
```go |
||||
frame.Meta = &data.FrameMeta{ |
||||
Custom: map[string]interface{}{ |
||||
"limit": 1000, |
||||
"error": "Error information", |
||||
} |
||||
} |
||||
``` |
||||
|
||||
**Example in TypeScript:** |
||||
|
||||
```ts |
||||
import { createDataFrame } from '@grafana/data'; |
||||
|
||||
const result = createDataFrame({ |
||||
fields: [...], |
||||
meta: { |
||||
custom: { |
||||
limit: 1000, |
||||
error: "Error information" |
||||
} |
||||
}, |
||||
}); |
||||
``` |
||||
|
||||
### Logs to trace using data link with url |
||||
|
||||
If your log data contains **trace IDs**, you can enhance your log data frames by adding a field with _trace ID values_ and _URL data links_. These links should use the trace ID value to accurately link to the appropriate trace. This enhancement enables users to seamlessly move from log lines to the relevant traces. |
||||
|
||||
**Example in TypeScript:** |
||||
|
||||
```ts |
||||
import { createDataFrame, FieldType } from '@grafana/data'; |
||||
|
||||
const result = createDataFrame({ |
||||
fields: [ |
||||
..., |
||||
{ name: 'traceID', |
||||
type: FieldType.string, |
||||
values: ['a006649127e371903a2de979', 'e206649127z371903c3be12q' 'k777549127c371903a2lw34'], |
||||
config: { |
||||
links: [ |
||||
{ |
||||
// Be sure to adjust this example based on your data source logic. |
||||
title: 'Trace view', |
||||
url: `http://linkToTraceID/${__value.raw}` // ${__value.raw} is a variable that will be replaced with actual traceID value. |
||||
} |
||||
] |
||||
} |
||||
} |
||||
], |
||||
..., |
||||
}); |
||||
``` |
||||
|
||||
### Color-coded log levels |
||||
|
||||
{{% admonition type="note" %}} This feature must be implemented in the data frame as a field. {{% |
||||
/admonition %}} |
||||
|
||||
Color-coded [log levels]({{< relref "../../../../explore/logs-integration/#log-level" >}}) are displayed at the beginning of each log line. They allow users to quickly assess the severity of log entries and facilitate log analysis and troubleshooting. The log level is determined from the `severity` field of the data frame. If the `severity` field isn't present, Grafana tries to evaluate the level based on the content of the log line. If inferring the log level from the content isn't possible, the log level is then set to `unknown`. |
||||
|
||||
Refer to [Logs data frame format](#logs-data-frame-format) for more information. |
||||
|
||||
### Copy link to log line |
||||
|
||||
{{% admonition type="note" %}} This feature must be implemented in the data frame as a field. {{% |
||||
/admonition %}} |
||||
|
||||
[Copy link to log line]({{< relref "../../../../explore/logs-integration/#copy-link-to-log-line" >}}) is a feature that allows you to generate a link to a specific log line for easy sharing and referencing. Grafana supports this feature in data sources that produce log data frames with `id` fields. |
||||
|
||||
If the underlying database doesn't return an `id` field, you can implement one within the data source. For example, in the Loki data source, a combination of nanosecond timestamp, labels, and the content of the log line is used to create a unique `id`. On the other hand, Elasticsearch returns an `_id` field that is unique for the specified index. In such cases, to ensure uniqueness, both the `index name` and `_id` are used to create a unique `id`. |
||||
|
||||
Refer to [Logs data frame format](#logs-data-frame-format) for more information. |
||||
|
||||
### Filter fields using Log details |
||||
|
||||
{{% admonition type="note" %}} Implement this feature through the data source method. {{% |
||||
/admonition %}} |
||||
|
||||
Every log line has an expandable part called "Log details" that you can open by clicking on the line. Within Log details, Grafana displays [Fields]({{< relref "../../../../explore/logs-integration/#fields" >}}) associated with that log entry. If the data source implements `modifyQuery?(query: TQuery, action: QueryFixAction): TQuery;` API, then filtering functionality is available for each field. For logs, two filtering options are currently available: |
||||
|
||||
- `ADD_FILTER` - Use to filter for log lines that include selected fields. |
||||
- `ADD_FILTER_OUT` - Use to filter for log lines that don't include selected fields. |
||||
|
||||
```ts |
||||
export class ExampleDatasource extends DataSourceApi<ExampleQuery, ExampleOptions> { |
||||
modifyQuery(query: ExampleQuery, action: QueryFixAction): ExampleQuery { |
||||
let queryText = query.query ?? ''; |
||||
switch (action.type) { |
||||
case 'ADD_FILTER': |
||||
if (action.options?.key && action.options?.value) { |
||||
// Be sure to adjust this example code based on your data source logic. |
||||
queryText = addPositiveFilterToQuery(queryText, action.options.key, action.options.value); |
||||
} |
||||
break; |
||||
case 'ADD_FILTER_OUT': |
||||
{ |
||||
if (action.options?.key && action.options?.value) { |
||||
// Be sure to adjust this example code based on your data source logic. |
||||
queryText = addNegativeFilterToQuery(queryText, action.options.key, action.options.value); |
||||
} |
||||
} |
||||
break; |
||||
} |
||||
return { ...query, query: queryText }; |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### Live tailing |
||||
|
||||
{{% admonition type="note" %}} Implement this feature data source method and enabled in `plugin.json` {{% |
||||
/admonition %}} |
||||
|
||||
[Live tailing]({{< relref "../../../../explore/logs-integration/#live-tailing" >}}) is a feature that enables real-time log result streaming using Explore. To enable live tailing for your data source, follow these steps: |
||||
|
||||
1. **Enable streaming in `plugin.json`**: In your data source plugin's `plugin.json` file, set the `streaming` attribute to `true`. This allows Explore to recognize and enable live tailing controls for your data source. |
||||
|
||||
```json |
||||
{ |
||||
"type": "datasource", |
||||
"name": "Example", |
||||
"id": "example", |
||||
"logs": true, |
||||
"streaming": true |
||||
} |
||||
``` |
||||
|
||||
2. Ensure that your data source's `query` method can handle queries with `liveStreaming` set to true. |
||||
|
||||
```ts |
||||
export class ExampleDatasource extends DataSourceApi<ExampleQuery, ExampleOptions> { |
||||
query(request: DataQueryRequest<ExampleQuery>): Observable<DataQueryResponse> { |
||||
// This is a mocked implementation. Be sure to adjust this based on your data source logic. |
||||
if (request.liveStreaming) { |
||||
return this.runLiveStreamQuery(request); |
||||
} |
||||
return this.runRegularQuery(request); |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### Log context |
||||
|
||||
{{% admonition type="note" %}} Implement this feature through the `DataSourceWithXXXSupport` interface{{% |
||||
/admonition %}} |
||||
|
||||
[Log context]({{< relref "../../../../explore/logs-integration/#log-context" >}}) is a feature in Explore that enables the display of additional lines of context surrounding a log entry that matches a specific search query. This feature allows users to gain deeper insights into the log data by viewing the log entry within its relevant context. Because Grafana will show the surrounding log lines, users can gain a better understanding of the sequence of events and the context in which the log entry occurred, improving log analysis and troubleshooting. |
||||
|
||||
```ts |
||||
import { |
||||
DataQueryRequest, |
||||
DataQueryResponse, |
||||
DataSourceWithLogsContextSupport, |
||||
LogRowContextOptions, |
||||
LogRowContextQueryDirection, |
||||
LogRowModel, |
||||
} from '@grafana/data'; |
||||
import { catchError, lastValueFrom, of, switchMap, Observable } from 'rxjs'; |
||||
|
||||
export class ExampleDatasource |
||||
extends DataSourceApi<ExampleQuery, ExampleOptions> |
||||
implements DataSourceWithLogsContextSupport<ExampleQuery> |
||||
{ |
||||
// Retrieve context for a given log row |
||||
async getLogRowContext( |
||||
row: LogRowModel, |
||||
options?: LogRowContextOptions, |
||||
query?: ExampleQuery |
||||
): Promise<DataQueryResponse> { |
||||
// Be sure to adjust this example implementation of createRequestFromQuery based on your data source logic. |
||||
const request = createRequestFromQuery(row, query, options); |
||||
return lastValueFrom( |
||||
// Be sure to adjust this example of this.query based on your data source logic. |
||||
this.query(request).pipe( |
||||
catchError((err) => { |
||||
const error: DataQueryError = { |
||||
message: 'Error during context query. Please check JS console logs.', |
||||
status: err.status, |
||||
statusText: err.statusText, |
||||
}; |
||||
throw error; |
||||
}), |
||||
// Be sure to adjust this example of processResultsToDataQueryResponse based on your data source logic. |
||||
switchMap((res) => of(processResultsToDataQueryResponse(res))) |
||||
) |
||||
); |
||||
} |
||||
|
||||
// Retrieve the context query object for a given log row. This is currently used to open LogContext queries in a split view. |
||||
getLogRowContextQuery( |
||||
row: LogRowModel, |
||||
options?: LogRowContextOptions, |
||||
query?: ExampleQuery |
||||
): Promise<ExampleQuery | null> { |
||||
// Data source internal implementation that creates context query based on row, options and original query |
||||
} |
||||
|
||||
// This method can be used to show "context" button based on runtime conditions (for example, row model data or plugin settings) |
||||
showContextToggle(row?: LogRowModel): boolean { |
||||
// If you want to always show toggle, you can just return true |
||||
if (row && row.searchWords && row.searchWords.length > 0) { |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
## APIs under development |
||||
|
||||
These APIs can be used in data sources within the [`grafana/grafana`](https://github.com/grafana/grafana) repository. They are not supported for external plugin developers. |
||||
|
||||
### Show full-range logs volume |
||||
|
||||
{{% admonition type="note" %}} This feature is not currently not supported for external plugins outside of Grafana repo. It is implemented in data source by implementing `DataSourceWithXXXSupport` interface. {{% |
||||
/admonition %}} |
||||
|
||||
With [full range logs volume]({{< relref "../../../../explore/logs-integration/#logs-volume" >}}), Explore displays a graph showing the log distribution for all the entered log queries. To add full-range logs volume support to the data source plugin, use the `DataSourceWithSupplementaryQueriesSupport` API. |
||||
|
||||
**How to implement `DataSourceWithSupplementaryQueriesSupport` API in data source:** |
||||
|
||||
{{% admonition type="note" %}} This API must be implemented in the data source in typescript code. {{% |
||||
/admonition %}} |
||||
|
||||
```ts |
||||
import { queryLogsVolume } from '../features/logs/logsModel'; // This is currently not available for use outside of the Grafana repo |
||||
import { |
||||
DataSourceWithSupplementaryQueriesSupport, |
||||
LogLevel, |
||||
SupplementaryQueryOptions, |
||||
SupplementaryQueryType, |
||||
} from '@grafana/data'; |
||||
|
||||
export class ExampleDatasource |
||||
extends DataSourceApi<ExampleQuery, ExampleOptions> |
||||
implements DataSourceWithSupplementaryQueriesSupport<ExampleQuery> |
||||
{ |
||||
// Returns supplementary query types that data source supports. |
||||
getSupportedSupplementaryQueryTypes(): SupplementaryQueryType[] { |
||||
return [SupplementaryQueryType.LogsVolume]; |
||||
} |
||||
|
||||
// Returns a supplementary query to be used to fetch supplementary data based on the provided type and original query. |
||||
// If provided query is not suitable for provided supplementary query type, undefined should be returned. |
||||
getSupplementaryQuery(options: SupplementaryQueryOptions, query: ExampleQuery): ExampleQuery | undefined { |
||||
if (!this.getSupportedSupplementaryQueryTypes().includes(options.type)) { |
||||
return undefined; |
||||
} |
||||
|
||||
switch (options.type) { |
||||
case SupplementaryQueryType.LogsVolume: |
||||
// This is a mocked implementation. Be sure to adjust this based on your data source logic. |
||||
return { ...query, refId: `logs-volume-${query.refId}`, queryType: 'count' }; |
||||
default: |
||||
return undefined; |
||||
} |
||||
} |
||||
|
||||
// Returns an observable that will be used to fetch supplementary data based on the provided |
||||
// supplementary query type and original request. |
||||
getDataProvider( |
||||
type: SupplementaryQueryType, |
||||
request: DataQueryRequest<ExampleQuery> |
||||
): Observable<DataQueryResponse> | undefined { |
||||
if (!this.getSupportedSupplementaryQueryTypes().includes(type)) { |
||||
return undefined; |
||||
} |
||||
|
||||
switch (type) { |
||||
case SupplementaryQueryType.LogsVolume: |
||||
return this.getLogsVolumeDataProvider(request); |
||||
default: |
||||
return undefined; |
||||
} |
||||
} |
||||
|
||||
// Be sure to adjust this example based your data source logic. |
||||
private getLogsVolumeDataProvider( |
||||
request: DataQueryRequest<ExampleQuery> |
||||
): Observable<DataQueryResponse> | undefined { |
||||
const logsVolumeRequest = cloneDeep(request); |
||||
const targets = logsVolumeRequest.targets |
||||
.map((query) => this.getSupplementaryQuery({ type: SupplementaryQueryType.LogsVolume }, query)) |
||||
.filter((query): query is ExampleQuery => !!query); |
||||
|
||||
if (!targets.length) { |
||||
return undefined; |
||||
} |
||||
|
||||
// Use imported queryLogsVolume. |
||||
return queryLogsVolume( |
||||
this, |
||||
{ ...logsVolumeRequest, targets }, |
||||
{ |
||||
// Implement extract level to produce color-coded graph. |
||||
extractLevel: (dataFrame: DataFrame) => LogLevel.unknown, |
||||
range: request.range, |
||||
targets: request.targets, |
||||
} |
||||
); |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### Logs sample |
||||
|
||||
{{% admonition type="note" %}} This feature is currently not supported for external plugins outside of the Grafana repo. Add support for this API in a data source by implementing the `DataSourceWith<Feature>Support` interface. {{% |
||||
/admonition %}} |
||||
|
||||
The [logs sample]({{< relref "../../../../explore/logs-integration/#logs-sample" >}}) feature is a valuable addition when your data source supports both logs and metrics. It enables users to view samples of log lines that contributed to the visualized metrics, providing deeper insights into the data. |
||||
|
||||
To implement the logs sample support in your data source plugin, you can use the `DataSourceWithSupplementaryQueriesSupport` API. |
||||
|
||||
```ts |
||||
import { queryLogsSample } from '../features/logs/logsModel'; // This is currently not possible to use outside of Grafana repo |
||||
import { |
||||
DataSourceWithSupplementaryQueriesSupport, |
||||
SupplementaryQueryOptions, |
||||
SupplementaryQueryType, |
||||
} from '@grafana/data'; |
||||
|
||||
export class ExampleDatasource |
||||
extends DataSourceApi<ExampleQuery, ExampleOptions> |
||||
implements DataSourceWithSupplementaryQueriesSupport<ExampleQuery> |
||||
{ |
||||
// Returns supplementary query types that data source supports. |
||||
getSupportedSupplementaryQueryTypes(): SupplementaryQueryType[] { |
||||
return [SupplementaryQueryType.LogsSample]; |
||||
} |
||||
|
||||
// Returns a supplementary query to be used to fetch supplementary data based on the provided type and original query. |
||||
// If provided query is not suitable for provided supplementary query type, undefined should be returned. |
||||
getSupplementaryQuery(options: SupplementaryQueryOptions, query: ExampleQuery): ExampleQuery | undefined { |
||||
if (!this.getSupportedSupplementaryQueryTypes().includes(options.type)) { |
||||
return undefined; |
||||
} |
||||
|
||||
switch (options.type) { |
||||
case SupplementaryQueryType.LogsSample: |
||||
// Be sure to adjust this example based on your data source logic. |
||||
return { ...query, refId: `logs-sample-${query.refId}`, queryType: 'logs' }; |
||||
default: |
||||
return undefined; |
||||
} |
||||
} |
||||
|
||||
// Returns an observable that will be used to fetch supplementary data based on the provided supplementary query type and original request. |
||||
getDataProvider( |
||||
type: SupplementaryQueryType, |
||||
request: DataQueryRequest<ExampleQuery> |
||||
): Observable<DataQueryResponse> | undefined { |
||||
if (!this.getSupportedSupplementaryQueryTypes().includes(type)) { |
||||
return undefined; |
||||
} |
||||
|
||||
switch (type) { |
||||
case SupplementaryQueryType.LogsSample: |
||||
return this.getLogsSampleDataProvider(request); |
||||
default: |
||||
return undefined; |
||||
} |
||||
} |
||||
|
||||
private getLogsSampleDataProvider( |
||||
request: DataQueryRequest<ExampleQuery> |
||||
): Observable<DataQueryResponse> | undefined { |
||||
const logsSampleRequest = cloneDeep(request); |
||||
const targets = logsVolumeRequest.targets |
||||
.map((query) => this.getSupplementaryQuery({ type: SupplementaryQueryType.LogsVolume }, query)) |
||||
.filter((query): query is ExampleQuery => !!query); |
||||
|
||||
if (!targets.length) { |
||||
return undefined; |
||||
} |
||||
|
||||
// Use imported queryLogsSample |
||||
return queryLogsSample(this, { ...logsVolumeRequest, targets }); |
||||
} |
||||
} |
||||
``` |
||||
|
||||
For an example of how to implement the logs sample in the Elasticsearch data source, refer to [PR 70258](https://github.com/grafana/grafana/pull/70258/). |
||||
|
||||
### Logs to trace using internal data links |
||||
|
||||
{{% admonition type="note" %}} This feature is currently not supported for external plugins outside of the Grafana repo. The `@internal` API is currently under development. {{% |
||||
/admonition %}} |
||||
|
||||
If you are developing a data source plugin that handles both logs and traces, and your log data contains trace IDs, you can enhance your log data frames by adding a field with trace ID values and internal data links. These links should use the trace ID value to accurately create a trace query that produces relevant trace. This enhancement enables users to seamlessly move from log lines to the traces. |
||||
|
||||
**Example in TypeScript:** |
||||
|
||||
```ts |
||||
import { createDataFrame } from '@grafana/data'; |
||||
|
||||
const result = createDataFrame({ |
||||
fields: [ |
||||
..., |
||||
{ name: 'traceID', |
||||
type: FieldType.string, |
||||
values: ['a006649127e371903a2de979', 'e206649127z371903c3be12q' 'k777549127c371903a2lw34'], |
||||
config: { |
||||
links: [ |
||||
{ |
||||
title: 'Trace view', |
||||
url: '', |
||||
internal: { |
||||
// Be sure to adjust this example with datasourceUid, datasourceName and query based on your data source logic. |
||||
datasourceUid: instanceSettings.uid, |
||||
datasourceName: instanceSettings.name, |
||||
query: { |
||||
{ ...query, queryType: 'trace', traceId: '${__value.raw}'}, // ${__value.raw} is a variable that will be replaced with actual traceID value. |
||||
} |
||||
} |
||||
|
||||
} |
||||
] |
||||
} |
||||
|
||||
} |
||||
], |
||||
..., |
||||
}); |
||||
``` |
||||
|
||||
### Log context query editor |
||||
|
||||
{{% admonition type="note" %}} This feature is currently not supported for external plugins outside of the Grafana repo. The`@alpha` API is currently under development. {{% |
||||
/admonition %}} |
||||
|
||||
It allows plugin developers to display a custom UI in the context view by implementing the `getLogRowContextUi?(row: LogRowModel, runContextQuery?: () => void, origQuery?: TQuery): React.ReactNode;` method. |
||||
|
||||
### Toggleable filters in Log Details |
||||
|
||||
{{% admonition type="note" %}} This feature is currently not supported for external plugins outside of the Grafana repo. Add support for this API in a data source by implementing the `DataSourceWith<Feature>Support` interface. {{% |
||||
/admonition %}} |
||||
|
||||
The [logs details]({{< relref "../../../../explore/logs-integration/#log-details-view" >}}) component offers the possibility of adding and removing filters from the query that generated a given log line by clicking the corresponding button next to each field associated with the log line. When this interface is implemented in your data source, you get access to the following functionalities: |
||||
|
||||
- If a positive filter for the field and field value is present in the query, the "plus" icon is displayed as active. |
||||
- If a positive filter is active, it can be toggled off (removed) from the source query. |
||||
- If a positive filter is active, it can be changed to a negative equivalent by clicking on the "minus" icon. |
||||
- A negative filter for a field and field value can be added to the source query by clicking on the corresponding icon. |
||||
|
||||
To implement toggleable filters support in your data source plugin, you can use the `DataSourceWithToggleableQueryFiltersSupport` API. |
||||
|
||||
```ts |
||||
import { |
||||
queryHasPositiveFilter, |
||||
removePositiveFilterFromQuery, |
||||
addPositiveFilterToQuery, |
||||
addNegativeFilterToQuery, |
||||
} from '../your/own/package/functions'; |
||||
import { DataSourceWithToggleableQueryFiltersSupport, QueryFilterOptions } from '@grafana/data'; |
||||
|
||||
export class ExampleDatasource |
||||
extends DataSourceApi<ExampleQuery, ExampleOptions> |
||||
implements DataSourceWithToggleableQueryFiltersSupport<ExampleQuery> |
||||
{ |
||||
/** |
||||
* Given a query, determine if it has a filter that matches the options. |
||||
*/ |
||||
queryHasFilter(query: ExampleQuery, filter: QueryFilterOptions): boolean { |
||||
/** |
||||
* Pass the query and the key => value pair to your query-analyzing function. |
||||
* We only care about equality/positive filters as only those fields will be |
||||
* present in the log lines. |
||||
*/ |
||||
return queryHasPositiveFilter(query.query, filter.key, filter.value); |
||||
} |
||||
|
||||
/** |
||||
* Toggle filters on and off from query. |
||||
* If the filter is already present, it should be removed. |
||||
* If the opposite filter is present, it should be replaced. |
||||
*/ |
||||
toggleQueryFilter(query: ExampleQuery, filter: ToggleFilterAction): LokiQuery { |
||||
const queryText = query.query; // The current query. |
||||
const { key, value } = filter.options; |
||||
// We currently support 2 types of filter: FILTER_FOR (positive) and FILTER_OUT (negative). |
||||
switch (filter.type) { |
||||
case 'FILTER_FOR': { |
||||
// This gives the user the ability to toggle a filter on and off. |
||||
queryText = queryHasPositiveFilter(queryText, key, value) |
||||
? removePositiveFilterFromQuery(queryText, key, value) |
||||
: addPositiveFilterToQuery(queryText, key, value); |
||||
break; |
||||
} |
||||
case 'FILTER_OUT': { |
||||
// If there is a positive filter with the same key and value, remove it. |
||||
if (queryHasPositiveFilter(queryText, key, value)) { |
||||
queryText = removePositiveLabelFromQuery(queryText, key, value); |
||||
} |
||||
// Add the inequality filter to the query. |
||||
queryText = addNegativeFilterToQuery(queryText, key, value); |
||||
break; |
||||
} |
||||
default: |
||||
break; |
||||
} |
||||
return { ...query, query: queryText }; |
||||
} |
||||
} |
||||
``` |
@ -1,240 +0,0 @@ |
||||
--- |
||||
description: how to use D3.js in your panel plugins. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- d3js |
||||
- d3 |
||||
- panel |
||||
- panel plugin |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Build a panel plugin with D3.js |
||||
weight: 200 |
||||
--- |
||||
|
||||
## 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 |
||||
- [LTS](https://nodejs.dev/en/about/releases/) version of Node.js |
||||
- 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. |
@ -1,264 +0,0 @@ |
||||
--- |
||||
description: Learn how to create a custom visualization for your dashboards. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- visualization |
||||
- custom visualization |
||||
- dashboard |
||||
- dashboards |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Build a panel plugin |
||||
weight: 100 |
||||
--- |
||||
|
||||
## 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 |
||||
- [LTS](https://nodejs.dev/en/about/releases/) version of Node.js |
||||
- 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. |
@ -1,166 +0,0 @@ |
||||
--- |
||||
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 |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Build a streaming data source plugin |
||||
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. |
@ -1,211 +0,0 @@ |
||||
--- |
||||
description: Learn at how to create an app for Grafana. |
||||
draft: true |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- app |
||||
- app plugin |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Build an app plugin |
||||
weight: 700 |
||||
--- |
||||
|
||||
## 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 |
||||
- [LTS](https://nodejs.dev/en/about/releases/) version of Node.js |
||||
- 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": "Clock Panel", |
||||
"id": "grafana-clock-panel", |
||||
"version": "^2.1.3" |
||||
} |
||||
] |
||||
} |
||||
``` |
||||
|
||||
## Summary |
||||
|
||||
In this tutorial you learned how to create an app plugin. |
@ -1,135 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../../plugins/working-with-data-frames/ |
||||
description: How to work with data frames. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- data frames |
||||
- dataframes |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Work with data frames |
||||
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); |
||||
``` |
@ -1,38 +0,0 @@ |
||||
--- |
||||
description: An index of how-to topics for extending or enhancing Grafana plugins. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- development |
||||
- extension |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
menuTitle: Extend a plugin |
||||
title: Extend a Grafana plugin |
||||
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/development/ci) |
||||
- [Create nested plugins](https://grafana.github.io/plugin-tools/docs/advanced-usage/nested-plugins) |
||||
- [Extend configurations](https://grafana.github.io/plugin-tools/docs/advanced-usage/advanced-configuration) |
@ -1,180 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../../plugins/add-anonymous-usage-reporting/ |
||||
description: How to add anonymous usage tracking to your Grafana plugin. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- anonymous usage |
||||
- reporting |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Add anonymous usage reporting |
||||
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' |
||||
} |
||||
} |
||||
``` |
@ -1,472 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../plugins/developing/auth-for-datasources/ |
||||
- /docs/grafana/next/developers/plugins/authentication/ |
||||
description: How to add authentication for data source plugins. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- authentication |
||||
- data source |
||||
- datasource |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Add authentication for data source plugins |
||||
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") |
||||
|
||||
// ... |
||||
} |
||||
``` |
@ -1,126 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../../plugins/add-distributed-tracing-for-backend-plugins/ |
||||
description: How to add distributed tracing for backend plugins. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- distributed tracing |
||||
- tracing |
||||
- backend |
||||
- back-end |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: 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. |
@ -1,88 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../../plugins/add-query-editor-help/ |
||||
description: How to add a help component to query editors in Grafana. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- queries |
||||
- query editor |
||||
- query editor help |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Add query editor help |
||||
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> |
||||
); |
||||
}; |
||||
``` |
@ -1,44 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../../plugins/add-support-for-annotations/ |
||||
description: Add support for annotations in your plugin. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- annotations |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
menuTitle: Enable annotations |
||||
title: Enable 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: { |
||||
} |
||||
``` |
@ -1,85 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../../plugins/add-support-for-explore-queries/ |
||||
description: Add features to Explore queries. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- queries |
||||
- explore queries |
||||
- explore |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Add features to Explore queries |
||||
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). |
@ -1,212 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../../plugins/add-support-for-variables/ |
||||
description: Add support for variables. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- queries |
||||
- variables |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Add support for 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. |
@ -1,89 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../../plugins/cross-plugin-linking/ |
||||
description: Learn how to add plugin links to a Grafana app plugin. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- links |
||||
- cross-plugin links |
||||
- extensions |
||||
- extensions api |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Work with cross-plugin links |
||||
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', |
||||
}); |
||||
``` |
@ -1,136 +0,0 @@ |
||||
--- |
||||
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 |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Build a custom panel option editor |
||||
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)} />; |
||||
}; |
||||
``` |
@ -1,146 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../../plugins/extend-the-grafana-ui-with-links/ |
||||
description: Learn how to add links to the Grafana user interface from an app plugin |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- links |
||||
- extensions |
||||
- app plugins |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Use extensions to add links to app plugins |
||||
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" >}}). |
@ -1,27 +0,0 @@ |
||||
--- |
||||
description: Get started with Grafana plugin development. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
menuTitle: Get started with plugins |
||||
title: Get started with Grafana plugin development |
||||
weight: 200 |
||||
--- |
||||
|
||||
# Get started with Grafana plugin development |
||||
|
||||
This section contains guidance for building plugins: |
||||
|
||||
- [Develop with local Grafana]({{< relref "./development-with-local-grafana.md" >}}) |
||||
|
||||
Additional resources: |
||||
|
||||
- [Get started with creating a plugin](https://grafana.github.io/plugin-tools/docs/get-started/) |
||||
- [Types of Grafana plugins](/docs/grafana/latest/administration/plugin-management/) |
||||
- [Set up your development environment](https://grafana.github.io/plugin-tools/docs/get-started/set-up-development-environment) |
@ -1,192 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../plugins/development-with-local-grafana/ |
||||
description: How to develop with a local Grafana environment. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- development environment |
||||
- local environment |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Develop with a local environment |
||||
weight: 400 |
||||
--- |
||||
|
||||
# Develop with a local environment |
||||
|
||||
Follow the steps in this guide to set up a development environment where you run Grafana and your plugin locally. With this setup, you can see your changes as you add them. |
||||
|
||||
## Run Grafana in your host |
||||
|
||||
To clone and run Grafana locally: |
||||
|
||||
1. Download and set up Grafana. Refer to the [developer-guide](https://github.com/grafana/grafana/blob/HEAD/contribute/developer-guide.md). |
||||
|
||||
2. Grafana looks for plugins, by default, in its `data/plugins` directory. You can create a symbolic link to your plugin repository to detect new changes: |
||||
|
||||
```bash |
||||
ln -s <plugin-path>/dist data/plugins/<plugin-name> |
||||
``` |
||||
|
||||
3. Optional: If the preceding step doesn't work for you (for example, if you are running on Windows), then modify the default path in the Grafana configuration. Find the default path at `conf/custom.ini`) and point it to your plugin's directory: |
||||
|
||||
```ini |
||||
[paths] |
||||
plugins = <path-to-your-plugin-parent-directory> |
||||
``` |
||||
|
||||
## Run Grafana with docker-compose |
||||
|
||||
Another option is to run Grafana with docker-compose so that it runs in a container. To do so, create the `docker-compose` file in your plugin directory. |
||||
|
||||
{{% admonition type="note" %}} |
||||
If your plugin already includes a docker-compose file, then skip this step. |
||||
{{% /admonition %}} |
||||
|
||||
```yaml |
||||
version: '3.7' |
||||
|
||||
services: |
||||
grafana: |
||||
# Change latest with your target version, if needed |
||||
image: grafana/grafana:latest |
||||
ports: |
||||
- 3000:3000/tcp |
||||
volumes: |
||||
# Use your plugin folder (for example, redshift-datasource) |
||||
- ./dist:/var/lib/grafana/plugins/<plugin-folder> |
||||
- ./provisioning:/etc/grafana/provisioning |
||||
environment: |
||||
- TERM=linux |
||||
- GF_LOG_LEVEL=debug |
||||
- GF_DATAPROXY_LOGGING=true |
||||
- GF_DEFAULT_APP_MODE=development |
||||
``` |
||||
|
||||
## Run your plugin in development mode |
||||
|
||||
Finally, start your plugin in development mode. Go to your plugin's root directory and follow these steps: |
||||
|
||||
1. Build your plugin backend and start the frontend in watch mode: |
||||
|
||||
```bash |
||||
mage -v |
||||
yarn watch |
||||
``` |
||||
|
||||
2. Start the Grafana backend and frontend: |
||||
|
||||
1. For a local copy of Grafana, go to the directory with Grafana source code and run: |
||||
|
||||
```bash |
||||
make run |
||||
``` |
||||
|
||||
```bash |
||||
yarn start |
||||
``` |
||||
|
||||
2. Or, with docker-compose, in your plugin directory, run: |
||||
|
||||
```bash |
||||
docker-compose up |
||||
``` |
||||
|
||||
After this, you should be able to see your plugin listed in Grafana, and then you can test your changes. |
||||
|
||||
If you make a change in the frontend, you must refresh your browser. However, changes in the backend may require that you rebuild your plugin binaries and reload the plugin (`mage && mage reloadPlugin` for local development, or run `docker-compose up` again if you are using docker-compose). |
||||
|
||||
## Run your backend plugin with a debugger |
||||
|
||||
{{% admonition type="note" %}} |
||||
The following method only works with a local Grafana instance and currently doesn't work with Docker. |
||||
{{% /admonition %}} |
||||
|
||||
Running a backend plugin with a debugger is supported in Visual Studio Code and GoLand out of the box, but it can also work with any other IDE or debugger. |
||||
|
||||
You can run a backend plugin and attach a debugger to it, which allows you to set breakpoints and debug your backend plugin directly from your IDE of choice: |
||||
|
||||
1. Go to your plugin's folder. |
||||
|
||||
1. Check your `go.mod` to make sure `grafana-plugin-sdk-go` are at least on `v0.156.0` |
||||
- If not, update it to the latest version: |
||||
``` |
||||
go get -u github.com/grafana/grafana-plugin-sdk-go |
||||
``` |
||||
1. Build your plugin at least once: |
||||
``` |
||||
yarn build && mage |
||||
``` |
||||
1. Install your plugin into your local Grafana instance. |
||||
|
||||
Now that your plugin is ready to run, follow the instructions below for your IDE of choice. |
||||
|
||||
### Visual Studio Code |
||||
|
||||
1. If it's not already present, go to your plugin's folder and place the following file inside `.vscode/launch.json`: |
||||
|
||||
```json |
||||
{ |
||||
"version": "0.2.0", |
||||
"configurations": [ |
||||
{ |
||||
"name": "Standalone debug mode", |
||||
"type": "go", |
||||
"request": "launch", |
||||
"mode": "debug", |
||||
"program": "${workspaceFolder}/pkg", |
||||
"env": {}, |
||||
"args": ["-standalone"] |
||||
} |
||||
] |
||||
} |
||||
``` |
||||
|
||||
1. Press `F5` to run your plugin in debug mode. |
||||
1. If Grafana isn't already running, run it. |
||||
|
||||
> If you re-run the configuration, Grafana automatically reloads the plugin. |
||||
|
||||
### GoLand |
||||
|
||||
1. Create a new Run/Debug configuration: |
||||
|
||||
- **Run kind**: Package |
||||
- **Package path**: your `pkg` package |
||||
- **Program arguments**: `-standalone` |
||||
|
||||
1. Run the config (with or without the debugger). |
||||
|
||||
1. If Grafana isn't already running, run it. |
||||
|
||||
{{% admonition type="note" %}} |
||||
If you re-run the configuration, Grafana automatically reloads the plugin. |
||||
{{% /admonition %}} |
||||
|
||||
### Other IDEs |
||||
|
||||
Configure your code editor to run the following steps: |
||||
|
||||
1. Build the executable file with debug flags. |
||||
``` |
||||
mage build:debug |
||||
``` |
||||
1. Run the plugin's executable file (inside `dist`) with `-standalone` flag. |
||||
``` |
||||
./gpx_xyz_linux_amd64 -standalone |
||||
``` |
||||
1. Attach a debugger to the process. |
||||
|
||||
1. If Grafana isn't already running, run it. |
||||
|
||||
> If you re-run the configuration, Grafana automatically reloads the plugin. |
||||
|
||||
### Notes |
||||
|
||||
- All logs are printed in the plugin's `stdout` rather than in Grafana logs. |
||||
- If the backend plugin doesn't serve requests after you stop debugging, you can force a reset to the standalone mode. To do so, delete the files `dist/standalone.txt`, `dist/pid.txt`, and the executable file, and then restart Grafana. |
||||
- Grafana doesn't support debugging backend plugins running inside Docker. But this is a [planned enhancement](https://github.com/grafana/grafana-plugin-sdk-go/issues/685). |
@ -1,25 +0,0 @@ |
||||
--- |
||||
description: Conceptual topics for Grafana plugin development. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
menuTitle: Introduction to plugin development |
||||
title: Introduction to Grafana plugin development |
||||
weight: 100 |
||||
--- |
||||
|
||||
# Introduction to Grafana plugin development |
||||
|
||||
This section contains documentation related to the key concepts for Grafana plugin development. |
||||
|
||||
- [Backend plugins]({{< relref "./backend" >}}) |
||||
- [Grafana plugin SDK for Go]({{< relref "./backend/grafana-plugin-sdk-for-go.md" >}}) |
||||
- [Plugin protocol]({{< relref "./backend/plugin-protocol.md" >}}) |
||||
- [Data frames]({{< relref "./data-frames.md" >}}) |
||||
- [Error handling]({{< relref "./error-handling.md" >}}) |
@ -1,104 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../plugins/developing/backend-plugins-guide/ |
||||
- ../../plugins/backend/ |
||||
description: Learn about the Grafana plugin system for backend development of Grafana |
||||
integrations. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- backend |
||||
- plugin |
||||
- backend-plugins |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Backend plugins |
||||
--- |
||||
|
||||
# Backend plugins |
||||
|
||||
The Grafana plugin system for backend development allows you to integrate Grafana with virtually anything and offer custom visualizations. This document explains the system's background, use cases, and key features. |
||||
|
||||
## Background |
||||
|
||||
Grafana added support for _frontend plugins_ in version 3.0 so that the Grafana community could create custom panels and data sources. It was wildly successful and has made Grafana much more useful for our user community. |
||||
|
||||
However, one limitation of these plugins is that they run on the client side, in the browser. Therefore, they can't support use cases that require server-side features. |
||||
|
||||
Since Grafana v7.0, we have supported server-side plugins that remove this limitation. We use the term _backend plugin_ to denote that a plugin has a backend component. A backend plugin usually requires frontend components as well. For example, some backend data source plugins need query editor components on the frontend. |
||||
|
||||
## Use cases for implementing a backend plugin |
||||
|
||||
The following examples give some common use cases for backend plugins: |
||||
|
||||
- Enable [Grafana Alerting]({{< relref "../../../../alerting" >}}) for data sources. |
||||
- Connect to SQL database servers and other non-HTTP services that normally can't be connected to from a browser. |
||||
- Keep state between users, for example, by query caching for data sources. |
||||
- Use custom authentication methods and/or authorization checks that aren't supported in Grafana. |
||||
- Use a custom data source request proxy (refer to [Resources]({{< relref "#resources" >}}) for more information). |
||||
|
||||
## Grafana backend plugin system |
||||
|
||||
The Grafana backend plugin system is based on HashiCorp's [Go Plugin System over RPC](https://github.com/hashicorp/go-plugin). Our implementation of the Grafana server launches each backend plugin as a subprocess and communicates with it over [gRPC](https://grpc.io/). |
||||
|
||||
### Benefits for plugin development |
||||
|
||||
Grafana's approach has benefits for developers: |
||||
|
||||
- **Stability:** Plugins can't crash your Grafana process: a panic in a plugin doesn't panic the server. |
||||
- **Ease of development:** Plugins can be written in any language that supports gRPC (for example, write a Go application and run `go build`). |
||||
- **Security:** Plugins only have access to the interfaces and arguments given to them, not to the entire memory space of the process. |
||||
|
||||
### Capabilities of the backend plugin system |
||||
|
||||
Grafana's backend plugin system exposes several key capabilities, or building blocks, that your backend plugin can implement: |
||||
|
||||
- Query data |
||||
- Resources |
||||
- Health checks |
||||
- Collect metrics |
||||
- Streaming |
||||
|
||||
#### Query data |
||||
|
||||
The query data capability allows a backend plugin to handle data source queries that are submitted from a [dashboard]({{< relref "../../../../dashboards" >}}), [Explore]({{< relref "../../../../explore" >}}) or [Grafana Alerting]({{< relref "../../../../alerting" >}}). The response contains [data frames]({{< relref "../data-frames.md" >}}), which are used to visualize metrics, logs, and traces. |
||||
|
||||
{{% admonition type="note" %}} Backend data source plugins are required to implement the query data capability.{{% |
||||
/admonition %}} |
||||
|
||||
#### Resources |
||||
|
||||
The resources capability allows a backend plugin to handle custom HTTP requests sent to the Grafana HTTP API and respond with custom HTTP responses. Here, the request and response formats can vary. For example, you can use JSON, plain text, HTML, or static resources such as images and files, and so on. |
||||
|
||||
Compared to the query data capability, where the response contains data frames, the resources capability gives the plugin developer more flexibility for extending and opening up Grafana for new and interesting use cases. |
||||
|
||||
### Use cases |
||||
|
||||
Examples of use cases for implementing resources: |
||||
|
||||
- Implement a custom data source proxy to provide certain authentication, authorization, or other requirements that are not supported in Grafana's [built-in data proxy]({{< relref "../../../http_api/data_source#data-source-proxy-calls" >}}). |
||||
- Return data or information in a format suitable for use within a data source query editor to provide auto-complete functionality. |
||||
- Return static resources such as images or files. |
||||
- Send a command to a device, such as a microcontroller or IoT device. |
||||
- Request information from a device, such as a microcontroller or IoT device. |
||||
- Extend Grafana's HTTP API with custom resources, methods and actions. |
||||
- Use [chunked transfer encoding](https://en.wikipedia.org/wiki/Chunked_transfer_encoding) to return large data responses in chunks or to enable certain streaming capabilities. |
||||
|
||||
#### Health checks |
||||
|
||||
The health checks capability allows a backend plugin to return the status of the plugin. For data source backend plugins, the health check is automatically called when a user edits a data source and selects _Save & Test_ in the UI. |
||||
|
||||
A plugin's health check endpoint is exposed in the Grafana HTTP API and allows external systems to continuously poll the plugin's health to make sure that it's running and working as expected. |
||||
|
||||
#### Collect metrics |
||||
|
||||
A backend plugin can collect and return runtime, process, and custom metrics using the text-based Prometheus [exposition format](https://prometheus.io/docs/instrumenting/exposition_formats/). If you're using the [Grafana Plugin SDK for Go]({{< relref "./grafana-plugin-sdk-for-go.md" >}}) to implement your backend plugin, then the [Prometheus instrumentation library for Go applications](https://github.com/prometheus/client_golang) is built-in. This SDK gives you Go runtime metrics and process metrics out of the box. You can use the [Prometheus instrumentation library](https://github.com/prometheus/client_golang) to add custom metrics to instrument your backend plugin. |
||||
|
||||
The Grafana HTTP API offers an endpoint (`/api/plugins/<plugin id>/metrics`) that allows you to configure a Prometheus instance to scrape the metrics. |
||||
|
||||
#### Streaming |
||||
|
||||
The streaming capability allows a backend plugin to handle data source queries that are streaming. For more information, refer to [Build a streaming data source plugin]({{< relref "../../create-a-grafana-plugin/develop-a-plugin/build-a-streaming-data-source-plugin" >}}). |
@ -1,36 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../plugins/backend/grafana-plugin-sdk-for-go/ |
||||
description: Learn about the Grafana plugin SDK for Go, a Go module with packages |
||||
for implementing a Grafana backend plugin. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- backend |
||||
- plugin |
||||
- backend-plugins |
||||
- sdk |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Grafana plugin SDK for Go |
||||
--- |
||||
|
||||
# Grafana plugin SDK for Go |
||||
|
||||
The [Grafana plugin SDK for Go](https://pkg.go.dev/mod/github.com/grafana/grafana-plugin-sdk-go?tab=overview) is a [Go](https://golang.org/) module that provides a set of [packages](https://pkg.go.dev/mod/github.com/grafana/grafana-plugin-sdk-go?tab=packages) that you can use to implement a backend plugin. |
||||
|
||||
The plugin SDK provides a high-level framework with APIs, utilities, and tooling. By using the SDK, you can avoid the need to learn the details of the [plugin protocol]({{< relref "./plugin-protocol.md" >}}) and RPC communication protocol, so you don't have to manage either one. |
||||
|
||||
## Versioning |
||||
|
||||
The Grafana plugin Go SDK is still in development. It is based on the [plugin protocol]({{< relref "./plugin-protocol" >}}), which is versioned separately and is considered stable. However, from time to time, we might introduce breaking changes in the SDK. |
||||
|
||||
When we update the plugin SDK, those plugins that use an older version of the SDK should still work with Grafana. However, these older plugins may be unable to use the new features and capabilities we introduce in updated SDK versions. |
||||
|
||||
## See also |
||||
|
||||
- [SDK source code](https://github.com/grafana/grafana-plugin-sdk-go) |
||||
- [Go reference documentation](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go) |
@ -1,53 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../plugins/backend/plugin-protocol/ |
||||
description: Learn about the Grafana wire protocol used for communication between |
||||
the Grafana server and backend plugins. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- backend |
||||
- plugin |
||||
- backend-plugins |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Plugin protocol |
||||
--- |
||||
|
||||
# Plugin protocol |
||||
|
||||
The Grafana server uses a physical wire protocol to communicate with backend plugins. This protocol establishes a contract between Grafana and backend plugins to allow them to communicate with each other. |
||||
|
||||
## Developing with the plugin protocol |
||||
|
||||
{{% admonition type="caution" %}} We strongly recommend that backend plugin development not be implemented directly against the protocol. Instead, we prefer that you use the [Grafana Plugin SDK for Go]({{< relref "./grafana-plugin-sdk-for-go" >}}) that implements this protocol and provides higher-level APIs. {{% |
||||
/admonition %}} |
||||
|
||||
If you choose to develop against the plugin protocol directly, you can do so using [Protocol Buffers](https://developers.google.com/protocol-buffers) (that is, protobufs) with [gRPC](https://grpc.io/). |
||||
|
||||
Grafana's plugin protocol protobufs are available in the [GitHub repository](https://github.com/grafana/grafana-plugin-sdk-go/blob/master/proto/backend.proto). |
||||
|
||||
{{% admonition type="note" %}} |
||||
The plugin protocol lives in the [Grafana Plugin SDK for Go]({{< relref "./grafana-plugin-sdk-for-go.md" >}}) because Grafana itself uses parts of the SDK as a dependency. |
||||
{{% /admonition %}} |
||||
|
||||
## Versioning |
||||
|
||||
From time to time, Grafana will offer additions of services, messages, and fields in the latest version of the plugin protocol. We don't expect these updates to introduce any breaking changes. However, if we must introduce breaking changes to the plugin protocol, we'll create a new major version of the plugin protocol. |
||||
|
||||
Grafana will release new major versions of the plugin protocol alongside new major Grafana releases. When this happens, we'll support both the old and the new plugin protocol for some time to make sure existing backend plugins continue to work. |
||||
|
||||
The plugin protocol attempts to follow Grafana's versioning. However, that doesn't mean we will automatically create a new major version of the plugin protocol when a new major release of Grafana is released. |
||||
|
||||
## Writing plugins without Go |
||||
|
||||
If you want to write a backend plugin in a language other than Go, then it's possible as long as the language supports gRPC. However, we recommend that you develop your plugin in Go for several reasons: |
||||
|
||||
- We offer an official plugin SDK. |
||||
- The compiled output is a single binary. |
||||
- Writing for multiple platforms is easy. Typically, no additional dependencies must be installed on the target platform. |
||||
- Small footprint for binary size. |
||||
- Small footprint for resource usage. |
@ -1,215 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../plugins/data-frames/ |
||||
description: Learn about data frames and how they work in plugins. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- data frames |
||||
- dataframes |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Data frames |
||||
--- |
||||
|
||||
# Data frames |
||||
|
||||
Grafana supports a variety of different data sources, each with its own data model. To make this possible, Grafana consolidates the query results from each of these data sources into one unified data structure called a _data frame_. |
||||
|
||||
The data frame structure is a concept that's borrowed from data analysis tools like the [R programming language](https://www.r-project.org) and [Pandas](https://pandas.pydata.org/). |
||||
|
||||
> **Note:** Data frames are available in Grafana 7.0+, and replaced the Time series and Table structures with a more generic data structure that can support a wider range of data types. |
||||
|
||||
This document gives an overview of the data frame structure, and of how data is handled within Grafana. |
||||
|
||||
## Data frame fields |
||||
|
||||
A data frame is a collection of _fields_, where each field corresponds to a column. Each field, in turn, consists of a collection of values and metadata, such as the data type of those values. |
||||
|
||||
```ts |
||||
export interface Field<T = any, V = Vector<T>> { |
||||
/** |
||||
* Name of the field (column) |
||||
*/ |
||||
name: string; |
||||
/** |
||||
* Field value type (string, number, and so on) |
||||
*/ |
||||
type: FieldType; |
||||
/** |
||||
* Meta info about how field and how to display it |
||||
*/ |
||||
config: FieldConfig; |
||||
|
||||
/** |
||||
* The raw field values |
||||
* In Grafana 10, this accepts both simple arrays and the Vector interface |
||||
* In Grafana 11, the Vector interface will be removed |
||||
*/ |
||||
values: V | T[]; |
||||
|
||||
/** |
||||
* When type === FieldType.Time, this can optionally store |
||||
* the nanosecond-precison fractions as integers between |
||||
* 0 and 999999. |
||||
*/ |
||||
nanos?: number[]; |
||||
|
||||
labels?: Labels; |
||||
|
||||
/** |
||||
* Cached values with appropriate display and id values |
||||
*/ |
||||
state?: FieldState | null; |
||||
|
||||
/** |
||||
* Convert a value for display |
||||
*/ |
||||
display?: DisplayProcessor; |
||||
|
||||
/** |
||||
* Get value data links with variables interpolated |
||||
*/ |
||||
getLinks?: (config: ValueLinkConfig) => Array<LinkModel<Field>>; |
||||
} |
||||
``` |
||||
|
||||
Let's look at an example. The following table demonstrates a data frame with two fields, _time_ and _temperature_: |
||||
|
||||
| time | temperature | |
||||
| ------------------- | ----------- | |
||||
| 2020-01-02 03:04:00 | 45.0 | |
||||
| 2020-01-02 03:05:00 | 47.0 | |
||||
| 2020-01-02 03:06:00 | 48.0 | |
||||
|
||||
Each field has three values, and each value in a field must share the same type. In this case, all values in the `time` field are timestamps, and all values in the `temperature` field are numbers. |
||||
|
||||
One restriction on data frames is that all fields in the frame must be of the same length to be a valid data frame. |
||||
|
||||
## Field configurations |
||||
|
||||
Each field in a data frame contains optional information about the values in the field, such as units, scaling, and so on. |
||||
|
||||
By adding field configurations to a data frame, Grafana can configure visualizations automatically. For example, you could configure Grafana to automatically set the unit provided by the data source. |
||||
|
||||
## Data transformations |
||||
|
||||
We have seen how field configs contain type information, and they also have another role. Data frame fields enable _data transformations_ within Grafana. |
||||
|
||||
A data transformation is any function that accepts a data frame as input, and returns another data frame as output. By using data frames in your plugin, you get a range of transformations for free. |
||||
|
||||
To learn more about data transformations in Grafana, refer to [Transform data]({{< relref "../../../panels-visualizations/query-transform-data/transform-data" >}}). |
||||
|
||||
## Data frames as time series |
||||
|
||||
A data frame with at least one time field is considered a _time series_. |
||||
|
||||
For more information on time series, refer to our [Introduction to time series]({{< relref "../../../fundamentals/timeseries" >}}). |
||||
|
||||
### Wide format |
||||
|
||||
When a collection of time series shares the same _time index_—the time fields in each time series are identical—they can be stored together, in a _wide_ format. By reusing the time field, less data is sent to the browser. |
||||
|
||||
In this example, the `cpu` usage from each host shares the time index, so we can store them in the same data frame: |
||||
|
||||
```text |
||||
Name: Wide |
||||
Dimensions: 3 fields by 2 rows |
||||
+---------------------+-----------------+-----------------+ |
||||
| Name: time | Name: cpu | Name: cpu | |
||||
| Labels: | Labels: host=a | Labels: host=b | |
||||
| Type: []time.Time | Type: []float64 | Type: []float64 | |
||||
+---------------------+-----------------+-----------------+ |
||||
| 2020-01-02 03:04:00 | 3 | 4 | |
||||
| 2020-01-02 03:05:00 | 6 | 7 | |
||||
+---------------------+-----------------+-----------------+ |
||||
``` |
||||
|
||||
However, if the two time series don't share the same time values, they are represented as two distinct data frames: |
||||
|
||||
```text |
||||
Name: cpu |
||||
Dimensions: 2 fields by 2 rows |
||||
+---------------------+-----------------+ |
||||
| Name: time | Name: cpu | |
||||
| Labels: | Labels: host=a | |
||||
| Type: []time.Time | Type: []float64 | |
||||
+---------------------+-----------------+ |
||||
| 2020-01-02 03:04:00 | 3 | |
||||
| 2020-01-02 03:05:00 | 6 | |
||||
+---------------------+-----------------+ |
||||
|
||||
Name: cpu |
||||
Dimensions: 2 fields by 2 rows |
||||
+---------------------+-----------------+ |
||||
| Name: time | Name: cpu | |
||||
| Labels: | Labels: host=b | |
||||
| Type: []time.Time | Type: []float64 | |
||||
+---------------------+-----------------+ |
||||
| 2020-01-02 03:04:01 | 4 | |
||||
| 2020-01-02 03:05:01 | 7 | |
||||
+---------------------+-----------------+ |
||||
``` |
||||
|
||||
A typical use for the wide format is when multiple time series are collected by the same process. In this case, every measurement is made at the same interval and therefore shares the same time values. |
||||
|
||||
### Long format |
||||
|
||||
Some data sources return data in a _long_ format (also called _narrow_ format). This is a common format returned by, for example, SQL databases. |
||||
|
||||
In the long format, string values are represented as separate fields rather than as labels. As a result, a data form in long form may have duplicated time values. |
||||
|
||||
Grafana can detect and convert data frames in long format into wide format. |
||||
|
||||
For example, the following data frame appears in long format: |
||||
|
||||
```text |
||||
Name: Long |
||||
Dimensions: 4 fields by 4 rows |
||||
+---------------------+-----------------+-----------------+----------------+ |
||||
| Name: time | Name: aMetric | Name: bMetric | Name: host | |
||||
| Labels: | Labels: | Labels: | Labels: | |
||||
| Type: []time.Time | Type: []float64 | Type: []float64 | Type: []string | |
||||
+---------------------+-----------------+-----------------+----------------+ |
||||
| 2020-01-02 03:04:00 | 2 | 10 | foo | |
||||
| 2020-01-02 03:04:00 | 5 | 15 | bar | |
||||
| 2020-01-02 03:05:00 | 3 | 11 | foo | |
||||
| 2020-01-02 03:05:00 | 6 | 16 | bar | |
||||
+---------------------+-----------------+-----------------+----------------+ |
||||
``` |
||||
|
||||
The above table can be converted into a data frame in wide format like this: |
||||
|
||||
```text |
||||
Name: Wide |
||||
Dimensions: 5 fields by 2 rows |
||||
+---------------------+------------------+------------------+------------------+------------------+ |
||||
| Name: time | Name: aMetric | Name: bMetric | Name: aMetric | Name: bMetric | |
||||
| Labels: | Labels: host=foo | Labels: host=foo | Labels: host=bar | Labels: host=bar | |
||||
| Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []float64 | |
||||
+---------------------+------------------+------------------+------------------+------------------+ |
||||
| 2020-01-02 03:04:00 | 2 | 10 | 5 | 15 | |
||||
| 2020-01-02 03:05:00 | 3 | 11 | 6 | 16 | |
||||
+---------------------+------------------+------------------+------------------+------------------+ |
||||
``` |
||||
|
||||
> **Note:** Not all panels support the wide time series data frame format. To keep full backward compatibility Grafana has introduced a transformation that you can use to convert from the wide to the long format. For usage information, refer to the [Prepare time series-transformation]({{< relref "../../../panels-visualizations/query-transform-data/transform-data#prepare-time-series" >}}). |
||||
|
||||
## Technical references |
||||
|
||||
The concept of a data frame in Grafana is borrowed from data analysis tools like the [R programming language](https://www.r-project.org), and [Pandas](https://pandas.pydata.org/). Other technical references are provided below. |
||||
|
||||
### Apache Arrow |
||||
|
||||
The data frame structure is inspired by, and uses the [Apache Arrow Project](https://arrow.apache.org/). Javascript Data frames use Arrow Tables as the underlying structure, and the backend Go code serializes its Frames in Arrow Tables for transmission. |
||||
|
||||
### Javascript |
||||
|
||||
The Javascript implementation of data frames is in the [`/src/dataframe` folder](https://github.com/grafana/grafana/tree/main/packages/grafana-data/src/dataframe) and [`/src/types/dataframe.ts`](https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/types/dataFrame.ts) of the [`@grafana/data` package](https://github.com/grafana/grafana/tree/main/packages/grafana-data). |
||||
|
||||
### Go |
||||
|
||||
For documentation on the Go implementation of data frames, refer to the [github.com/grafana/grafana-plugin-sdk-go/data package](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/data?tab=doc). |
@ -1,78 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../plugins/error-handling/ |
||||
description: How to handle errors in plugins. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- errors |
||||
- error handling |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Work with error handling |
||||
--- |
||||
|
||||
# Work with error handling |
||||
|
||||
This guide explains how to handle errors in plugins and provides suggestions for common scenarios. |
||||
|
||||
## Provide usable defaults |
||||
|
||||
Allow the user to learn your plugin in small steps. Provide a useful default configuration so that: |
||||
|
||||
- The user can get started right away. |
||||
- You can avoid unnecessary error messages. |
||||
|
||||
For example, by selecting the first field of an expected type, the panel can display a visualization without any user configuration. If a user explicitly selects a field, then use that one. Otherwise, default to the first field of type `string`: |
||||
|
||||
```ts |
||||
const numberField = frame.fields.find((field) => |
||||
options.numberFieldName ? field.name === options.numberFieldName : field.type === FieldType.number |
||||
); |
||||
``` |
||||
|
||||
## Display error messages |
||||
|
||||
To display an error message to the user, `throw` an `Error` with the message you want to display: |
||||
|
||||
```ts |
||||
throw new Error('An error occurred'); |
||||
``` |
||||
|
||||
Grafana displays the error message in the top-left corner of the panel. |
||||
|
||||
{{< figure src="/static/img/docs/panel_error.png" class="docs-image--no-shadow" max-width="850px" >}} |
||||
|
||||
We recommend that you avoid displaying overly technical error messages to the user. If you want to let technical users report an error, consider logging it to the console instead. |
||||
|
||||
```ts |
||||
try { |
||||
failingFunction(); |
||||
} catch (err) { |
||||
console.error(err); |
||||
throw new Error('Something went wrong'); |
||||
} |
||||
``` |
||||
|
||||
> **Note:** Grafana displays the exception message in the UI as written, so use grammatically correct sentences. For more information, refer to the [Documentation style guide](/docs/writers-toolkit/). |
||||
|
||||
## Common error scenarios |
||||
|
||||
Here are some examples of situations where you might want to display an error to the user. |
||||
|
||||
### Invalid query response |
||||
|
||||
Users have full freedom when they create data source queries for panels. If your panel plugin requires a specific format for the query response, then use the panel canvas to guide the user. |
||||
|
||||
```ts |
||||
if (!numberField) { |
||||
throw new Error('Query result is missing a number field'); |
||||
} |
||||
|
||||
if (frame.length === 0) { |
||||
throw new Error('Query returned an empty result'); |
||||
} |
||||
``` |
@ -1,138 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../plugins/development/ |
||||
- /docs/grafana/next/plugins/apps/ |
||||
- /docs/grafana/next/plugins/datasources/ |
||||
- /docs/grafana/next/plugins/developing/development/ |
||||
- /docs/grafana/next/plugins/panels/ |
||||
description: Deprecated guide for Angular plugin development. |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Work with legacy plugins |
||||
weight: 600 |
||||
--- |
||||
|
||||
# Work with legacy plugins |
||||
|
||||
> **Note:** Since Grafana 7.0, writing plugins using Angular is no longer recommended. If you're looking to build a new plugin, refer to our [Plugins]({{< relref "../../plugins" >}}) documentation. |
||||
|
||||
You can extend Grafana by writing your own plugins and then share them with other users in [our plugin repository](/plugins/). |
||||
|
||||
Grafana already has a strong community of contributors and plugin developers. By making it easier to develop and install plugins with resources such as this guide, we hope that the community can grow even stronger and develop new plugins that we would never think about. |
||||
|
||||
## Short version |
||||
|
||||
1. [Set up Grafana](https://github.com/grafana/grafana/blob/main/contribute/developer-guide.md) |
||||
1. Clone an example plugin into `/var/lib/grafana/plugins` or `data/plugins` (relative to grafana git repo if you're running development version from source dir) |
||||
1. Use one of our example plugins as a starting point |
||||
|
||||
Example plugins |
||||
|
||||
- ["Hello World" panel using Angular](https://github.com/grafana/simple-angular-panel) |
||||
- ["Hello World" panel using React](https://github.com/grafana/simple-react-panel) |
||||
- [Simple json data source](https://github.com/grafana/simple-json-datasource) |
||||
- [Clock panel](https://github.com/grafana/clock-panel) |
||||
- [Pie chart panel](https://github.com/grafana/piechart-panel) |
||||
|
||||
You might also be interested in the available tutorials around authoring a plugin. |
||||
|
||||
- [Grafana Tutorials]({{< relref "../create-a-grafana-plugin/develop-a-plugin" >}}) |
||||
|
||||
## What languages? |
||||
|
||||
Since everything turns into JavaScript, it's up to you to choose which language you want. That said, it's probably a good idea to choose es6 or TypeScript, because we use es6 classes in Grafana. So it's easier to get inspiration from the Grafana repo if you choose one of those languages. |
||||
|
||||
## Buildscript |
||||
|
||||
You can use any build system that supports `systemjs`. All the built content should end up in a folder named `dist` and be committed to the repository. By committing the dist folder, the person who installs your plugin does not have to run any build script. All of our example plugins have a build script configured. |
||||
|
||||
## Keep your plugin up to date |
||||
|
||||
New versions of Grafana can sometimes cause plugins to break. See our [documentation]({{< relref "../migration-guide" >}}) for changes in |
||||
Grafana that can impact your plugin. |
||||
|
||||
## Metadata |
||||
|
||||
See the [coding styleguide]({{< relref "./style-guide.md" >}}) for details on the metadata. |
||||
|
||||
## module.(js|ts) |
||||
|
||||
This is the entry point for every plugin. This is the place where you should export |
||||
your plugin implementation. Depending on what kind of plugin you are developing you |
||||
will be expected to export different things. You can find what's expected for [datasource]({{< relref "./data-sources.md" >}}), [panels]({{< relref "./panels.md" >}}) |
||||
and [apps]({{< relref "./apps.md" >}}) plugins in the documentation. |
||||
|
||||
The Grafana SDK is quite small so far and can be found here: |
||||
|
||||
- [SDK file in Grafana](https://github.com/grafana/grafana/blob/main/public/app/plugins/sdk.ts) |
||||
|
||||
The SDK contains three different plugin classes: PanelCtrl, MetricsPanelCtrl and QueryCtrl. For plugins of the panel type, the module.js file should export one of these. There are some extra classes for [data sources]({{< relref "./data-sources.md" >}}). |
||||
|
||||
Example: |
||||
|
||||
```javascript |
||||
import { ClockCtrl } from './clock_ctrl'; |
||||
|
||||
export { ClockCtrl as PanelCtrl }; |
||||
``` |
||||
|
||||
The module class is also where css for the dark and light themes is imported: |
||||
|
||||
```javascript |
||||
import { loadPluginCss } from 'app/plugins/sdk'; |
||||
import WorldmapCtrl from './worldmap_ctrl'; |
||||
|
||||
loadPluginCss({ |
||||
dark: 'plugins/grafana-worldmap-panel/css/worldmap.dark.css', |
||||
light: 'plugins/grafana-worldmap-panel/css/worldmap.light.css', |
||||
}); |
||||
|
||||
export { WorldmapCtrl as PanelCtrl }; |
||||
``` |
||||
|
||||
## Start developing your plugin |
||||
|
||||
There are three ways that you can start developing a Grafana plugin. |
||||
|
||||
1. Set up a Grafana development environment. [(described here)](https://github.com/grafana/grafana/blob/main/contribute/developer-guide.md) and place your plugin in the `data/plugins` folder. |
||||
1. Install Grafana and place your plugin in the plugins directory which is set in your config file. By default this is `/var/lib/grafana/plugins` on Linux systems. |
||||
1. Place your plugin directory anywhere you like and specify it grafana.ini. |
||||
|
||||
We encourage people to set up the full Grafana environment so that you can get inspiration from the rest of the Grafana code base. |
||||
|
||||
When Grafana starts, it scans the plugin folders and mounts every folder that contains a plugin.json file unless |
||||
the folder contains a subfolder named dist. In that case, Grafana mounts the dist folder instead. |
||||
This makes it possible to have both built and src content in the same plugin Git repo. |
||||
|
||||
## Grafana Events |
||||
|
||||
There are a number of Grafana events that a plugin can hook into: |
||||
|
||||
- `init-edit-mode` can be used to add tabs when editing a panel |
||||
- `panel-teardown` can be used for clean up |
||||
- `data-received` is an event in that is triggered on data refresh and can be hooked into |
||||
- `data-snapshot-load` is an event triggered to load data when in snapshot mode. |
||||
- `data-error` is used to handle errors on dashboard refresh. |
||||
|
||||
If a panel receives data and hooks into the `data-received` event then it should handle snapshot mode too. Otherwise the panel will not work if saved as a snapshot. [Getting Plugins to work in Snapshot Mode]({{< relref "./snapshot-mode" >}}) describes how to add support for this. |
||||
|
||||
## Examples |
||||
|
||||
We have three different examples that you can fork/download to get started developing your Grafana plugin. |
||||
|
||||
- [simple-json-datasource](https://github.com/grafana/simple-json-datasource) (small data source plugin for querying json data from backends) |
||||
- [simple-app-plugin](https://github.com/grafana/simple-app-plugin) |
||||
- [clock-panel](https://github.com/grafana/clock-panel) |
||||
- [singlestat-panel](https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/singlestat) |
||||
- [piechart-panel](https://github.com/grafana/piechart-panel) |
||||
|
||||
## Other Articles |
||||
|
||||
- [Getting Plugins to work in Snapshot Mode]({{< relref "./snapshot-mode.md" >}}) |
||||
- [Plugin Defaults and Editor Mode]({{< relref "./defaults-and-editor-mode.md" >}}) |
||||
- [Grafana Plugin Code Styleguide]({{< relref "./style-guide.md" >}}) |
||||
- [Grafana Apps]({{< relref "./apps.md" >}}) |
||||
- [Grafana Data Sources]({{< relref "./data-sources.md" >}}) |
||||
- [plugin.json Schema]({{< relref "../metadata.md" >}}) |
@ -1,61 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../../plugins/developing/apps/ |
||||
description: Deprecated guide for Angular plugin development. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Legacy app plugins |
||||
--- |
||||
|
||||
# Legacy app plugins |
||||
|
||||
App plugins are Grafana plugins that can bundle data source and panel plugins within one package. They also enable the plugin author to create custom pages within Grafana. The custom pages enable the plugin author to include things like documentation, sign-up forms, or to control other services with HTTP requests. |
||||
|
||||
Data source and panel plugins will show up like normal plugins. The app pages will be available in the main menu. |
||||
|
||||
## Enabling app plugins |
||||
|
||||
After installing an app, it has to be enabled before it shows up as a data source or panel. You can do that on the app page in the configuration tab. |
||||
|
||||
## Developing an App Plugin |
||||
|
||||
An App is a bundle of panels, dashboards and/or data source(s). There is nothing different about developing panels and data sources for an app. |
||||
|
||||
Apps have to be enabled in Grafana and should import any included dashboards when the user enables it. A ConfigCtrl class should be created and the dashboards imported in the postUpdate hook. See example below: |
||||
|
||||
```javascript |
||||
export class ConfigCtrl { |
||||
/** @ngInject */ |
||||
constructor($scope, $injector, $q) { |
||||
this.$q = $q; |
||||
this.enabled = false; |
||||
this.appEditCtrl.setPostUpdateHook(this.postUpdate.bind(this)); |
||||
} |
||||
|
||||
postUpdate() { |
||||
if (!this.appModel.enabled) { |
||||
return; |
||||
} |
||||
|
||||
// TODO, whatever you want |
||||
console.log('Post Update:', this); |
||||
} |
||||
} |
||||
ConfigCtrl.templateUrl = 'components/config/config.html'; |
||||
``` |
||||
|
||||
If possible, a link to a dashboard or custom page should be shown after enabling the app to guide the user to the appropriate place. |
||||
|
||||
{{< figure class="float-right" src="/static/img/docs/app_plugin_after_enable.png" caption="After enabling" >}} |
||||
|
||||
### Develop your own App |
||||
|
||||
> Our goal is not to have a very extensive documentation but rather have actual |
||||
> code that people can look at. An example implementation of an app can be found |
||||
> in this [example app repo](https://github.com/grafana/simple-app-plugin) |
@ -1,189 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../../plugins/developing/datasources/ |
||||
description: Deprecated guide for Angular plugin development. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Legacy data source plugins |
||||
--- |
||||
|
||||
# Legacy data source plugins |
||||
|
||||
Data source plugins enable people to develop plugins for any database that |
||||
communicates over HTTP. Its up to the plugin to transform the data into |
||||
time series data so that any grafana panel can then show it. |
||||
|
||||
## Data source development |
||||
|
||||
> Our goal is not to have a very extensive documentation but rather have actual |
||||
> code that people can look at. Example implementations of a data source can be |
||||
> found in these repos: |
||||
|
||||
> - [simple-json-datasource](https://github.com/grafana/simple-json-datasource) |
||||
> - [simple-datasource](https://github.com/grafana/simple-datasource) |
||||
> - [simple-json-backend-datasource](https://github.com/grafana/simple-json-backend-datasource) |
||||
|
||||
To interact with the rest of grafana the plugins module file can export 4 different components. |
||||
|
||||
- Datasource (Required) |
||||
- QueryCtrl (Required) |
||||
- ConfigCtrl (Required) |
||||
- AnnotationsQueryCtrl |
||||
|
||||
## Plugin json |
||||
|
||||
There are two data source specific settings for the plugin.json |
||||
|
||||
```json |
||||
"metrics": true, |
||||
"annotations": false, |
||||
``` |
||||
|
||||
These settings indicate what kind of data the plugin can deliver. At least one of them has to be true. |
||||
|
||||
## Data source |
||||
|
||||
The javascript object that communicates with the database and transforms data to times series. |
||||
|
||||
The Data source should contain the following functions: |
||||
|
||||
```javascript |
||||
query(options); // used by panels to get data |
||||
testDatasource(); // used by data source configuration page to make sure the connection is working |
||||
annotationQuery(options); // used by dashboards to get annotations |
||||
metricFindQuery(options); // used by query editor to get metric suggestions. |
||||
``` |
||||
|
||||
### testDatasource |
||||
|
||||
When a user clicks on the _Save & Test_ button when adding a new data source, the details are first saved to the database and then the `testDatasource` function that is defined in your data source plugin will be called. It is recommended that this function makes a query to the data source that will also test that the authentication details are correct. This is so the data source is correctly configured when the user tries to write a query in a new dashboard. |
||||
|
||||
### Query |
||||
|
||||
Request object passed to datasource.query function: |
||||
|
||||
```json |
||||
{ |
||||
"range": { "from": "2015-12-22T03:06:13.851Z", "to": "2015-12-22T06:48:24.137Z" }, |
||||
"interval": "5s", |
||||
"targets": [ |
||||
{ "refId": "B", "target": "upper_75" }, |
||||
{ "refId": "A", "target": "upper_90" } |
||||
], |
||||
"format": "json", |
||||
"maxDataPoints": 2495 // decided by the panel |
||||
} |
||||
``` |
||||
|
||||
There are two different kinds of results for data sources: |
||||
time series and table. Time series is the most common format and is supported by all data sources and panels. Table format is only supported by the InfluxDB data source and table panel. But we might see more of this in the future. |
||||
|
||||
Time series response from datasource.query. |
||||
An array of: |
||||
|
||||
```json |
||||
[ |
||||
{ |
||||
"target": "upper_75", |
||||
"datapoints": [ |
||||
[622, 1450754160000], |
||||
[365, 1450754220000] |
||||
] |
||||
}, |
||||
{ |
||||
"target": "upper_90", |
||||
"datapoints": [ |
||||
[861, 1450754160000], |
||||
[767, 1450754220000] |
||||
] |
||||
} |
||||
] |
||||
``` |
||||
|
||||
Table response from datasource.query. |
||||
An array of: |
||||
|
||||
```json |
||||
[ |
||||
{ |
||||
"columns": [ |
||||
{ |
||||
"text": "Time", |
||||
"type": "time", |
||||
"sort": true, |
||||
"desc": true |
||||
}, |
||||
{ |
||||
"text": "mean" |
||||
}, |
||||
{ |
||||
"text": "sum" |
||||
} |
||||
], |
||||
"rows": [ |
||||
[1457425380000, null, null], |
||||
[1457425370000, 1002.76215352, 1002.76215352] |
||||
], |
||||
"type": "table" |
||||
} |
||||
] |
||||
``` |
||||
|
||||
### Annotation Query |
||||
|
||||
Request object passed to datasource.annotationQuery function: |
||||
|
||||
```json |
||||
{ |
||||
"range": { "from": "2016-03-04T04:07:55.144Z", "to": "2016-03-04T07:07:55.144Z" }, |
||||
"rangeRaw": { "from": "now-3h", "to": "now" }, |
||||
"annotation": { |
||||
"datasource": "generic datasource", |
||||
"enable": true, |
||||
"name": "annotation name" |
||||
}, |
||||
"dashboard": DashboardModel |
||||
} |
||||
``` |
||||
|
||||
Expected result from datasource.annotationQuery: |
||||
|
||||
```json |
||||
[ |
||||
{ |
||||
"annotation": { |
||||
"name": "annotation name", //should match the annotation name in grafana |
||||
"enabled": true, |
||||
"datasource": "generic datasource" |
||||
}, |
||||
"title": "Cluster outage", |
||||
"time": 1457075272576, |
||||
"text": "Joe causes brain split", |
||||
"tags": ["joe", "cluster", "failure"] |
||||
} |
||||
] |
||||
``` |
||||
|
||||
## QueryCtrl |
||||
|
||||
A JavaScript class that will be instantiated and treated as an Angular controller when the user edits metrics in a panel. This class has to inherit from the `app/plugins/sdk.QueryCtrl` class. |
||||
|
||||
Requires a static template or `templateUrl` variable which will be rendered as the view for this controller. |
||||
|
||||
## ConfigCtrl |
||||
|
||||
A JavaScript class that will be instantiated and treated as an Angular controller when a user tries to edit or create a new data source of this type. |
||||
|
||||
Requires a static template or `templateUrl` variable which will be rendered as the view for this controller. |
||||
|
||||
## AnnotationsQueryCtrl |
||||
|
||||
A JavaScript class that will be instantiated and treated as an Angular controller when the user chooses this type of data source in the templating menu in the dashboard. |
||||
|
||||
Requires a static template or `templateUrl` variable which will be rendered as the view for this controller. The fields that are bound to this controller are then sent to the Database objects annotationQuery function. |
@ -1,38 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../../plugins/developing/panels/ |
||||
description: Deprecated guide for Angular plugin development. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- panel |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Legacy panel plugins |
||||
--- |
||||
|
||||
# Legacy panel plugins |
||||
|
||||
Panels are the main building blocks of dashboards. |
||||
|
||||
## Panel development |
||||
|
||||
### Scrolling |
||||
|
||||
The grafana dashboard framework controls the panel height. To enable a scrollbar within the panel the PanelCtrl needs to set the scrollable static variable: |
||||
|
||||
```javascript |
||||
export class MyPanelCtrl extends PanelCtrl { |
||||
static scrollable = true; |
||||
... |
||||
``` |
||||
|
||||
In this case, make sure the template has a single `<div>...</div>` root. The plugin loader will modify that element adding a scrollbar. |
||||
|
||||
### Examples |
||||
|
||||
- [clock-panel](https://github.com/grafana/clock-panel) |
||||
- [singlestat-panel](https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/singlestat) |
@ -1,186 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../../plugins/developing/plugin-review-guidelines/ |
||||
description: Deprecated guide for Angular plugin development. |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Legacy review guidelines |
||||
--- |
||||
|
||||
# Legacy review guidelines |
||||
|
||||
The Grafana team reviews all plugins that are published on Grafana.com. There are two areas we review, the metadata for the plugin and the plugin functionality. |
||||
|
||||
## Metadata |
||||
|
||||
The plugin metadata consists of a `plugin.json` file and the README.md file. The `plugin.json` file is used by Grafana to load the plugin, and the README.md file is shown in the [plugins catalog](/plugins). |
||||
|
||||
### README.md |
||||
|
||||
The README.md file is shown on the plugins page in Grafana and the plugin page on Grafana.com. There are some differences between the GitHub markdown and the markdown allowed in Grafana/Grafana.com: |
||||
|
||||
- Cannot contain inline HTML. |
||||
- Any image links should be absolute links. For example: https://raw.githubusercontent.com/grafana/azure-monitor-datasource/master/dist/img/grafana_cloud_install.png |
||||
|
||||
The README should: |
||||
|
||||
- describe the purpose of the plugin. |
||||
- contain steps on how to get started. |
||||
|
||||
### Plugin.json |
||||
|
||||
The `plugin.json` file is the same concept as the `package.json` file for an npm package. When the Grafana server starts it will scan the plugin folders (all folders in the data/plugins subfolder) and load every folder that contains a `plugin.json` file unless the folder contains a subfolder named `dist`. In that case, the Grafana server will load the `dist` folder instead. |
||||
|
||||
A minimal `plugin.json` file: |
||||
|
||||
```json |
||||
{ |
||||
"type": "panel", |
||||
"name": "Clock", |
||||
"id": "yourorg-clock-panel", |
||||
|
||||
"info": { |
||||
"description": "Clock panel for grafana", |
||||
"author": { |
||||
"name": "Author Name", |
||||
"url": "http://yourwebsite.com" |
||||
}, |
||||
"keywords": ["clock", "panel"], |
||||
"version": "1.0.0", |
||||
"updated": "2018-03-24" |
||||
}, |
||||
|
||||
"dependencies": { |
||||
"grafanaVersion": "3.x.x", |
||||
"plugins": [] |
||||
} |
||||
} |
||||
``` |
||||
|
||||
- The convention for the plugin id is **[grafana.com username/org]-[plugin name]-[datasource|app|panel]** and it has to be unique. The org **cannot** be `grafana` unless it is a plugin created by the Grafana core team. |
||||
|
||||
Examples: |
||||
|
||||
- raintank-worldping-app |
||||
- ryantxu-ajax-panel |
||||
- alexanderzobnin-zabbix-app |
||||
- hawkular-datasource |
||||
|
||||
- The `type` field should be either `datasource` `app` or `panel`. |
||||
- The `version` field should be in the form: x.x.x e.g. `1.0.0` or `0.4.1`. |
||||
|
||||
The full file format for `plugin.json` file is in [plugin.json](http://docs.grafana.org/plugins/developing/plugin.json/). |
||||
|
||||
## Plugin Language |
||||
|
||||
JavaScript, TypeScript, ES6 (or any other language) are all fine as long as the contents of the `dist` subdirectory are transpiled to JavaScript (ES5). |
||||
|
||||
## File and Directory Structure Conventions |
||||
|
||||
Here is a typical directory structure for a plugin. |
||||
|
||||
```bash |
||||
johnnyb-awesome-datasource |
||||
|-- dist |
||||
|-- src |
||||
| |-- img |
||||
| | |-- logo.svg |
||||
| |-- partials |
||||
| | |-- annotations.editor.html |
||||
| | |-- config.html |
||||
| | |-- query.editor.html |
||||
| |-- datasource.js |
||||
| |-- module.js |
||||
| |-- plugin.json |
||||
| |-- query_ctrl.js |
||||
|-- Gruntfile.js |
||||
|-- LICENSE |
||||
|-- package.json |
||||
|-- README.md |
||||
``` |
||||
|
||||
Most JavaScript projects have a build step. The generated JavaScript should be placed in the `dist` directory and the source code in the `src` directory. We recommend that the plugin.json file be placed in the src directory and then copied over to the dist directory when building. The `README.md` can be placed in the root or in the dist directory. |
||||
|
||||
Directories: |
||||
|
||||
- `src/` contains plugin source files. |
||||
- `src/partials` contains html templates. |
||||
- `src/img` contains plugin logos and other images. |
||||
- `dist/` contains built content. |
||||
|
||||
## HTML and CSS |
||||
|
||||
For the HTML on editor tabs, we recommend using the inbuilt Grafana styles rather than defining your own. This makes plugins feel like a more natural part of Grafana. If done correctly, the html will also be responsive and adapt to smaller screens. The `gf-form` css classes should be used for labels and inputs. |
||||
|
||||
Below is a minimal example of an editor row with one form group and two fields, a dropdown and a text input: |
||||
|
||||
```html |
||||
<div class="editor-row"> |
||||
<div class="section gf-form-group"> |
||||
<h5 class="section-heading">My Plugin Options</h5> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label width-10">Label1</label> |
||||
<div class="gf-form-select-wrapper max-width-10"> |
||||
<select |
||||
class="input-small gf-form-input" |
||||
ng-model="ctrl.panel.mySelectProperty" |
||||
ng-options="t for t in ['option1', 'option2', 'option3']" |
||||
ng-change="ctrl.onSelectChange()" |
||||
></select> |
||||
</div> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label width-10">Label2</label> |
||||
<input |
||||
type="text" |
||||
class="input-small gf-form-input width-10" |
||||
ng-model="ctrl.panel.myProperty" |
||||
ng-change="ctrl.onFieldChange()" |
||||
placeholder="suggestion for user" |
||||
ng-model-onblur |
||||
/> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
``` |
||||
|
||||
Use the `width-x` and `max-width-x` classes to control the width of your labels and input fields. Try to get labels and input fields to line up neatly by having the same width for all the labels in a group and the same width for all inputs in a group if possible. |
||||
|
||||
## Data Sources |
||||
|
||||
For more information about data sources, refer to the [basic guide for data sources](http://docs.grafana.org/plugins/developing/datasources/). |
||||
|
||||
### Configuration Page Guidelines |
||||
|
||||
- It should be as easy as possible for a user to configure a URL. If the data source is using the `datasource-http-settings` component, it should use the `suggest-url` attribute to suggest the default URL or a URL that is similar to what it should be (especially important if the URL refers to a REST endpoint that is not common knowledge for most users e.g. `https://yourserver:4000/api/custom-endpoint`). |
||||
|
||||
```html |
||||
<datasource-http-settings current="ctrl.current" suggest-url="http://localhost:8080"> </datasource-http-settings> |
||||
``` |
||||
|
||||
- The `testDatasource` function should make a query to the data source that will also test that the authentication details are correct. This is so the data source is correctly configured when the user tries to write a query in a new dashboard. |
||||
|
||||
#### Password Security |
||||
|
||||
If possible, any passwords or secrets should be saved in the `secureJsonData` blob. To encrypt sensitive data, the Grafana server's proxy feature must be used. The Grafana server has support for token authentication (OAuth) and HTTP Header authentication. If the calls have to be sent directly from the browser to a third-party API, this will not be possible and sensitive data will not be encrypted. |
||||
|
||||
Read more here about how [authentication for data sources]({{< relref "../create-a-grafana-plugin/extend-a-plugin/add-authentication-for-data-source-plugins" >}}) works. |
||||
|
||||
If using the proxy feature, the Configuration page should use the `secureJsonData` blob like this: |
||||
|
||||
- good: `<input type="password" class="gf-form-input" ng-model='ctrl.current.secureJsonData.password' placeholder="password"></input>` |
||||
- bad: `<input type="password" class="gf-form-input" ng-model='ctrl.current.password' placeholder="password"></input>` |
||||
|
||||
### Query Editor |
||||
|
||||
Each query editor is unique and can have a unique style. It should be adapted to what the users of the data source are used to. |
||||
|
||||
- Should use the Grafana CSS `gf-form` classes. |
||||
- Should be neat and tidy. Labels and fields in columns should be aligned and should be the same width if possible. |
||||
- The data source should be able to handle when a user toggles a query (by clicking on the eye icon) and not execute the query. This is done by checking the `hide` property - an [example](https://github.com/grafana/grafana/blob/e75840737e81f70b6d169df21eca86a624d4bdc4/public/app/plugins/datasource/postgres/datasource.ts#L73). |
||||
- Should not execute queries if fields in the Query Editor are empty and the query will throw an exception (defensive programming). |
||||
- Should handle errors. There are two main ways to do this: |
||||
- use the notification system in Grafana to show a toaster pop-up with the error message. For an example of a pop-up with the error message, refer to [code in triggers_panel_ctrl](https://github.com/alexanderzobnin/grafana-zabbix/blob/fdbbba2fb03f5f2a4b3b0715415e09d5a4cf6cde/src/panel-triggers/triggers_panel_ctrl.js#L467-L471). |
||||
- provide an error notification in the query editor like the MySQL/Postgres data sources do. For an example of error notification in the query editor, refer to [code in query_ctrl](https://github.com/grafana/azure-monitor-datasource/blob/b184d077f082a69f962120ef0d1f8296a0d46f03/src/query_ctrl.ts#L36-L51) and in the [html](https://github.com/grafana/azure-monitor-datasource/blob/b184d077f082a69f962120ef0d1f8296a0d46f03/src/partials/query.editor.html#L190-L193). |
@ -1,81 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../../plugins/developing/snapshot-mode/ |
||||
description: Deprecated guide for Angular plugin development. |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Legacy snapshot mode |
||||
--- |
||||
|
||||
# Legacy snapshot mode |
||||
|
||||
{{< figure class="float-right" src="/static/img/docs/Grafana-snapshot-example.png" caption="A dashboard using snapshot data and not live data." >}} |
||||
|
||||
Grafana has this great feature where you can [save a snapshot of your dashboard]({{< relref "../../../dashboards/build-dashboards/view-dashboard-json-model" >}}). Instead of sending a screenshot of a dashboard to someone, you can send them a working, interactive Grafana dashboard with the snapshot data embedded inside it. The snapshot can be saved on your Grafana server and is available to all your co-workers. Raintank also hosts a [snapshot server](https://snapshots.raintank.io) if you want to send the snapshot to someone who does not have access to your Grafana server. |
||||
|
||||
{{< figure class="float-right" src="/static/img/docs/animated_gifs/snapshots.gif" caption="Selecting a snapshot" >}} |
||||
|
||||
This all works because Grafana saves a snapshot of the current data in the dashboard json instead of fetching the data from a data source. However, if you are building a custom panel plugin then this will not work straight out of the box. You will need to make some small (and easy!) changes first. |
||||
|
||||
## Enabling support for loading snapshot data |
||||
|
||||
Grafana automatically saves data from data sources in the dashboard json when the snapshot is created so we do not have to write any code for that. Enabling snapshot support for reading time series data is very simple. First in the constructor, we need to add an event handler for `data-snapshot-load`. This event is triggered by Grafana when the snapshot data is loaded from the dashboard json. |
||||
|
||||
```javascript |
||||
constructor($scope, $injector, contextSrv) { |
||||
super($scope, $injector); |
||||
... |
||||
this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); |
||||
this.events.on('data-received', this.onDataReceived.bind(this)); |
||||
this.events.on('panel-teardown', this.onPanelTeardown.bind(this)); |
||||
this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this)); |
||||
``` |
||||
|
||||
Then we need to create a simple event handler that just forwards the data on to our regular `data-received` handler: |
||||
|
||||
```javascript |
||||
onDataSnapshotLoad(snapshotData) { |
||||
this.onDataReceived(snapshotData); |
||||
} |
||||
``` |
||||
|
||||
This will cover most use cases for snapshot support. Sometimes you will want to save data that is not time series data from a Grafana data source and then you have to do a bit more work to get snapshot support. |
||||
|
||||
## Saving custom data for snapshots |
||||
|
||||
Data that is not time series data from a Grafana data source is not saved automatically by Grafana. Saving custom data for snapshot mode has to be done manually. |
||||
|
||||
{{< figure class="float-right" src="/static/img/docs/Grafana-save-snapshot.png" caption="Save snapshot" >}} |
||||
|
||||
Grafana gives us a chance to save data to the dashboard json when it is creating a snapshot. In the 'data-received' event handler, you can check the snapshot flag on the dashboard object. If this is true, then Grafana is creating a snapshot and you can manually save custom data to the panel json. In the example, a new field called snapshotLocationData in the panel json is initialized with a snapshot of the custom data. |
||||
|
||||
```javascript |
||||
onDataReceived(dataList) { |
||||
if (!dataList) return; |
||||
|
||||
if (this.dashboard.snapshot && this.locations) { |
||||
this.panel.snapshotLocationData = this.locations; |
||||
} |
||||
``` |
||||
|
||||
Now the location data is saved in the dashboard json but we will have to load it manually as well. |
||||
|
||||
## Loading custom data for snapshots |
||||
|
||||
The example below shows a function that loads the custom data. The data source for the custom data (an external API in this case) is not available in snapshot mode so a guard check is made to see if there is any snapshot data available first. If there is, then the snapshot data is used instead of trying to load the data from the external API. |
||||
|
||||
```javascript |
||||
loadLocationDataFromFile(reload) { |
||||
if (this.map && !reload) return; |
||||
|
||||
if (this.panel.snapshotLocationData) { |
||||
this.locations = this.panel.snapshotLocationData; |
||||
return; |
||||
} |
||||
``` |
||||
|
||||
It is really easy to forget to add this support but it enables a great feature and can be used to demo your panel. |
||||
|
||||
If there is a panel plugin that you would like to be installed on the Raintank Snapshot server then please contact us via [Slack](https://slack.grafana.com) or [GitHub](https://github.com/grafana/grafana). |
@ -1,198 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../../plugins/developing/code-styleguide/ |
||||
description: Deprecated guide for Angular plugin development. |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Legacy code style guide |
||||
--- |
||||
|
||||
# Legacy code style guide |
||||
|
||||
This guide has two parts. The first part describes the metadata and the second part is a styleguide for HTML/CSS and JavaScript in Grafana plugins and applies if you are using ES6 in your plugin. If using TypeScript then the [Angular TypeScript styleguide](https://angular.io/styleguide) is recommended. |
||||
|
||||
## Metadata |
||||
|
||||
The plugin metadata consists of a plugin.json file and the README.md file. These two files are used by Grafana and Grafana.com. |
||||
|
||||
### Plugin.json (mandatory) |
||||
|
||||
The plugin.json file is the same concept as the package.json file for an npm package. When Grafana starts it will scan the plugin folders and mount every folder that contains a plugin.json file unless the folder contains a subfolder named `dist`. In that case grafana will mount the `dist` folder instead. |
||||
|
||||
The most important fields are the first three, especially the id. The convention for the plugin id is **[github username/org]-[plugin name]-[datasource|app|panel]** and it has to be unique. |
||||
|
||||
Examples: |
||||
|
||||
```bash |
||||
raintank-worldping-app |
||||
grafana-simple-json-datasource |
||||
grafana-piechart-panel |
||||
mtanda-histogram-panel |
||||
``` |
||||
|
||||
For more information about the file format for `plugin.json` file, refer to [metadata]({{< relref "../metadata.md" >}}). |
||||
|
||||
Minimal plugin.json: |
||||
|
||||
```javascript |
||||
{ |
||||
"type": "panel", |
||||
"name": "Clock", |
||||
"id": "yourorg-clock-panel", |
||||
|
||||
"info": { |
||||
"description": "Clock panel for grafana", |
||||
"author": { |
||||
"name": "Grafana Labs", |
||||
"url": "https://grafana.com" |
||||
}, |
||||
"keywords": ["clock", "panel"], |
||||
"version": "1.0.0", |
||||
"updated": "2015-03-24" |
||||
}, |
||||
|
||||
"dependencies": { |
||||
"grafanaVersion": "3.x.x", |
||||
"plugins": [ ] |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### README.md |
||||
|
||||
The README.md file is rendered both in the grafana.com plugins page, and within the Grafana application. The only difference from how GitHub renders markdown is that html is not allowed. |
||||
|
||||
## File and Directory Structure Conventions |
||||
|
||||
Here is a typical directory structure for a plugin. |
||||
|
||||
```bash |
||||
johnnyb-awesome-datasource |
||||
|-- dist |
||||
|-- spec |
||||
| |-- datasource_spec.js |
||||
| |-- query_ctrl_spec.js |
||||
| |-- test-main.js |
||||
|-- src |
||||
| |-- img |
||||
| | |-- logo.svg |
||||
| |-- partials |
||||
| | |-- annotations.editor.html |
||||
| | |-- config.html |
||||
| | |-- query.editor.html |
||||
| |-- datasource.js |
||||
| |-- module.js |
||||
| |-- plugin.json |
||||
| |-- query_ctrl.js |
||||
|-- Gruntfile.js |
||||
|-- LICENSE |
||||
|-- package.json |
||||
|-- README.md |
||||
``` |
||||
|
||||
Most JavaScript projects have a build step and most Grafana plugins are built using Babel and ES6. The generated JavaScript should be placed in the `dist` directory and the source code in the `src` directory. We recommend that the plugin.json file be placed in the src directory and then copied over to the dist directory when building. The `README.md` can be placed in the root or in the dist directory. |
||||
|
||||
Directories: |
||||
|
||||
- `src/` contains plugin source files. |
||||
- `src/partials` contains html templates. |
||||
- `src/img` contains plugin logos and other images. |
||||
- `spec/` contains tests (optional). |
||||
- `dist/` contains built content. |
||||
|
||||
## HTML and CSS |
||||
|
||||
For the HTML on editor tabs, we recommend using the inbuilt Grafana styles rather than defining your own. This makes plugins feel like a more natural part of Grafana. If done correctly, the html will also be responsive and adapt to smaller screens. The `gf-form` css classes should be used for labels and inputs. |
||||
|
||||
Below is a minimal example of an editor row with one form group and two fields, a dropdown and a text input: |
||||
|
||||
```html |
||||
<div class="editor-row"> |
||||
<div class="section gf-form-group"> |
||||
<h5 class="section-heading">My Plugin Options</h5> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label width-10">Label1</label> |
||||
<div class="gf-form-select-wrapper max-width-10"> |
||||
<select |
||||
class="input-small gf-form-input" |
||||
ng-model="ctrl.panel.mySelectProperty" |
||||
ng-options="t for t in ['option1', 'option2', 'option3']" |
||||
ng-change="ctrl.onSelectChange()" |
||||
></select> |
||||
</div> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label width-10">Label2</label> |
||||
<input |
||||
type="text" |
||||
class="input-small gf-form-input width-10" |
||||
ng-model="ctrl.panel.myProperty" |
||||
ng-change="ctrl.onFieldChange()" |
||||
placeholder="suggestion for user" |
||||
ng-model-onblur |
||||
/> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
``` |
||||
|
||||
Use the `width-x` and `max-width-x` classes to control the width of your labels and input fields. Try to get labels and input fields to line up neatly by having the same width for all the labels in a group and the same width for all inputs in a group if possible. |
||||
|
||||
## Build Scripts |
||||
|
||||
Our recommendation is to use whatever you usually use - Grunt, Gulp or npm scripts. Most plugins seems to use Grunt so that is probably the easiest to get started with if you do not have a preferred build system. The only requirement is that it supports systemjs which is required by Grafana to load plugins. |
||||
|
||||
## Linting |
||||
|
||||
We recommend that you use a linter for your JavaScript. For ES6, the standard linter is [eslint](http://eslint.org/). Rules for linting are described in an .eslintrc that is placed in the root directory. For an example of linting rules in a plugin, refer to [.eslintrc](https://github.com/grafana/worldmap-panel/blob/master/.eslintrc). |
||||
|
||||
### ES6 features |
||||
|
||||
1. Use `const` if a variable is not going to be reassigned. |
||||
1. Prefer to use `let` instead `var` ([Exploring ES6](http://exploringjs.com/es6/ch_core-features.html#_from-var-to-letconst)) |
||||
1. Use arrow functions, which don’t shadow `this` ([Exploring ES6](http://exploringjs.com/es6/ch_core-features.html#_from-function-expressions-to-arrow-functions)): |
||||
|
||||
```js |
||||
testDatasource() { |
||||
return this.getServerStatus() |
||||
.then(status => { |
||||
return this.doSomething(status); |
||||
}) |
||||
} |
||||
``` |
||||
|
||||
better than |
||||
|
||||
```js |
||||
testDatasource() { |
||||
var self = this; |
||||
return this.getServerStatus() |
||||
.then(function(status) { |
||||
return self.doSomething(status); |
||||
}) |
||||
} |
||||
``` |
||||
|
||||
1. Use native _Promise_ object: |
||||
|
||||
```js |
||||
metricFindQuery(query) { |
||||
if (!query) { |
||||
return Promise.resolve([]); |
||||
} |
||||
} |
||||
``` |
||||
|
||||
better than |
||||
|
||||
```js |
||||
metricFindQuery(query) { |
||||
if (!query) { |
||||
return this.$q.when([]); |
||||
} |
||||
} |
||||
``` |
||||
|
||||
1. If using Lodash, then be consistent and prefer that to the native ES6 array functions. |
@ -1,259 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../plugins/developing/plugin.json/ |
||||
description: Reference for the plugin.json metadata file. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
menuTitle: Reference (plugin.json) |
||||
title: plugin.json |
||||
weight: 700 |
||||
--- |
||||
|
||||
# plugin.json |
||||
|
||||
The plugin.json file is required for all plugins. When Grafana starts, it scans the plugin folders and mounts every folder that contains a plugin.json file unless the folder contains a subfolder named dist. In that case, Grafana mounts the dist folder instead. |
||||
|
||||
## Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------------------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
||||
| `id` | string | **Yes** | Unique name of the plugin. If the plugin is published on grafana.com, then the plugin `id` should follow the Grafana naming convention. | |
||||
| `name` | string | **Yes** | Human-readable name of the plugin that is shown to the user in the UI. | |
||||
| `type` | string | **Yes** | Plugin type. Possible values are: `app`, `datasource`, `panel`, `renderer`, `secretsmanager`. | |
||||
| `info` | [object](#info) | **Yes** | Metadata for the plugin. Some fields are used on the plugins page in Grafana and others on grafana.com if the plugin is published. | |
||||
| `dependencies` | [object](#dependencies) | **Yes** | Dependency information related to Grafana and other plugins. | |
||||
| `$schema` | string | No | Schema definition for the plugin.json file. Used primarily for schema validation. | |
||||
| `alerting` | boolean | No | For data source plugins, if the plugin supports alerting. Requires `backend` to be set to `true`. | |
||||
| `annotations` | boolean | No | For data source plugins, if the plugin supports annotation queries. | |
||||
| `autoEnabled` | boolean | No | Set to true for app plugins that should be enabled and pinned to the navigation bar in all orgs. | |
||||
| `backend` | boolean | No | If the plugin has a backend component. | |
||||
| `category` | string | No | Plugin category used on the "Add data source" page. Possible values are: `tsdb`, `logging`, `cloud`, `tracing`, `profiling`, `sql`, `enterprise`, `iot`, `other`. | |
||||
| `enterpriseFeatures` | [object](#enterprisefeatures) | No | Grafana Enterprise specific features | |
||||
| `executable` | string | No | The first part of the file name of the backend component executable. There can be multiple executables built for different operating system and architecture. Grafana will check for executables named `<executable>_<$GOOS>_<lower case $GOARCH><.exe for Windows>`, e.g. `plugin_linux_amd64`. Combination of $GOOS and $GOARCH can be found here: https://golang.org/doc/install/source#environment. | |
||||
| `includes` | [object](#includes)[] | No | Resources to include in plugin. | |
||||
| `logs` | boolean | No | For data source plugins, if the plugin supports logs. It may be used to filter logs only features. | |
||||
| `metrics` | boolean | No | For data source plugins, if the plugin supports metric queries. Used to enable the plugin in the panel editor. | |
||||
| `preload` | boolean | No | Initialize plugin on startup. By default, the plugin initializes on first use. Useful for app plugins that should load without user interaction. | |
||||
| `queryOptions` | [object](#queryoptions) | No | For data source plugins. There is a query options section in the plugin's query editor and these options can be turned on if needed. | |
||||
| `routes` | [object](#routes)[] | No | For data source plugins. Proxy routes used for plugin authentication and adding headers to HTTP requests made by the plugin. For more information, refer to [Authentication for data source plugins]({{< relref "./create-a-grafana-plugin/extend-a-plugin/add-authentication-for-data-source-plugins" >}}). | |
||||
| `skipDataQuery` | boolean | No | For panel plugins. Hides the query editor. | |
||||
| `state` | string | No | Marks a plugin as a pre-release. Possible values are: `alpha`, `beta`. | |
||||
| `streaming` | boolean | No | For data source plugins, if the plugin supports streaming. Used in Explore to start live streaming. | |
||||
| `tracing` | boolean | No | For data source plugins, if the plugin supports tracing. Used for example to link logs (e.g. Loki logs) with tracing plugins. | |
||||
|
||||
## dependencies |
||||
|
||||
Dependencies needed by the plugin. |
||||
|
||||
### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| ------------------- | -------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- | |
||||
| `grafanaDependency` | string | **Yes** | Required Grafana version for this plugin. Validated using https://github.com/npm/node-semver. | |
||||
| `grafanaVersion` | string | No | (Deprecated) Required Grafana version for this plugin, e.g. `6.x.x 7.x.x` to denote plugin requires Grafana v6.x.x or v7.x.x. | |
||||
| `plugins` | [object](#plugins)[] | No | An array of required plugins on which this plugin depends. | |
||||
|
||||
### plugins |
||||
|
||||
Plugin dependency. Used to display information about plugin dependencies in the Grafana UI. |
||||
|
||||
#### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| --------- | ------ | -------- | -------------------------------------------------- | |
||||
| `id` | string | **Yes** | | |
||||
| `name` | string | **Yes** | | |
||||
| `type` | string | **Yes** | Possible values are: `app`, `datasource`, `panel`. | |
||||
| `version` | string | **Yes** | | |
||||
|
||||
## enterpriseFeatures |
||||
|
||||
Grafana Enterprise specific features. |
||||
|
||||
### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| ------------------------- | ------- | -------- | ------------------------------------------------------------------- | |
||||
| `healthDiagnosticsErrors` | boolean | No | Enable/Disable health diagnostics errors. Requires Grafana >=7.5.5. | |
||||
|
||||
## includes |
||||
|
||||
### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| ------------ | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
||||
| `addToNav` | boolean | No | Add the include to the side menu. | |
||||
| `component` | string | No | (Legacy) The Angular component to use for a page. | |
||||
| `defaultNav` | boolean | No | Page or dashboard when user clicks the icon in the side menu. | |
||||
| `icon` | string | No | Icon to use in the side menu. For information on available icon, refer to [Icons Overview](https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview). | |
||||
| `name` | string | No | | |
||||
| `path` | string | No | Used for app plugins. | |
||||
| `role` | string | No | Possible values are: `Admin`, `Editor`, `Viewer`. | |
||||
| `type` | string | No | Possible values are: `dashboard`, `page`, `panel`, `datasource`. | |
||||
| `uid` | string | No | Unique identifier of the included resource | |
||||
|
||||
## info |
||||
|
||||
Metadata for the plugin. Some fields are used on the plugins page in Grafana and others on grafana.com if the plugin is published. |
||||
|
||||
### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| ------------- | ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------- | |
||||
| `keywords` | string[] | **Yes** | Array of plugin keywords. Used for search on grafana.com. | |
||||
| `logos` | [object](#logos) | **Yes** | SVG images that are used as plugin icons. | |
||||
| `updated` | string | **Yes** | Date when this plugin was built. | |
||||
| `version` | string | **Yes** | Project version of this commit, e.g. `6.7.x`. | |
||||
| `author` | [object](#author) | No | Information about the plugin author. | |
||||
| `build` | [object](#build) | No | Build information | |
||||
| `description` | string | No | Description of plugin. Used on the plugins page in Grafana and for search on grafana.com. | |
||||
| `links` | [object](#links)[] | No | An array of link objects to be displayed on this plugin's project page in the form `{name: 'foo', url: 'http://example.com'}` | |
||||
| `screenshots` | [object](#screenshots)[] | No | An array of screenshot objects in the form `{name: 'bar', path: 'img/screenshot.png'}` | |
||||
|
||||
### author |
||||
|
||||
Information about the plugin author. |
||||
|
||||
#### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------- | ------ | -------- | ------------------------- | |
||||
| `email` | string | No | Author's name. | |
||||
| `name` | string | No | Author's name. | |
||||
| `url` | string | No | Link to author's website. | |
||||
|
||||
### build |
||||
|
||||
Build information |
||||
|
||||
#### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------- | ------ | -------- | ---------------------------------------------------- | |
||||
| `branch` | string | No | Git branch the plugin was built from. | |
||||
| `hash` | string | No | Git hash of the commit the plugin was built from | |
||||
| `number` | number | No | | |
||||
| `pr` | number | No | GitHub pull request the plugin was built from | |
||||
| `repo` | string | No | | |
||||
| `time` | number | No | Time when the plugin was built, as a Unix timestamp. | |
||||
|
||||
### links |
||||
|
||||
#### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------- | ------ | -------- | ----------- | |
||||
| `name` | string | No | | |
||||
| `url` | string | No | | |
||||
|
||||
### logos |
||||
|
||||
SVG images that are used as plugin icons. |
||||
|
||||
#### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------- | |
||||
| `large` | string | **Yes** | Link to the "large" version of the plugin logo, which must be an SVG image. "Large" and "small" logos can be the same image. | |
||||
| `small` | string | **Yes** | Link to the "small" version of the plugin logo, which must be an SVG image. "Large" and "small" logos can be the same image. | |
||||
|
||||
### screenshots |
||||
|
||||
#### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------- | ------ | -------- | ----------- | |
||||
| `name` | string | No | | |
||||
| `path` | string | No | | |
||||
|
||||
## queryOptions |
||||
|
||||
For data source plugins. There is a query options section in the plugin's query editor and these options can be turned on if needed. |
||||
|
||||
### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| --------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------- | |
||||
| `cacheTimeout` | boolean | No | For data source plugins. If the `cache timeout` option should be shown in the query options section in the query editor. | |
||||
| `maxDataPoints` | boolean | No | For data source plugins. If the `max data points` option should be shown in the query options section in the query editor. | |
||||
| `minInterval` | boolean | No | For data source plugins. If the `min interval` option should be shown in the query options section in the query editor. | |
||||
|
||||
## routes |
||||
|
||||
For data source plugins. Proxy routes used for plugin authentication and adding headers to HTTP requests made by the plugin. For more information, refer to [Authentication for data source plugins]({{< relref "./create-a-grafana-plugin/extend-a-plugin/add-authentication-for-data-source-plugins" >}}). |
||||
|
||||
### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------------- | ----------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- | |
||||
| `body` | [object](#body) | No | For data source plugins. Route headers set the body content and length to the proxied request. | |
||||
| `headers` | array | No | For data source plugins. Route headers adds HTTP headers to the proxied request. | |
||||
| `jwtTokenAuth` | [object](#jwttokenauth) | No | For data source plugins. Token authentication section used with an JWT OAuth API. | |
||||
| `method` | string | No | For data source plugins. Route method matches the HTTP verb like GET or POST. Multiple methods can be provided as a comma-separated list. | |
||||
| `path` | string | No | For data source plugins. The route path that is replaced by the route URL field when proxying the call. | |
||||
| `reqRole` | string | No | | |
||||
| `reqSignedIn` | boolean | No | | |
||||
| `tokenAuth` | [object](#tokenauth) | No | For data source plugins. Token authentication section used with an OAuth API. | |
||||
| `url` | string | No | For data source plugins. Route URL is where the request is proxied to. | |
||||
|
||||
### body |
||||
|
||||
For data source plugins. Route headers set the body content and length to the proxied request. |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------- | ---- | -------- | ----------- | |
||||
|
||||
### jwtTokenAuth |
||||
|
||||
For data source plugins. Token authentication section used with an JWT OAuth API. |
||||
|
||||
#### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------- | ----------------- | -------- | --------------------------------------------------------------------- | |
||||
| `params` | [object](#params) | No | Parameters for the JWT token authentication request. | |
||||
| `scopes` | string[] | No | The list of scopes that your application should be granted access to. | |
||||
| `url` | string | No | URL to fetch the JWT token. | |
||||
|
||||
#### params |
||||
|
||||
Parameters for the JWT token authentication request. |
||||
|
||||
##### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------------- | ------ | -------- | ----------- | |
||||
| `client_email` | string | No | | |
||||
| `private_key` | string | No | | |
||||
| `token_uri` | string | No | | |
||||
|
||||
### tokenAuth |
||||
|
||||
For data source plugins. Token authentication section used with an OAuth API. |
||||
|
||||
#### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| -------- | ----------------- | -------- | --------------------------------------------------------------------- | |
||||
| `params` | [object](#params) | No | Parameters for the token authentication request. | |
||||
| `scopes` | string[] | No | The list of scopes that your application should be granted access to. | |
||||
| `url` | string | No | URL to fetch the authentication token. | |
||||
|
||||
#### params |
||||
|
||||
Parameters for the token authentication request. |
||||
|
||||
##### Properties |
||||
|
||||
| Property | Type | Required | Description | |
||||
| --------------- | ------ | -------- | ----------------------------------------------------------------------------------------- | |
||||
| `client_id` | string | No | OAuth client ID | |
||||
| `client_secret` | string | No | OAuth client secret. Usually populated by decrypting the secret from the SecureJson blob. | |
||||
| `grant_type` | string | No | OAuth grant type | |
||||
| `resource` | string | No | OAuth resource | |
@ -1,10 +0,0 @@ |
||||
+++ |
||||
# ------------------------------------------------------------------------- |
||||
# Do not edit this file. It is automatically generated by json-schema-docs. |
||||
# ------------------------------------------------------------------------- |
||||
title = "plugin.json" |
||||
keywords = ["grafana", "plugins", "documentation"] |
||||
aliases = ["/docs/grafana/latest/plugins/developing/plugin.json/"] |
||||
+++ |
||||
|
||||
{{ .Markdown 1 }} |
@ -1,47 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../plugins/developing/migration-guide |
||||
description: A guide for migrating and updating a Grafana plugin. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- migration |
||||
- plugin |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Migrate a plugin |
||||
weight: 500 |
||||
--- |
||||
|
||||
<script> |
||||
(function () { |
||||
// Previously all the migration docs were on a single page, and the different sections could be linked using URL hashes. |
||||
var anchorRedirects = { |
||||
"migrate-a-plugin-from-angular-to-react": "./angular-react/", |
||||
"from-version-62x-to-740": "./v6.x-v7.x#from-version-62x-to-740", |
||||
"from-version-65x-to-730": "./v6.x-v7.x#from-version-65x-to-730", |
||||
"from-version-6xx-to-700": "./v6.x-v7.x/", |
||||
"migrate-to-data-frames": "./v6.x-v7.x/", |
||||
"troubleshoot-plugin-migration": "./v6.x-v7.x/", |
||||
"from-version-7xx-to-8xx": "./v7.x-v8.x/", |
||||
"from-version-83x-to-84x": "./v8.3.x-8.4.x/", |
||||
"from-version-8x-to-9x": "./v8.x-v9.x/", |
||||
"from-version-91x-to-92x": "./v9.1.x-v9.2.x/", |
||||
"from-version-93x-to-94x": "./v9.3.x-9.4.x/", |
||||
}; |
||||
var hash = window.location.hash.substring(1); |
||||
var redirectTo = anchorRedirects[hash]; |
||||
if (redirectTo) { |
||||
window.location.replace(redirectTo); |
||||
} |
||||
})(); |
||||
</script> |
||||
|
||||
# Migrate a plugin |
||||
|
||||
The following guides help you identify the steps required to update a plugin following changes between versions of Grafana. |
||||
|
||||
{{< section menuTitle="true">}} |
@ -1,63 +0,0 @@ |
||||
--- |
||||
description: Guide for migrating plugins from AngularJS to React. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- migration |
||||
- plugin |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
menuTitle: Angular to React |
||||
title: Migrate a plugin from AngularJS to React |
||||
weight: 1000 |
||||
--- |
||||
|
||||
# Migrate a plugin from AngularJS to React |
||||
|
||||
If you want to migrate a plugin to Grafana's React-based plugin platform, then we recommend that you release it under a new major version. Consider keeping a release branch for the previous version to be able to roll out patch releases for versions prior to Grafana 7. |
||||
|
||||
While there's no standard migration path from an Angular plugin to the new React platform, we’ve learned that one of the easiest ways to migrate is to: |
||||
|
||||
1. Create a new branch called `migrate-to-react`. |
||||
1. Start from scratch with one of the templates provided by the [`create-plugin`](https://www.npmjs.com/package/@grafana/create-plugin) tool. |
||||
1. Move the existing code into the new plugin incrementally, one component at a time. |
||||
|
||||
## Migrate a panel plugin |
||||
|
||||
Starting with Grafana 7.0, plugins export a `PanelPlugin` from `module.ts` where `MyPanel`is a React component containing the props from `PanelProps`. |
||||
|
||||
**src/module.ts** |
||||
|
||||
```ts |
||||
import { PanelPlugin } from '@grafana/data'; |
||||
|
||||
export const plugin = new PanelPlugin<MyOptions>(MyPanel); |
||||
``` |
||||
|
||||
**src/MyPanel.tsx** |
||||
|
||||
```ts |
||||
import { PanelProps } from '@grafana/data'; |
||||
|
||||
interface Props extends PanelProps<SimpleOptions> {} |
||||
|
||||
export function MyPanel({ options, data, width, height }: Props) { |
||||
// ... |
||||
} |
||||
``` |
||||
|
||||
## Migrate a data source plugin |
||||
|
||||
While all plugins are different, we'd like to share a migration process that has worked for some of our users. |
||||
|
||||
1. Define your configuration model and `ConfigEditor`. For many plugins, the configuration editor is the simplest component, so it's a good candidate to start with. |
||||
1. Implement the `testDatasource()` method on the class that extends `DataSourceApi`. Use the settings in your configuration model to make sure that you can successfully configure and access the external API. |
||||
1. Implement the `query()` method. At this point, you can hard-code your query, because we haven’t yet implemented the query editor. The `query()` method supports both the new data frame response and the old `TimeSeries` response, so don’t worry about converting to the new format just yet. |
||||
1. Implement the `QueryEditor`. How much work this requires depends on how complex your query model is. |
||||
|
||||
By now, you should be able to release your new version. |
||||
|
||||
To fully migrate to the new plugin platform, convert the time series response to a data frame response. |
@ -1,33 +0,0 @@ |
||||
--- |
||||
description: Guide for migrating plugins from Grafana v10.0.x to v10.1.x |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- migration |
||||
- plugin |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
menutitle: v10.0.x to v10.1.x |
||||
title: Migrate plugins from Grafana 10.0.x to 10.1.x |
||||
weight: 1900 |
||||
--- |
||||
|
||||
# Migrate plugins from Grafana version 10.0.x to 10.1.x |
||||
|
||||
## Accessibility update for IconButton component in grafana-ui |
||||
|
||||
We updated the component's TypeScript interface due to an accessibility issue. This change was delivered to the core `grafana` repo with [PR 69699](https://github.com/grafana/grafana/pull/69699). |
||||
|
||||
In case you are using the IconButton component in your plugin you will get TypeScript errors related to the change. |
||||
|
||||
**Recommended actions:** |
||||
|
||||
- Review use cases of IconButton in your plugin. |
||||
- Add a meaningful tooltip which the component will also use as an aria-label. |
||||
- Another option is to set an aria-label. In this case a tooltip will not be shown. |
||||
|
||||
**Please note:** |
||||
The IconButton used to have a property called `ariaLabel` which got deprecated with this change. You can now use the regular property `aria-label` instead. |
@ -1,133 +0,0 @@ |
||||
--- |
||||
description: Guide for migrating plugins from Grafana v6.x to v7.x |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- migration |
||||
- plugin |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
menuTitle: v6.x to v7.x |
||||
title: Migrate plugins from Grafana version 6.x to 7.0 |
||||
weight: 2500 |
||||
--- |
||||
|
||||
# Migrate plugins from Grafana version 6.x to 7.0 |
||||
|
||||
Follow the instructions in this section to upgrade Grafana from version 6.x to 7.0. |
||||
|
||||
## What's new in Grafana 7.0? |
||||
|
||||
Grafana 7.0 introduced a whole new plugin platform based on React. This new platform supersedes the previous Angular-based plugin platform. |
||||
|
||||
Plugins built using Angular still work in the near term, but we strongly encourage new plugin authors to develop with the new platform. |
||||
|
||||
### New data format |
||||
|
||||
Along with the move to React, the new plugin platform introduced a new internal data format called [data frames]({{< relref "../../introduction-to-plugin-development/data-frames" >}}). |
||||
|
||||
Previously, data source plugins could send data either as time series or tables. With data frames, data sources can send any data in a table-like structure. This gives you more flexibility to visualize your data in Grafana. |
||||
|
||||
### Improved TypeScript support |
||||
|
||||
Although the previous Angular-based plugin SDK supported TypeScript, we’ve greatly improved the support for the React platform. All our APIs are now written in TypeScript, which might require existing code to update to the new stricter type definitions. Grafana 7.0 also introduced several new APIs for plugin developers that take advantage of many of the new features in Grafana 7.0. |
||||
|
||||
### Grafana Toolkit |
||||
|
||||
With Grafana 7.0, we released a new tool for making it easier to develop plugins. Before, you’d use Gulp, Grunt, or similar tools to generate the minified assets. Grafana Toolkit takes care of building and testing your plugin without complicated configuration files. |
||||
|
||||
For more information, refer to [@grafana/toolkit](https://www.npmjs.com/package/@grafana/toolkit). |
||||
|
||||
{{% admonition type="note" %}} As of Grafana 10.0, `@grafana/toolkit` is deprecated. It is replaced by the [`create-plugin`](https://grafana.github.io/plugin-tools/docs/get-started/) tool. |
||||
{{% /admonition %}} |
||||
|
||||
### Field options |
||||
|
||||
Grafana 7.0 introduced the concept of _field options_, a new way of configuring your data before it gets visualized. Since this was not available in previous versions, any plugin that enables field-based configuration will not work in previous versions of Grafana. |
||||
|
||||
For plugins prior to Grafana 7.0, all options are considered _Display options_. The tab for field configuration isn't available. |
||||
|
||||
### Backend plugins |
||||
|
||||
While backend plugins were available as an experimental feature in previous versions of Grafana, their support has been greatly improved for Grafana 7. Backend plugins for Grafana 7.0 are backwards-compatible and will continue to work. However, the old backend plugin system has been deprecated, and we recommend that you use the new SDK for backend plugins. |
||||
|
||||
Since Grafana 7.0 introduced signing of backend plugins, community plugins won’t load by default if they’re unsigned. |
||||
|
||||
### Migrate to data frames |
||||
|
||||
Before 7.0, data source and panel plugins exchanged data using either time series or tables. Starting with 7.0, plugins use the new data frame format to pass data from data sources to panels. |
||||
|
||||
Grafana 7.0 is backward compatible with the old data format used in previous versions. Panels and data sources using the old format will still work with plugins using the new data frame format. |
||||
|
||||
The `DataQueryResponse` returned by the `query` method can be either a [LegacyResponseData](https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/types/datasource.ts#L419) or a [DataFrame](https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/types/dataFrame.ts#L200). |
||||
|
||||
The [toDataFrame()](https://github.com/grafana/grafana/blob/main/packages/grafana-data/src/dataframe/processDataFrame.ts#L309) function converts a legacy response, such as `TimeSeries` or `Table`, to a `DataFrame`. Use it to gradually move your code to the new format. |
||||
|
||||
```ts |
||||
import { toDataFrame } from '@grafana/data'; |
||||
``` |
||||
|
||||
```ts |
||||
async query(options: DataQueryRequest<MyQuery>): Promise<DataQueryResponse> { |
||||
return { |
||||
data: options.targets.map(query => { |
||||
const timeSeries: TimeSeries = await doLegacyRequest(query); |
||||
return toDataFrame(timeSeries); |
||||
} |
||||
}; |
||||
} |
||||
``` |
||||
|
||||
## Troubleshoot plugin migration |
||||
|
||||
As of Grafana 7.0, backend plugins can now be cryptographically signed to verify their origin. By default, Grafana ignores unsigned plugins. For more information, refer to [Allow unsigned plugins]({{< relref "../../../../administration/plugin-management#allow-unsigned-plugins" >}}). |
||||
|
||||
## From version 6.5.x to 7.3.0 |
||||
|
||||
Follow the instructions in this section to upgrade Grafana from version 6.5.x to 7.3.0. |
||||
|
||||
### getColorForTheme changes |
||||
|
||||
The `getColorForTheme` function arguments have changed from `(color: ColorDefinition, theme?: GrafanaThemeType)` to `(color: string, theme: GrafanaTheme)`. |
||||
|
||||
```ts |
||||
// before |
||||
const color: ColorDefinition = { |
||||
hue: 'green'; |
||||
name: 'dark-green'; |
||||
variants: { |
||||
light: '#19730E' |
||||
dark: '#37872D' |
||||
}; |
||||
} |
||||
const themeType: GrafanaThemeType = 'dark'; |
||||
const themeColor = getColorForTheme(color, themeType); |
||||
|
||||
// after |
||||
const color = 'green'; |
||||
const theme: GrafanaTheme = useTheme(); |
||||
const themeColor = getColorForTheme(color, theme); |
||||
|
||||
``` |
||||
|
||||
## From 6.2.x to v7.4.x |
||||
|
||||
Follow the instructions in this section to upgrade Grafana from version 6.2.x to v7.4.x. |
||||
|
||||
### Legend components |
||||
|
||||
The Legend components have been refactored and introduced the following changes within the `@grafana/ui` package. |
||||
|
||||
```ts |
||||
// before |
||||
import { LegendItem, LegendOptions, GraphLegend } from '@grafana/ui'; |
||||
|
||||
// after |
||||
import { VizLegendItem, VizLegendOptions, VizLegend } from '@grafana/ui'; |
||||
``` |
||||
|
||||
- `LegendPlacement` has been updated from `'under' | 'right' | 'over'` to `'bottom' | 'right'`, so you can't place the legend above the visualization anymore. |
||||
- The `isVisible` attribute in the `LegendItem` has been renamed to `disabled` in `VizLegendItem`. |
@ -1,363 +0,0 @@ |
||||
--- |
||||
description: Guide for migrating plugins from Grafana v7.x to v8.x. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- migration |
||||
- plugin |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
menuTitle: v7.x to v8.x |
||||
title: Migrate plugins from Grafana version 7.x.x to 8.x.x |
||||
weight: 2400 |
||||
--- |
||||
|
||||
# Migrate plugins from Grafana version 7.x.x to 8.x.x |
||||
|
||||
This section explains how to migrate Grafana v7.x.x plugins to the updated plugin system available in Grafana v8.x.x. Depending on your plugin, you need to perform one or more of the following steps. |
||||
|
||||
In this section, we've documented the breaking changes in Grafana v8.x.x and the steps you need to take to upgrade your plugin. |
||||
|
||||
## Backend plugin v1 support has been dropped |
||||
|
||||
Use the new [plugin SDK for Go](https://github.com/grafana/grafana-plugin-sdk-go) to run your backend plugin running in Grafana 8. |
||||
|
||||
### 1. Add dependency on grafana-plugin-sdk-go |
||||
|
||||
Add a dependency on the `https://github.com/grafana/grafana-plugin-sdk-go`. We recommend using [Go modules](https://go.dev/blog/using-go-modules) to manage your dependencies. |
||||
|
||||
### 2. Update the way you bootstrap your plugin |
||||
|
||||
Update your `main` package to bootstrap via the new plugin Go SDK. |
||||
|
||||
```go |
||||
// before |
||||
package main |
||||
|
||||
import ( |
||||
"github.com/grafana/grafana_plugin_model/go/datasource" |
||||
hclog "github.com/hashicorp/go-hclog" |
||||
plugin "github.com/hashicorp/go-plugin" |
||||
|
||||
"github.com/myorgid/datasource/pkg/plugin" |
||||
) |
||||
|
||||
func main() { |
||||
pluginLogger.Debug("Running GRPC server") |
||||
|
||||
ds, err := NewSampleDatasource(pluginLogger); |
||||
if err != nil { |
||||
pluginLogger.Error("Unable to create plugin"); |
||||
} |
||||
|
||||
plugin.Serve(&plugin.ServeConfig{ |
||||
HandshakeConfig: plugin.HandshakeConfig{ |
||||
ProtocolVersion: 1, |
||||
MagicCookieKey: "grafana_plugin_type", |
||||
MagicCookieValue: "datasource", |
||||
}, |
||||
Plugins: map[string]plugin.Plugin{ |
||||
"myorgid-datasource": &datasource.DatasourcePluginImpl{Plugin: ds}, |
||||
}, |
||||
GRPCServer: plugin.DefaultGRPCServer, |
||||
}) |
||||
} |
||||
|
||||
// after |
||||
package main |
||||
|
||||
import ( |
||||
"os" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log" |
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource" |
||||
|
||||
"github.com/myorgid/datasource/pkg/plugin" |
||||
) |
||||
|
||||
func main() { |
||||
log.DefaultLogger.Debug("Running GRPC server") |
||||
|
||||
if err := datasource.Manage("myorgid-datasource", NewSampleDatasource, datasource.ManageOpts{}); err != nil { |
||||
log.DefaultLogger.Error(err.Error()) |
||||
os.Exit(1) |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### 3. Update the plugin package |
||||
|
||||
Update your `plugin` package to use the new plugin Go SDK: |
||||
|
||||
```go |
||||
// before |
||||
package plugin |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/grafana/grafana_plugin_model/go/datasource" |
||||
"github.com/hashicorp/go-hclog" |
||||
) |
||||
|
||||
func NewSampleDatasource(pluginLogger hclog.Logger) (*SampleDatasource, error) { |
||||
return &SampleDatasource{ |
||||
logger: pluginLogger, |
||||
}, nil |
||||
} |
||||
|
||||
type SampleDatasource struct{ |
||||
logger hclog.Logger |
||||
} |
||||
|
||||
func (d *SampleDatasource) Query(ctx context.Context, tsdbReq *datasource.DatasourceRequest) (*datasource.DatasourceResponse, error) { |
||||
d.logger.Info("QueryData called", "request", req) |
||||
// logic for querying your datasource. |
||||
} |
||||
|
||||
// after |
||||
package plugin |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend" |
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" |
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log" |
||||
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||
) |
||||
|
||||
func NewSampleDatasource(_ backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { |
||||
return &SampleDatasource{}, nil |
||||
} |
||||
|
||||
type SampleDatasource struct{} |
||||
|
||||
|
||||
func (d *SampleDatasource) Dispose() { |
||||
// Clean up datasource instance resources. |
||||
} |
||||
|
||||
func (d *SampleDatasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { |
||||
log.DefaultLogger.Info("QueryData called", "request", req) |
||||
// logic for querying your datasource. |
||||
} |
||||
|
||||
func (d *SampleDatasource) CheckHealth(_ context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { |
||||
log.DefaultLogger.Info("CheckHealth called", "request", req) |
||||
// The main use case for these health checks is the test button on the |
||||
// datasource configuration page which allows users to verify that |
||||
// a datasource is working as expected. |
||||
} |
||||
``` |
||||
|
||||
## Sign and load backend plugins |
||||
|
||||
We strongly recommend that you not allow unsigned plugins in your Grafana installation. By allowing unsigned plugins, we can't guarantee the authenticity of the plugin, which could compromise the security of your Grafana installation. |
||||
|
||||
To sign your plugin, see [Sign a plugin]({{< relref "../../publish-a-plugin/sign-a-plugin.md" >}}). |
||||
|
||||
You can still run and develop an unsigned plugin by running your Grafana instance in [development mode](/docs/grafana/latest/administration/configuration/#app_mode). Alternatively, you can use the [allow_loading_unsigned_plugins]({{< relref "../../../../setup-grafana/configure-grafana#allow_loading_unsigned_plugins" >}}) configuration setting. |
||||
|
||||
## Update react-hook-form from v6 to v7 |
||||
|
||||
We have upgraded react-hook-form from version 6 to version 7. To make your forms compatible with version 7, refer to the [react-hook-form-migration-guide](https://react-hook-form.com/migrate-v6-to-v7/). |
||||
|
||||
## Update the plugin.json |
||||
|
||||
The property that defines which Grafana version your plugin supports has been renamed and now it is a range instead of a specific version. |
||||
|
||||
```json |
||||
// before |
||||
{ |
||||
"dependencies": { |
||||
"grafanaVersion": "7.5.x", |
||||
"plugins": [] |
||||
} |
||||
} |
||||
|
||||
// after |
||||
{ |
||||
"dependencies": { |
||||
"grafanaDependency": ">=8.0.0", |
||||
"plugins": [] |
||||
} |
||||
} |
||||
``` |
||||
|
||||
## Update imports to match emotion 11 |
||||
|
||||
Grafana uses the Emotion library to manage frontend styling. We've updated the Emotion package and this can affect your frontend plugin if you have custom styling. You only need to update the `import` statements to get it working in Grafana 8. |
||||
|
||||
```ts |
||||
// before |
||||
import { cx, css } from 'emotion'; |
||||
|
||||
// after |
||||
import { cx, css } from '@emotion/css'; |
||||
``` |
||||
|
||||
## Update needed for app plugins using dashboards |
||||
|
||||
To make side navigation work properly - app plugins targeting Grafana `8.+` and integrating into the side menu via [addToNav]({{< relref "../../metadata#properties-4" >}}) property need to adjust their `plugin.json` and all dashboard json files to have a matching `uid`. |
||||
|
||||
**`plugin.json`** |
||||
|
||||
```json "linenos=inline,hl_lines=7,linenostart=1" |
||||
{ |
||||
"id": "plugin-id", |
||||
// ... |
||||
"includes": [ |
||||
{ |
||||
"type": "dashboard", |
||||
"name": "(Team) Situation Overview", |
||||
"path": "dashboards/example-dashboard.json", |
||||
"addToNav": true, |
||||
"defaultNav": false, |
||||
"uid": "l3KqBxCMz" |
||||
} |
||||
] |
||||
// ... |
||||
} |
||||
``` |
||||
|
||||
**`dashboards/example-dashboard.json`** |
||||
|
||||
```json |
||||
{ |
||||
// ... |
||||
"title": "Example Dashboard", |
||||
"uid": "l3KqBxCMz", |
||||
"version": 1 |
||||
// ... |
||||
} |
||||
``` |
||||
|
||||
## 8.0 deprecations |
||||
|
||||
The following features have been deprecated in version 8.0. |
||||
|
||||
### Grafana theme v1 |
||||
|
||||
In Grafana 8 we have introduced a new improved version of our theming system. The previous version of the theming system is still available but is deprecated and will be removed in the next major version of Grafana. |
||||
|
||||
You can find more detailed information on how to apply the v2 theme [here](https://github.com/grafana/grafana/blob/main/contribute/style-guides/themes.md#theming-grafana). |
||||
|
||||
#### How to style a functional component |
||||
|
||||
The `useStyles` hook is the preferred way to access the theme when styling. It provides basic memoization and access to the theme object: |
||||
|
||||
```ts |
||||
// before |
||||
import React, { ReactElement } from 'react'; |
||||
import css from 'emotion'; |
||||
import { GrafanaTheme } from '@grafana/data'; |
||||
import { useStyles } from '@grafana/ui'; |
||||
|
||||
function Component(): ReactElement | null { |
||||
const styles = useStyles(getStyles); |
||||
} |
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({ |
||||
myStyle: css` |
||||
background: ${theme.colors.bodyBg}; |
||||
display: flex; |
||||
`, |
||||
}); |
||||
|
||||
// after |
||||
import React, { ReactElement } from 'react'; |
||||
import { css } from '@emotion/css'; |
||||
import { GrafanaTheme2 } from '@grafana/data'; |
||||
import { useStyles2 } from '@grafana/ui'; |
||||
|
||||
function Component(): ReactElement | null { |
||||
const theme = useStyles2(getStyles); |
||||
} |
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({ |
||||
myStyle: css` |
||||
background: ${theme.colors.background.canvas}; |
||||
display: flex; |
||||
`, |
||||
}); |
||||
``` |
||||
|
||||
#### How to use the theme in a functional component |
||||
|
||||
This example shows how to use the theme in a functional component: |
||||
|
||||
```ts |
||||
// before |
||||
import React, { ReactElement } from 'react'; |
||||
import { useTheme } from '@grafana/ui'; |
||||
|
||||
function Component(): ReactElement | null { |
||||
const theme = useTheme(); |
||||
} |
||||
|
||||
// after |
||||
import React, { ReactElement } from 'react'; |
||||
import { useTheme2 } from '@grafana/ui'; |
||||
|
||||
function Component(): ReactElement | null { |
||||
const theme = useTheme2(); |
||||
// Your component has access to the theme variables now |
||||
} |
||||
``` |
||||
|
||||
#### How to use the theme in a class component |
||||
|
||||
This example shows how to use the theme in a class: |
||||
|
||||
```ts |
||||
// before |
||||
import React from 'react'; |
||||
import { Themeable, withTheme } from '@grafana/ui'; |
||||
|
||||
type Props = {} & Themeable; |
||||
|
||||
class Component extends React.Component<Props> { |
||||
render() { |
||||
const { theme } = this.props; |
||||
// Your component has access to the theme variables now |
||||
} |
||||
} |
||||
|
||||
export default withTheme(Component); |
||||
|
||||
// after |
||||
import React from 'react'; |
||||
import { Themeable2, withTheme2 } from '@grafana/ui'; |
||||
|
||||
type Props = {} & Themeable2; |
||||
|
||||
class Component extends React.Component<Props> { |
||||
render() { |
||||
const { theme } = this.props; |
||||
// Your component has access to the theme variables now |
||||
} |
||||
} |
||||
|
||||
export default withTheme2(Component); |
||||
``` |
||||
|
||||
## Gradual migration of components |
||||
|
||||
If you need to use both the v1 and v2 themes because you've used both migrated and non-migrated components in the same context, then use the `v1` property on the `v2` theme. |
||||
|
||||
**Example:** |
||||
|
||||
```ts |
||||
function Component(): ReactElement | null { |
||||
const theme = useTheme2(); |
||||
return ( |
||||
<NonMigrated theme={theme.v1}> |
||||
<Migrated theme={theme] /> |
||||
</NonMigrate> |
||||
); |
||||
}; |
||||
``` |
@ -1,113 +0,0 @@ |
||||
--- |
||||
description: Guide for migrating plugins from Grafana v8.3.x to v8.4.x |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- migration |
||||
- plugin |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
menuTitle: v8.3.x to v8.4.x |
||||
title: Migrate plugins from Grafana version 8.3.x to 8.4.x |
||||
weight: 2200 |
||||
--- |
||||
|
||||
# Migrate plugins from Grafana version 8.3.x to 8.4.x |
||||
|
||||
This section explains how to migrate Grafana v8.3.x plugins to the updated plugin system available in Grafana v8.4.x. Depending on your plugin, you need to perform one or more of the following steps. |
||||
|
||||
## Value Mapping Editor has been removed from @grafana-ui library |
||||
|
||||
Removed because it is an internal component. |
||||
|
||||
## Thresholds Editor has been removed from @grafana-ui library |
||||
|
||||
Removed because it is an internal component. |
||||
|
||||
## 8.4 deprecations |
||||
|
||||
The following features have been deprecated in version 8.4. |
||||
|
||||
### `LocationService` replaces `getLocationSrv` |
||||
|
||||
In a previous release, we migrated to use a new routing system and introduced a new service for managing locations, navigation, and related information. In this release, we are making that new service the primary service. |
||||
|
||||
**Example:** |
||||
|
||||
Import the service: |
||||
|
||||
```ts |
||||
// before |
||||
import { getLocationSrv } from '@grafana/runtime'; |
||||
|
||||
// after |
||||
import { locationService } from '@grafana/runtime'; |
||||
``` |
||||
|
||||
**Example:** |
||||
|
||||
Navigate to a path and add a new record in the navigation history so that you can navigate back to the previous one: |
||||
|
||||
```ts |
||||
// before |
||||
getLocationSrv.update({ |
||||
path: '/route-to-navigate-to', |
||||
replace: false, |
||||
}); |
||||
|
||||
// after |
||||
locationService.push('/route-to-navigate-to'); |
||||
``` |
||||
|
||||
**Example:** |
||||
|
||||
Navigate to a path and replace the current record in the navigation history: |
||||
|
||||
```ts |
||||
// before |
||||
getLocationSrv.update({ |
||||
path: '/route-to-navigate-to', |
||||
replace: true, |
||||
}); |
||||
|
||||
// after |
||||
locationService.replace('/route-to-navigate-to'); |
||||
``` |
||||
|
||||
**Example:** |
||||
|
||||
Update the search or query parameter for the current route and add a new record in the navigation history so that you can navigate back to the previous one: |
||||
|
||||
```ts |
||||
// How to navigate to a new path |
||||
// before |
||||
getLocationSrv.update({ |
||||
query: { |
||||
value: 1, |
||||
}, |
||||
partial: true, |
||||
replace: false, |
||||
}); |
||||
|
||||
// after |
||||
locationService.partial({ value: 1 }); |
||||
``` |
||||
|
||||
**Example:** Update the search or query parameter for the current route and add replacing it in the navigation history: |
||||
|
||||
```ts |
||||
// before |
||||
getLocationSrv.update({ |
||||
query: { |
||||
'var-variable': 1, |
||||
}, |
||||
partial: true, |
||||
replace: true, |
||||
}); |
||||
|
||||
// after |
||||
locationService.partial({ 'var-variable': 1 }, true); |
||||
``` |
@ -1,120 +0,0 @@ |
||||
--- |
||||
description: Guide for migrating plugins from Grafana v8.x to v9.x |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- migration |
||||
- plugin |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
menuTitle: v8.x to v9.x |
||||
title: Migrate plugins from Grafana version 8.x to 9.x |
||||
weight: 2300 |
||||
--- |
||||
|
||||
# Migrate plugins from Grafana version 8.x to 9.x |
||||
|
||||
Follow the instructions in this section to migrate plugins from version 8.x to 9.x. |
||||
|
||||
## 9.0 breaking changes |
||||
|
||||
The following breaking changes are introduced in version 9.0 of Grafana. |
||||
|
||||
### `theme.visualization.getColorByName` replaces `getColorForTheme` |
||||
|
||||
The `getColorForTheme` was removed, so you should use `theme.visualization.getColorByName` instead. |
||||
|
||||
**Example:** |
||||
|
||||
```ts |
||||
// before |
||||
fillColor: getColorForTheme(panel.sparkline.fillColor, config.theme) |
||||
|
||||
// after |
||||
fillColor: config.theme.visualization.getColorByName(panel.sparkline.fillColor), |
||||
``` |
||||
|
||||
### `VizTextDisplayOptions` replaces `TextDisplayOptions` |
||||
|
||||
The `TextDisplayOptions` was removed, so you should use `VizTextDisplayOptions` instead. |
||||
|
||||
**Example:** |
||||
|
||||
```ts |
||||
// before |
||||
interface Options { |
||||
... |
||||
text?: TextDisplayOptions; |
||||
... |
||||
} |
||||
|
||||
// after |
||||
interface Options { |
||||
... |
||||
text?: VizTextDisplayOptions; |
||||
... |
||||
} |
||||
``` |
||||
|
||||
### Changes in the internal of `backendSrv.fetch()` |
||||
|
||||
We have changed the internals of `backendSrv.fetch()` to throw an error when the response is an incorrect JSON. Make sure to handle possible errors on the callsite where using `backendSrv.fetch()` (or any other `backendSrv` methods). |
||||
|
||||
```ts |
||||
// PREVIOUSLY: this was returning with an empty object {} - in case the response is an invalid JSON |
||||
return await getBackendSrv().post(`${API_ROOT}/${id}/install`); |
||||
|
||||
// AFTER THIS CHANGE: the following will throw an error - in case the response is an invalid JSON |
||||
return await getBackendSrv().post(`${API_ROOT}/${id}/install`); |
||||
``` |
||||
|
||||
### `GrafanaTheme2` and `useStyles2` replaces `getFormStyles` |
||||
|
||||
We have removed the deprecated `getFormStyles` function from [grafana-ui](https://www.npmjs.com/package/@grafana/ui). Use `GrafanaTheme2` and the `useStyles2` hook instead. |
||||
|
||||
### `/api/ds/query` replaces `/api/tsdb/query` |
||||
|
||||
We have removed the deprecated `/api/tsdb/query` metrics endpoint. Use [/api/ds/query]({{< relref "../../../http_api/data_source#query-a-data-source" >}}) instead. |
||||
|
||||
### `selectOptionInTest` has been removed |
||||
|
||||
The `@grafana/ui` package helper function `selectOptionInTest` used in frontend tests has been removed because it caused testing libraries to be bundled in the production code of Grafana. If you were using this helper function in your tests, then update your code accordingly: |
||||
|
||||
```ts |
||||
// before |
||||
import { selectOptionInTest } from '@grafana/ui'; |
||||
// ...test usage |
||||
await selectOptionInTest(selectEl, 'Option 2'); |
||||
|
||||
// after |
||||
import { select } from 'react-select-event'; |
||||
// ...test usage |
||||
await select(selectEl, 'Option 2', { container: document.body }); |
||||
``` |
||||
|
||||
### Toolkit 9 and webpack |
||||
|
||||
Plugins using custom Webpack configs could potentially break due to the changes between webpack@4 and webpack@5. Please refer to the [official webpack migration guide](https://webpack.js.org/migrate/5/) for assistance. |
||||
|
||||
Webpack 5 does not include polyfills for node.js core modules by default (for example, `buffer`, `stream`, `os`). This can result in failed builds for plugins. If polyfills are required, then it is recommended to create a custom webpack config in the root of the plugin repo and add the required fallbacks: |
||||
|
||||
```js |
||||
// webpack.config.js |
||||
|
||||
module.exports.getWebpackConfig = (config, options) => ({ |
||||
...config, |
||||
resolve: { |
||||
...config.resolve, |
||||
fallback: { |
||||
os: require.resolve('os-browserify/browser'), |
||||
stream: require.resolve('stream-browserify'), |
||||
timers: require.resolve('timers-browserify'), |
||||
}, |
||||
}, |
||||
}); |
||||
``` |
||||
|
||||
Please refer to the webpack build error messages or the [official migration guide](https://webpack.js.org/migrate/5/) for assistance with fallbacks. |
@ -1,77 +0,0 @@ |
||||
--- |
||||
description: Guide for migrating plugins from Grafana v9.1.x to v9.2.x |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- migration |
||||
- plugin |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
menuTitle: v9.1.x to v9.2.x |
||||
title: Migrate plugins from Grafana version 9.1.x to 9.2.x |
||||
weight: 2100 |
||||
--- |
||||
|
||||
# Migrate plugins from Grafana version 9.1.x to 9.2.x |
||||
|
||||
Follow the instructions in this section to migrate plugins from Grafana version 9.1.x to 9.2.x. |
||||
|
||||
## React and React-dom as peer dependencies |
||||
|
||||
In earlier versions of Grafana packages, `react` and `react-dom` were installed during a `yarn install` command regardless of a plugin's dependencies. In version 9.2.0, the `@grafana` packages declare these React packages as `peerDependencies` and must be added to a plugin's `package.json` file for test commands. |
||||
|
||||
**Example:** |
||||
|
||||
```json |
||||
// before |
||||
"dependencies": { |
||||
"@grafana/data": "9.1.0", |
||||
"@grafana/ui": "9.1.0", |
||||
}, |
||||
|
||||
// after |
||||
"dependencies": { |
||||
"@grafana/data": "9.2.0", |
||||
"@grafana/ui": "9.2.0", |
||||
"react": "17.0.2", |
||||
"react-dom": "17.0.2" |
||||
}, |
||||
|
||||
``` |
||||
|
||||
## `NavModelItem` requires a valid icon name |
||||
|
||||
The typings of the `NavModelItem` have improved to only allow a valid `IconName` for the icon property. For a complete list of valid icons, refer to the [source code](https://github.com/grafana/grafana/blob/v9.2.0-beta1/packages/grafana-data/src/types/icon.ts). These icons will work for older versions of Grafana 9. |
||||
|
||||
**Example:** |
||||
|
||||
```ts |
||||
// before |
||||
const model: NavModelItem = { |
||||
id: 'settings', |
||||
text: 'Settings', |
||||
icon: 'fa fa-cog', |
||||
url: `${baseUrl}/settings`, |
||||
}; |
||||
|
||||
// after |
||||
const model: NavModelItem = { |
||||
id: 'settings', |
||||
text: 'Settings', |
||||
icon: 'cog', |
||||
url: `${baseUrl}/settings`, |
||||
}; |
||||
``` |
||||
|
||||
## Additional type availability |
||||
|
||||
`FieldProps`, `ModalProps`, and `QueryFieldProps` are now exposed from `@grafana/ui`. They can be imported in the same way as other types. |
||||
|
||||
**Example:** |
||||
|
||||
```ts |
||||
import { FieldProps, ModalProps, QueryFieldProps } from '@grafana/ui'; |
||||
``` |
@ -1,160 +0,0 @@ |
||||
--- |
||||
description: Guide for migrating plugins from Grafana v9.3.x to v9.4.x |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- migration |
||||
- plugin |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
menuTitle: v9.3.x to v9.4.x |
||||
title: Migrate plugins from Grafana 9.3.x to 9.4.x |
||||
weight: 2000 |
||||
--- |
||||
|
||||
# Migrate plugins from Grafana 9.3.x to 9.4.x |
||||
|
||||
Follow the instructions in this section to migrate from Grafana 9.3.x to 9.4.x. |
||||
|
||||
## New navigation layout is supported |
||||
|
||||
First, enable the `topnav` feature flag in `custom.ini` to check how your plugin renders in the new navigation layout: |
||||
|
||||
```ini |
||||
[feature_toggles] |
||||
enable = topnav |
||||
``` |
||||
|
||||
### Migrate from `onNavChanged` |
||||
|
||||
If your plugin uses the `onNavChanged` callback to inform Grafana of its navigation model and child pages, you should see that this results in duplicated navigation elements. If you disable `topnav`, then it should look just like before. |
||||
|
||||
If `topnav` is enabled, then we need to update the plugin to take advantage of the new `PluginPage` component. In this case, we do not call `onNavChanged`, which is now deprecated. |
||||
|
||||
### Switch to `PluginPage` component |
||||
|
||||
Grafana now exposes a new `PluginPage` component from `@grafana/runtime` that hooks into the new navigation and page layouts. This new component also supports the old page layouts when the `topnav` feature is disabled. |
||||
|
||||
The new `PluginPage` component will also handle rendering the section navigation. The section navigation can include other core sections and other plugins. To control what pages are displayed in the section navigation for a specific plugin, Grafana uses the pages that have been added in `plugin.json` in which `addToNav` was set to `true`. |
||||
|
||||
To use this component, simply wrap it around your page content: |
||||
|
||||
```tsx |
||||
import { PluginPage } from '@grafana/runtime'; |
||||
|
||||
... |
||||
|
||||
return ( |
||||
<PluginPage> |
||||
{your page content here} |
||||
</PluginPage> |
||||
); |
||||
``` |
||||
|
||||
Grafana looks at the URL to know what plugin and page should be active in the section nav. Accordingly, this component only works for pages that you have specified in `plugin.json`. The `PluginPage` will then render a page header based on the page name specified in `plugin.json`. |
||||
|
||||
### Use `PluginPage` for pages not defined in `plugin.json` |
||||
|
||||
The `PluginPage` component also exposes a `pageNav` property that is a `NavModelItem`. This `pageNav` property is useful for pages that are not defined in `plugin.json` (for example, individual item pages). The `text` and `description` that you specify in the `pageNav` model are used to populate the breadcrumbs and the page header. |
||||
|
||||
**Example:** |
||||
|
||||
```tsx |
||||
const pageNav = { |
||||
text: 'Write errors cortex-prod-04', |
||||
description: 'Incident timeline and details' |
||||
}; |
||||
|
||||
return ( |
||||
<PluginPage pageNav={pageNav}> |
||||
{your page content here} |
||||
</PluginPage> |
||||
); |
||||
``` |
||||
|
||||
The way the active page is matched in the breadcrumbs and section nav relies on the page routes being hierarchical. If you have a list page and an item page, then you need to make the item page into a subroute of the list page. Furthermore, you also need to specify the list page URL in your `plugin.json`. |
||||
|
||||
For example, you might have a list of users at `/users`. This means that the item page for a specific user needs to be at `/users/:id`. This may require some refactoring of your routes. |
||||
|
||||
### Use `PluginPage` with tabs |
||||
|
||||
You can also create a further layer of hierarchy by specifying `children` in the `pageNav` model to create a page with tabbed navigation. |
||||
|
||||
**Example:** |
||||
|
||||
```tsx |
||||
const pageNav = { |
||||
text: 'My page', |
||||
description: 'Incident timeline and details', |
||||
url: '/a/myorgid-pluginname-app', |
||||
children: [ |
||||
{ |
||||
url: '/a/myorgid-pluginname-app/tab1', |
||||
text: 'Tab1', |
||||
active: true, |
||||
}, |
||||
{ |
||||
url: '/a/myorgid-pluginname-app/tab2', |
||||
text: 'Tab1', |
||||
}, |
||||
], |
||||
}; |
||||
|
||||
return ( |
||||
<PluginPage pageNav={pageNav}> |
||||
{your page content here} |
||||
</PluginPage> |
||||
); |
||||
``` |
||||
|
||||
### Use `PluginPage` in a backwards-compatible way |
||||
|
||||
If you want to maintain backwards-compatibility with older versions of Grafana, one way is to implement a `PluginPage` wrapper. If `PluginPage` is available and the `topnav` feature is enabled, then use the real `PluginPage`. In other scenarios, fall back to whatever each plugin is doing today (such as making a call to `onNavChanged`). |
||||
|
||||
**Example:** |
||||
|
||||
```tsx |
||||
import { PluginPageProps, PluginPage as RealPluginPage, config } from '@grafana/runtime'; |
||||
|
||||
export const PluginPage = RealPluginPage && config.featureToggles.topnav ? RealPluginPage : PluginPageFallback; |
||||
|
||||
function PluginPageFallback(props: PluginPageProps) { |
||||
return props.children; |
||||
} |
||||
``` |
||||
|
||||
There’s an additional step (and `if` block) that is needed to hide or show tabs depending on whether `config.features.topnav` is `true`. Your plugin needs to include these changes in the `useNavModel.ts` file: |
||||
|
||||
```tsx |
||||
// useNavModel.ts |
||||
|
||||
import { config } from '@grafana/runtime'; |
||||
|
||||
... |
||||
|
||||
export function useNavModel({ meta, rootPath, onNavChanged }: Args) { |
||||
const { pathname, search } = useLocation(); |
||||
useEffect(() => { |
||||
if (config.featureToggles.topnav) { |
||||
return; |
||||
} |
||||
}, [config]); |
||||
|
||||
... |
||||
``` |
||||
|
||||
## Forwarded HTTP headers in the plugin SDK for Go |
||||
|
||||
We recommended to use the `<request>.GetHTTPHeader` or `<request>.GetHTTPHeaders` methods when retrieving forwarded HTTP headers. See [Forward OAuth identity for the logged-in user]({{< relref "../../create-a-grafana-plugin/extend-a-plugin/add-authentication-for-data-source-plugins.md#forward-oauth-identity-for-the-logged-in-user" >}}), [Forward cookies for the logged-in user |
||||
]({{< relref "../../create-a-grafana-plugin/extend-a-plugin/add-authentication-for-data-source-plugins.md#forward-user-header-for-the-logged-in-user" >}}) or [Forward user header for the logged-in user]({{< relref "../../create-a-grafana-plugin/extend-a-plugin/add-authentication-for-data-source-plugins.md#forward-user-header-for-the-logged-in-user" >}}) for example usages. |
||||
|
||||
### Technical details |
||||
|
||||
The Grafana SDK for Go [v0.147.0](https://github.com/grafana/grafana-plugin-sdk-go/releases/tag/v0.147.0) introduces a new interface [ForwardHTTPHeaders](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go@v0.147.0/backend#ForwardHTTPHeaders) that `QueryDataRequest`, `CheckHealthRequest` and `CallResourceRequest` implements. |
||||
|
||||
Newly introduced forwarded HTTP headers in Grafana v9.4.0 are `X-Grafana-User`, `X-Panel-Id`, `X-Dashboard-Uid`, `X-Datasource-Uid` and `X-Grafana-Org-Id`. Internally, we prefix these with `http_` and they are sent as `http_<HTTP header name>` in [CheckHealthRequest.Headers](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go@v0.147.0/backend#CheckHealthRequest) and [QueryDataRequest.Headers](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go@v0.147.0/backend#QueryDataRequest). |
||||
|
||||
We recommend using the [ForwardHTTPHeaders](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go@v0.147.0/backend#ForwardHTTPHeaders) methods so that you're guaranteed to be able to operate on HTTP headers without using the prefix. That is, you can operate on `X-Grafana-User`, `X-Panel-Id`, `X-Dashboard-Uid`, `X-Datasource-Uid` and `X-Grafana-Org-Id`. |
@ -1,40 +0,0 @@ |
||||
--- |
||||
description: Guide for migrating plugins from Grafana v9.x to v10.x |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- migration |
||||
- plugin |
||||
- documentation |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
menuTitle: v9.x to v10.x |
||||
title: Migrate plugins from Grafana version 9.x to 10.x |
||||
weight: 1900 |
||||
--- |
||||
|
||||
# Migrate plugins from Grafana version 9.x to 10.x |
||||
|
||||
## Verify plugin behavior with React 18 |
||||
|
||||
Grafana 10 includes our upgrade to React 18 and use of the new React client-side rendering API. These changes were delivered to the core `grafana` repo with [PR 64428](https://github.com/grafana/grafana/pull/64428). |
||||
|
||||
Although these updates bring many significant benefits, there's a potential for them to impact the way that your plugin works. In particular, there could be unintended side effects caused by the changes around improving consistency with `useEffect` timings and automatic batching of state updates. |
||||
|
||||
**Recommended actions:** |
||||
|
||||
- Review the React 18 [upgrade docs](https://react.dev/blog/2022/03/08/react-18-upgrade-guide). |
||||
- Test your plugins against one of the latest [grafana-dev docker images](https://hub.docker.com/r/grafana/grafana-dev/tags?page=1) (for example, [this one](https://hub.docker.com/layers/grafana/grafana-dev/10.0.0-111404pre/images/sha256-ac78acf54b44bd2ce7e68b796b1df47030da7f35e53b02bc3eec3f4de05f780f?context=explore)). |
||||
- If your plugin is affected, add a comment to the [forum discussion](https://community.grafana.com/t/grafana-10-is-upgrading-to-react-18/86051). Be sure to communicate with us so we are aware of the issue and can provide help. |
||||
|
||||
## Data frame field values are now just arrays |
||||
|
||||
In Grafana 10, the values in data frames are now managed as simple JavaScript arrays (see [PR #66480](https://github.com/grafana/grafana/issues/66480)). It is no longer necessary to wrap values in a [Vector<T>](https://github.com/grafana/grafana/blob/v9.5.x/packages/grafana-data/src/types/vector.ts) implementation. |
||||
|
||||
Most code targeting 9.x will continue to work without any issues. An exception is the rare case in which existing code directly implements [Vector<T>](https://github.com/grafana/grafana/blob/v9.5.x/packages/grafana-data/src/types/vector.ts) rather than extending or using base classes. In this case, the code should either return an array or extend [FunctionalVector<T>](https://github.com/grafana/grafana/blob/v10.0.x/packages/grafana-data/src/vector/FunctionalVector.ts#L9). All Vector implementations have been deprecated and will be removed in the future. |
||||
|
||||
When writing plugins that should run on 9.x, continue to use the Vector interfaces. In this case, when targeting versions 10+, you can now use simple arrays rather than wrapper classes. |
||||
|
||||
To make this transition seamless, we employed the Original JavaScript Sin™. That is, we [extended the native Array prototype](https://github.com/grafana/grafana/blob/v10.0.x/packages/grafana-data/src/types/vector.ts) with several Vector methods. We will atone and undo this in v11, when Vector interfaces and classes are removed. |
@ -1,25 +0,0 @@ |
||||
--- |
||||
description: An index to documentation related to plugin publishing and signing. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- publish |
||||
- publishing |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
menuTitle: Publish a plugin |
||||
title: Publish or sign a plugin |
||||
weight: 400 |
||||
--- |
||||
|
||||
# Publish or sign a plugin |
||||
|
||||
This section contains topics related to publishing and signing Grafana plugins. |
||||
|
||||
- [Package a plugin]({{< relref "./package-a-plugin.md" >}}) |
||||
- [Publishing and signing criteria]({{< relref "./publishing-and-signing-criteria.md" >}}) |
||||
- [Publish or update a plugin]({{< relref "./publish-or-update-a-plugin.md" >}}) |
||||
- [Sign a plugin]({{< relref "./sign-a-plugin.md" >}}) |
@ -1,82 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../package-a-plugin/ |
||||
description: How to package a plugin |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- links |
||||
- package |
||||
- packaging |
||||
- packages |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Package a plugin |
||||
weight: 100 |
||||
--- |
||||
|
||||
# Package a plugin |
||||
|
||||
Package a plugin to organize the plugin code and make it ready for use in your organization. Follow these steps to package the plugin in a ZIP file. |
||||
|
||||
1. Build the plugin |
||||
|
||||
``` |
||||
yarn install --pure-lockfile |
||||
yarn build |
||||
``` |
||||
|
||||
1. Optional: If your data source plugin has a backend plugin, build it as well. |
||||
|
||||
``` |
||||
mage |
||||
``` |
||||
|
||||
Make sure that all the binaries are executable and have a `0755` (`-rwxr-xr-x`) permission. |
||||
|
||||
1. Sign the plugin. To learn more, refer to [Sign a plugin]({{< relref "./sign-a-plugin" >}}). |
||||
|
||||
1. Rename the `dist` directory to match your plugin ID, and then create a ZIP archive. |
||||
|
||||
``` |
||||
mv dist/ myorg-simple-panel |
||||
zip myorg-simple-panel-1.0.0.zip myorg-simple-panel -r |
||||
``` |
||||
|
||||
1. Optional: Verify that your plugin is packaged correctly using [zipinfo](https://linux.die.net/man/1/zipinfo). |
||||
It should look like this: |
||||
|
||||
``` |
||||
zipinfo grafana-clickhouse-datasource-1.1.2.zip |
||||
|
||||
Archive: grafana-clickhouse-datasource-1.1.2.zip |
||||
Zip file size: 34324077 bytes, number of entries: 22 |
||||
drwxr-xr-x 0 bx stor 22-Mar-24 23:23 grafana-clickhouse-datasource/ |
||||
-rw-r--r-- 1654 bX defN 22-Mar-24 23:23 grafana-clickhouse-datasource/CHANGELOG.md |
||||
-rw-r--r-- 11357 bX defN 22-Mar-24 23:23 grafana-clickhouse-datasource/LICENSE |
||||
-rw-r--r-- 2468 bX defN 22-Mar-24 23:23 grafana-clickhouse-datasource/MANIFEST.txt |
||||
-rw-r--r-- 8678 bX defN 22-Mar-24 23:23 grafana-clickhouse-datasource/README.md |
||||
drwxr-xr-x 0 bx stor 22-Mar-24 23:23 grafana-clickhouse-datasource/dashboards/ |
||||
-rw-r--r-- 42973 bX defN 22-Mar-24 23:23 grafana-clickhouse-datasource/dashboards/cluster-analysis.json |
||||
-rw-r--r-- 56759 bX defN 22-Mar-24 23:23 grafana-clickhouse-datasource/dashboards/data-analysis.json |
||||
-rw-r--r-- 39406 bX defN 22-Mar-24 23:23 grafana-clickhouse-datasource/dashboards/query-analysis.json |
||||
-rwxr-xr-x 16469136 bX defN 22-Mar-24 23:23 grafana-clickhouse-datasource/gpx_clickhouse_darwin_amd64 |
||||
-rwxr-xr-x 16397666 bX defN 22-Mar-24 23:23 grafana-clickhouse-datasource/gpx_clickhouse_darwin_arm64 |
||||
-rwxr-xr-x 14942208 bX defN 22-Mar-24 23:23 grafana-clickhouse-datasource/gpx_clickhouse_linux_amd64 |
||||
-rwxr-xr-x 14155776 bX defN 22-Mar-24 23:23 grafana-clickhouse-datasource/gpx_clickhouse_linux_arm |
||||
-rwxr-xr-x 14548992 bX defN 22-Mar-24 23:23 grafana-clickhouse-datasource/gpx_clickhouse_linux_arm64 |
||||
-rwxr-xr-x 15209472 bX defN 22-Mar-24 23:23 grafana-clickhouse-datasource/gpx_clickhouse_windows_amd64.exe |
||||
drwxr-xr-x 0 bx stor 22-Mar-24 23:23 grafana-clickhouse-datasource/img/ |
||||
-rw-r--r-- 304 bX defN 22-Mar-24 23:23 grafana-clickhouse-datasource/img/logo.png |
||||
-rw-r--r-- 1587 bX defN 22-Mar-24 23:23 grafana-clickhouse-datasource/img/logo.svg |
||||
-rw-r--r-- 138400 bX defN 22-Mar-24 23:23 grafana-clickhouse-datasource/module.js |
||||
-rw-r--r-- 808 bX defN 22-Mar-24 23:23 grafana-clickhouse-datasource/module.js.LICENSE.txt |
||||
-rw-r--r-- 487395 bX defN 22-Mar-24 23:23 grafana-clickhouse-datasource/module.js.map |
||||
-rw-r--r-- 1616 bX defN 22-Mar-24 23:23 grafana-clickhouse-datasource/plugin.json |
||||
22 files, 92516655 bytes uncompressed, 34319591 bytes compressed: 62.9% |
||||
``` |
||||
|
||||
When you've packaged your plugin, you can proceed to [publishing a plugin]({{< relref "./publish-or-update-a-plugin.md" >}}) or [installing a packaged plugin](/docs/grafana/latest/administration/plugin-management/#install-a-packaged-plugin). |
@ -1,140 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../plugins/publish-a-plugin/ |
||||
- share-a-plugin/ |
||||
description: Learn how to package and share your plugin. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- publish plugin |
||||
- update plugin |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Publish or update a plugin |
||||
type: docs |
||||
weight: 300 |
||||
--- |
||||
|
||||
# Publish or update a plugin |
||||
|
||||
You've just built your plugin; now you want to share it with the world. |
||||
|
||||
In this guide, you'll learn how to package and share your plugin with others. |
||||
|
||||
The best way to share your plugin with the world is to publish it in the [Grafana plugin catalog]{{< relref "/plugins" >}}). |
||||
By having your plugin published on Grafana.com, more users will be able to discover your plugin. |
||||
|
||||
## Before you begin |
||||
|
||||
When you build a plugin from source, a `dist` folder is created. This folder contains the production build or _plugin assets_ for your plugin. |
||||
|
||||
To package a plugin, refer to [Package a plugin]({{< relref "./package-a-plugin.md" >}}). |
||||
|
||||
### Follow our guidelines |
||||
|
||||
Get familiar with our plugin [publishing and signing criteria]({{< relref "./publishing-and-signing-criteria" >}}) |
||||
|
||||
### Do this for best results |
||||
|
||||
To speed up the time it takes to review your plugin: |
||||
|
||||
- Check that your plugin is ready for review using the [plugin validator](https://github.com/grafana/plugin-validator). |
||||
- Read our [6 tips for improving your Grafana plugin before you publish](/blog/2021/01/21/6-tips-for-improving-your-grafana-plugin-before-you-publish/). |
||||
- Refer to [plugin-examples](https://github.com/grafana/grafana-plugin-examples) to review best practices for building your plugin. |
||||
|
||||
## Publish your plugin |
||||
|
||||
Follow these steps to publish your plugin for the first time. |
||||
|
||||
1. [Sign in](/auth/sign-in) to your Grafana Cloud account. |
||||
1. In the left menu, under **Org settings**, click **My Plugins**. |
||||
1. Click **Submit Plugin**. The Create Plugin Submission dialog appears. |
||||
|
||||
{{< figure src="/static/img/docs/plugins/plugins-submission-create2.png" class="docs-image--no-shadow" max-width="650px" >}} |
||||
|
||||
1. Enter the information requested by the form. |
||||
- **OS & Architecture:** |
||||
- Select **Single** if your plugin archive contains binaries for multiple architectures. |
||||
- Select **Multiple** if you'd like to submit separate plugin archives for each architecture. |
||||
This can lead to faster downloads since users can select the specific architecture on which they want to install the plugin. |
||||
- **URL:** A URL that points to a ZIP archive of your packaged plugin. |
||||
- **Source Code URL:** A URL that points to a public Git repository or ZIP archive of your complete plugin source code. |
||||
- **MD5:** The MD5 hash of the plugin specified by the **URL**. |
||||
- The remaining questions help us determine the [signature level]({{< relref "./sign-a-plugin#plugin-signature-levels" >}}) for your plugin. |
||||
1. Click **Submit**. |
||||
After you submit your plugin, we run an automated validation to make sure it adheres to our guidelines. |
||||
Once your submission passes the validation, it's placed in a review queue. |
||||
All submissions are manually inspected by a plugin reviewer. |
||||
For every new plugin, we perform a manual review that includes the following checks: |
||||
|
||||
- **Code review:** For quality and security purposes, we review the source code for the plugin. |
||||
If you're unable to make the source code publicly available, let us know in a comment on your plugin submission. |
||||
- **Tests:** We install your plugin on one of our Grafana instances to test it for basic use. |
||||
For more advanced plugins, we may ask you to assist us in configuring a test environment for the plugin. |
||||
We use the test environment whenever you submit a plugin update. |
||||
|
||||
## Update your plugin |
||||
|
||||
To submit an **update** for an already published plugin: |
||||
|
||||
1. [Sign in](/auth/sign-in) to your Grafana Cloud account. |
||||
1. In the left menu, under **Org settings**, click **My Plugins**. |
||||
1. Click **Submit Update** for the plugin you want to update. The Create Plugin Submission dialog appears. |
||||
{{< figure src="/static/img/docs/plugins/plugins-submission-create2.png" class="docs-image--no-shadow" max-width="650px" >}} |
||||
1. Enter the information requested by the form. |
||||
- **OS & Architecture:** |
||||
- Select **Single** if your plugin archive contains binaries for multiple architectures. |
||||
- Select **Multiple** if you'd like to submit separate plugin archives for each architecture. |
||||
This can lead to faster downloads since users can select the specific architecture they want to install the plugin on. |
||||
- **URL:** A URL that points to a ZIP archive of your packaged plugin. |
||||
- **Source Code URL:** A URL that points to a public Git repository or ZIP archive of your complete plugin source code. See [examples](#what-source-code-url-formats-are-supported). |
||||
- **MD5:** The MD5 hash of the plugin specified by the **URL**. |
||||
1. Click **Submit**. |
||||
|
||||
## Frequently asked questions |
||||
|
||||
### Do I need to submit a private plugin? |
||||
|
||||
- No. Please only submit plugins that you wish to make publicly available for the Grafana community. |
||||
|
||||
### How long does it take to review my submission? |
||||
|
||||
- We're not able to give an estimate at this time, though we're constantly working on improving the time it takes to review a plugin. |
||||
|
||||
### Can I decide a date when my plugin will be published? |
||||
|
||||
- No. We cannot guarantee specific publishing dates, as plugins are immediately published after a review based on our internal prioritization. |
||||
|
||||
### Can I see metrics of my plugin installs, downloads or usage? |
||||
|
||||
- No. We don't offer this information at the moment to plugin authors. |
||||
|
||||
### How can I update my plugin's catalog page? |
||||
|
||||
- The plugin's catalog page content is extracted from the plugin README file. |
||||
To update the plugin's catalog page, submit an updated plugin with the new content included in the README file. |
||||
|
||||
### Can I unlist my plugin from the Grafana plugin catalog in case of a bug? |
||||
|
||||
- In the event of a bug, unlisting the plugin from our catalog may be possible in exceptional cases, such as security concerns. However, we don't have control over the instances where the plugin is installed. |
||||
|
||||
### Can I distribute my plugin somewhere else other than the Grafana plugin catalog? |
||||
|
||||
- The official method for distributing Grafana plugins is through our catalog. Alternative methods, such as installing private or development plugins on local Grafana instances, are available as per the guidelines provided in [this guide]({{< relref "../../../administration/plugin-management#install-plugin-on-local-grafana" >}}). |
||||
|
||||
### Can I still use Angular for my plugin? |
||||
|
||||
- No. We will not accept any new plugin submissions written in Angular. For more information, refer to our [Angular support deprecation documentation]({{< relref "../../angular_deprecation" >}}). |
||||
|
||||
### Do plugin signatures expire? |
||||
|
||||
- Plugin signatures do not currently expire. |
||||
|
||||
### What source code URL formats are supported? |
||||
|
||||
- Using a tag or branch: `https://github.com/grafana/clock-panel/tree/v2.1.3` |
||||
- Using a tag or branch and the code is in a subdirectory (important for mono repos): `https://github.com/grafana/clock-panel/tree/v2.1.3/plugin/` (here, the plugin contains the plugin code) |
||||
- Using the latest main or master branch commit: `https://github.com/grafana/clock-panel/` (not recommended, it's better to pass a tag or branch) |
@ -1,38 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../../plugins/publishing-and-signing-criteria/ |
||||
description: Acceptance criteria for publishing and signing Grafana plugins. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- publish plugin |
||||
- sign plugin |
||||
- publishing |
||||
- signing |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Plugin publishing and signing criteria |
||||
weight: 200 |
||||
--- |
||||
|
||||
# Plugin publishing and signing criteria |
||||
|
||||
Grafana plugins must adhere to the Grafana Labs [Plugin Policy](/legal/plugins/). Our review process for publishing and signing will examine your compliance with this policy. |
||||
|
||||
> **Important:** Grafana Labs reserves the right to decline or remove any plugin at its discretion. Failure to comply with publishing and signing criteria may result in immediate removal from the Grafana plugin catalog. |
||||
|
||||
## Plugin licensing |
||||
|
||||
Plugins must be licensed under one of the following AGPL compliant licenses for publishing to the Grafana plugin catalog: |
||||
|
||||
- AGPL-3.0 |
||||
- Apache-2.0 |
||||
- BSD |
||||
- GPL-3.0 |
||||
- LGPL-3.0 |
||||
- MIT |
||||
|
||||
If contributing a plugin on behalf of an organization, be sure to seek guidance from your legal team. |
@ -1,149 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- ../sign-a-plugin/ |
||||
description: How to sign a Grafana plugin. |
||||
keywords: |
||||
- grafana |
||||
- plugins |
||||
- plugin |
||||
- sign plugin |
||||
- signing plugin |
||||
labels: |
||||
products: |
||||
- enterprise |
||||
- oss |
||||
title: Sign a plugin |
||||
weight: 400 |
||||
--- |
||||
|
||||
# Sign a plugin |
||||
|
||||
Grafana requires all plugins to be signed so that we can verify their authenticity with [signature verification]({{< relref "../../../administration/plugin-management#plugin-signatures" >}}). |
||||
|
||||
All Grafana Labs-authored backend plugins, including Enterprise plugins, are signed. By [default]({{< relref "../../../administration/plugin-management#allow-unsigned-plugins" >}}), Grafana **requires** all plugins to be signed in order for them to be loaded. |
||||
|
||||
Before you can sign your plugin, you need to decide whether you want to sign it as a _public_ or a _private_ plugin. |
||||
|
||||
To make your plugin publicly available outside of your organization, sign your plugin under a _community_ or _commercial_ [signature level](#plugin-signature-levels). Public plugins are available from the [Grafana plugin catalog](/plugins) and can be installed by anyone. |
||||
|
||||
If you intend to only use the plugin within your organization, sign it under a _private_ [signature level](#plugin-signature-levels). |
||||
|
||||
## Generate a token |
||||
|
||||
To verify ownership of your plugin, generate an access token that you'll use every time you need to sign a new version of your plugin. |
||||
|
||||
1. [Create a Grafana Cloud account](/signup). |
||||
|
||||
1. Login into your account and navigate to **My Account > Security > Access Policies**. Click **Create access policy**. |
||||
|
||||
Realm: has to be your-org-name (all-stacks) |
||||
Scope: plugins:write |
||||
|
||||
{{< figure src="/media/docs/plugins/create-access-policy-v2.png" class="docs-image--no-shadow" max-width="650px" >}} |
||||
|
||||
1. Click **Create token** to create a new token. |
||||
|
||||
The expiration date field is optional, though you should change tokens periodically for increased security. |
||||
|
||||
{{< figure src="/media/docs/plugins/create-access-policy-token.png" class="docs-image--no-shadow" max-width="650px" >}} |
||||
|
||||
1. Click **Create** and save a copy of the token somewhere secure for future reference. |
||||
|
||||
## Sign a public plugin |
||||
|
||||
Public plugins need to be reviewed by the Grafana team before you can sign them. |
||||
|
||||
1. Submit your plugin for [review]({{< relref "./publish-or-update-a-plugin.md#publish-your-plugin" >}}). |
||||
1. If we approve your plugin, you're granted a plugin signature level. You need this signature level to proceed. |
||||
1. In your plugin directory, sign the plugin with the API key you just created. Grafana Sign Plugin creates a [MANIFEST.txt](#plugin-manifest) file in the `dist` directory of your plugin: |
||||
|
||||
```bash |
||||
export GRAFANA_ACCESS_POLICY_TOKEN=<YOUR_ACCESS_POLICY_TOKEN> |
||||
npx @grafana/sign-plugin@latest |
||||
``` |
||||
|
||||
## Sign a private plugin |
||||
|
||||
1. In your plugin directory, sign the plugin with the API key you just created. Grafana Sign Plugin creates a [MANIFEST.txt](#plugin-manifest) file in the `dist` directory of your plugin. |
||||
|
||||
```bash |
||||
export GRAFANA_ACCESS_POLICY_TOKEN=<YOUR_ACCESS_POLICY_TOKEN> |
||||
npx @grafana/sign-plugin@latest --rootUrls https://example.com/grafana |
||||
``` |
||||
|
||||
1. After the `rootUrls` flag, enter a comma-separated list of URLs for the Grafana instances where you intend to install the plugin. |
||||
|
||||
## Plugin signature levels |
||||
|
||||
To sign a plugin, you need to select the _signature level_ that you want to sign it under. The signature level of your plugin determines how you can distribute it. |
||||
|
||||
You can sign your plugin under three different _signature levels_: _private_, _community_, and _commercial_. |
||||
|
||||
| **Signature Level** | **Paid Subscription Required?** | **Description** | |
||||
| ------------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
||||
| Private | No;<br>Free of charge | Private plugins are for use on your own Grafana instance. They may not be shared to the Grafana community or to your customers, and are not published in the Grafana catalog.<br>Private plugins are not Supported in Grafana Cloud. | |
||||
| Community | No;<br>Free of charge | Community plugins contain dependent technologies that are open source and/or not for profit.<br>Community plugins are published to the official Grafana catalog, and are available to the Grafana community for direct installation.<br>Support is provided by the individual developer and/or community.<br>Supported in Grafana Cloud.<br>Not commercial in nature and not affiliated with any commercial endeavor. | |
||||
| Commercial | Yes;<br>Commercial Plugin Subscription required | Commercial plugins contain dependent technologies that are closed source or commercially backed (even if open source at their core). These plugins meet the commercial plugin criteria and are partner-developed.<br>Commercial plugins are published to the official Grafana catalog, and are available to the Grafana community for direct installation.<br>Support is provided by the Partner.<br>Supported in Grafana Cloud. | |
||||
|
||||
For instructions on how to sign a plugin under the Community and Commercial signature level, refer to [Sign a public plugin](#sign-a-public-plugin). |
||||
|
||||
For instructions on how to sign a plugin under the Private signature level, refer to [Sign a private plugin](#sign-a-private-plugin). |
||||
|
||||
## Plugin manifest |
||||
|
||||
For Grafana to verify the digital signature of a plugin, the plugin must include a signed manifest file, `MANIFEST.txt`. The signed manifest file contains two sections: |
||||
|
||||
- **Signed message -** Contains plugin metadata and plugin files with their respective checksums (SHA256). |
||||
- **Digital signature -** Created by encrypting the signed message using a private key. Grafana has a public key built-in that can be used to verify that the digital signature has been encrypted using the expected private key. |
||||
|
||||
**Example** |
||||
|
||||
```txt |
||||
-----BEGIN PGP SIGNED MESSAGE----- |
||||
Hash: SHA512 |
||||
|
||||
{ |
||||
"manifestVersion": "2.0.0", |
||||
"signatureType": "community", |
||||
"signedByOrg": "myorgid", |
||||
"signedByOrgName": "My Org", |
||||
"plugin": "myorgid-simple-panel", |
||||
"version": "1.0.0", |
||||
"time": 1602753404133, |
||||
"keyId": "7e4d0c6a708866e7", |
||||
"files": { |
||||
"LICENSE": "12ab7a0961275f5ce7a428e662279cf49bab887d12b2ff7bfde738346178c28c", |
||||
"module.js.LICENSE.txt": "0d8f66cd4afb566cb5b7e1540c68f43b939d3eba12ace290f18abc4f4cb53ed0", |
||||
"module.js.map": "8a4ede5b5847dec1c6c30008d07bef8a049408d2b1e862841e30357f82e0fa19", |
||||
"plugin.json": "13be5f2fd55bee787c5413b5ba6a1fae2dfe8d2df6c867dadc4657b98f821f90", |
||||
"README.md": "2d90145b28f22348d4f50a81695e888c68ebd4f8baec731fdf2d79c8b187a27f", |
||||
"module.js": "b4b6945bbf3332b08e5e1cb214a5b85c82557b292577eb58c8eb1703bc8e4577" |
||||
} |
||||
} |
||||
-----BEGIN PGP SIGNATURE----- |
||||
Version: OpenPGP.js v4.10.1 |
||||
Comment: https://openpgpjs.org |
||||
|
||||
wqEEARMKAAYFAl+IE3wACgkQfk0ManCIZudpdwIHTCqjVzfm7DechTa7BTbd |
||||
+dNIQtwh8Tv2Q9HksgN6c6M9nbQTP0xNHwxSxHOI8EL3euz/OagzWoiIWulG |
||||
7AQo7FYCCQGucaLPPK3tsWaeFqVKy+JtQhrJJui23DAZLSYQYZlKQ+nFqc9x |
||||
T6scfmuhWC/TOcm83EVoCzIV3R5dOTKHqkjIUg== |
||||
=GdNq |
||||
-----END PGP SIGNATURE----- |
||||
``` |
||||
|
||||
## Troubleshooting |
||||
|
||||
### Why do I get a "Modified signature" error? |
||||
|
||||
In some cases an invalid `MANIFEST.txt` is generated because of an issue when signing the plugin on Windows. You can fix this by replacing all double backslashes, `\\`, with a forward slash, `/`, in the `MANIFEST.txt` file. You need to do this every time you sign your plugin. |
||||
|
||||
### Why do I get a "Field is required: `rootUrls`" error for my public plugin? |
||||
|
||||
With a **public** plugin, your plugin doesn't have a plugin signature level assigned to it yet. A Grafana team member will assign a signature level to your plugin once it has been reviewed and approved. For more information, refer to [Sign a public plugin](#sign-a-public-plugin). |
||||
|
||||
### Why do I get a "Field is required: `rootUrls`" error for my private plugin? |
||||
|
||||
With a **private** plugin, you need to add a `rootUrls` flag to the `plugin:sign` command. The `rootUrls` must match the [root_url]({{< relref "../../../setup-grafana/configure-grafana#root_url" >}}) configuration. For more information, refer to [Sign a private plugin](#sign-a-private-plugin). |
||||
|
||||
If you still get this error, make sure that the API key was generated by a Grafana Cloud account that matches the first part of the plugin ID. |
Loading…
Reference in new issue