Skip to content

Commit

Permalink
Merge branch 'main' into andreas/update-plugin-e2e
Browse files Browse the repository at this point in the history
  • Loading branch information
sunker committed Jun 11, 2024
2 parents 662719f + fff7884 commit feafd59
Show file tree
Hide file tree
Showing 40 changed files with 4,182 additions and 1,924 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ jobs:
id: resolve-versions
uses: grafana/plugin-actions/e2e-version@main
with:
version-resolver-type: version-support-policy
version-resolver-type: plugin-grafana-dependency
grafana-dependency: '>=8.5.0'

playwright-tests:
needs: resolve-versions
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ playwright-report/
packages/plugin-e2e/test-results/
packages/plugin-e2e/playwright/.cache/
packages/plugin-e2e/playwright/.auth
.nx/cache
.nx

# Used in CI to pass built packages to the next job
packed-artifacts/
85 changes: 85 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,88 @@
# (Fri Jun 07 2024)

#### 🚀 Enhancement

- `@grafana/[email protected]`
- Plugin E2E: Add support for testing alert query [#779](https://github.com/grafana/plugin-tools/pull/779) ([@sunker](https://github.com/sunker))

#### Authors: 1

- Erik Sundell ([@sunker](https://github.com/sunker))

---

# (Thu Jun 06 2024)

#### 🐛 Bug Fix

- `@grafana/[email protected]`
- Create Plugin: Remove meta-extractor from package.json [#946](https://github.com/grafana/plugin-tools/pull/946) ([@jackw](https://github.com/jackw))
- Chore(deps): Bump marked-terminal from 6.2.0 to 7.0.0 [#793](https://github.com/grafana/plugin-tools/pull/793) ([@dependabot[bot]](https://github.com/dependabot[bot]))
- Chore(deps): Bump which from 3.0.1 to 4.0.0 [#794](https://github.com/grafana/plugin-tools/pull/794) ([@dependabot[bot]](https://github.com/dependabot[bot]))

#### Authors: 2

- [@dependabot[bot]](https://github.com/dependabot[bot])
- Jack Westbrook ([@jackw](https://github.com/jackw))

---

# (Wed Jun 05 2024)

#### 🐛 Bug Fix

- `@grafana/[email protected]`
- Plugin E2E: Fix menuitemclick in Grafana <=9.1.0 [#944](https://github.com/grafana/plugin-tools/pull/944) ([@sunker](https://github.com/sunker))

#### Authors: 1

- Erik Sundell ([@sunker](https://github.com/sunker))

---

# (Tue Jun 04 2024)

:tada: This release contains work from a new contributor! :tada:

Thank you, Giuseppe Marazzi ([@beppemarazzi](https://github.com/beppemarazzi)), for all your work!

#### 🐛 Bug Fix

- `@grafana/[email protected]`
- Create Plugin: Fix bad baseUrl path in webpack configuration [#932](https://github.com/grafana/plugin-tools/pull/932) ([@beppemarazzi](https://github.com/beppemarazzi))

#### Authors: 1

- Giuseppe Marazzi ([@beppemarazzi](https://github.com/beppemarazzi))

---

# (Tue Jun 04 2024)

#### 🐛 Bug Fix

- `@grafana/[email protected]`, `@grafana/[email protected]`
- Plugin E2E: Fix - skip creating user if it's the default server admin [#941](https://github.com/grafana/plugin-tools/pull/941) ([@sunker](https://github.com/sunker))

#### Authors: 1

- Erik Sundell ([@sunker](https://github.com/sunker))

---

# (Thu May 30 2024)

#### 🚀 Enhancement

- `@grafana/[email protected]`
- Plugin E2E: Allow overriding grafanaAPICredentials [#930](https://github.com/grafana/plugin-tools/pull/930) ([@sunker](https://github.com/sunker))

#### Authors: 1

- Erik Sundell ([@sunker](https://github.com/sunker))

---

# (Tue May 28 2024)

:tada: This release contains work from a new contributor! :tada:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
---
id: add-resource-handler
title: Add resource handler for data source plugins
description: Learn how to add a resource handler for data source plugins.
keywords:
- grafana
- plugins
- plugin
- data source
- datasource
- resource
- resource handler
---

# Add resource handler for data source plugins

You can add a resource handler to your data source backend to extend the Grafana HTTP API with your own data source-specific routes. This guide explains why you may want to add [resource](/introduction/backend-plugins#resources) handlers and some common ways for doing so.

## Uses of resource handlers

The primary way for a data source to retrieve data from a backend is through the [query method](./add-query-editor-help.md). But sometimes your data source needs to request data on demand; for example, to offer auto-completion automatically inside the data source’s query editor.

Resource handlers are also useful for building control panels that allow the user to write back to the data source. For example, you could add a resource handler to update the state of an IoT device.

## Implement the resource handler interface

To add a resource handler to your backend plugin, you need to implement the `backend.CallResourceHandler` interface for your data source struct.

```go
func (d *MyDatasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
return sender.Send(&backend.CallResourceResponse{
Status: http.StatusOK,
Body: []byte("Hello, world!"),
})
}
```

You can then access your resources through the following endpoint: `http://<GRAFANA_HOSTNAME>:<PORT>/api/datasources/uid/<DATASOURCE_UID>/resources`

In this example code, `DATASOURCE_UID` is the data source unique identifier (UID) that uniquely identifies your data source.

:::tip

To verify the data source UID, you can enter `window.grafanaBootData.settings.datasources` in your browser's developer tools console, to list all the configured data sources in your Grafana instance.

:::

## Add support for multiple routes

To support multiple routes in your data source plugin, you can use a switch with the `req.Path`:

```go
func (d *MyDatasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
switch req.Path {
case "namespaces":
return sender.Send(&backend.CallResourceResponse{
Status: http.StatusOK,
Body: []byte(`{ namespaces: ["ns-1", "ns-2"] }`),
})
case "projects":
return sender.Send(&backend.CallResourceResponse{
Status: http.StatusOK,
Body: []byte(`{ projects: ["project-1", "project-2"] }`),
})
default:
return sender.Send(&backend.CallResourceResponse{
Status: http.StatusNotFound,
})
}
}
```

You can also query your resources using the `getResource` and `postResource` helpers from the `DataSourceWithBackend` class.

For example, in your query editor component, you can access the data source instance from the `props` object:

```
const namespaces = await props.datasource.getResource('namespaces');
props.datasource.postResource('device', { state: "on" });
```

## Advanced use cases

If you have some more advanced use cases or want to use a more Go-agnostic approach for handling resources, you can use the regular [`http.Handler`](https://pkg.go.dev/net/http#Handler). You can do so by using a package provided by the [Grafana Plugin SDK for Go](../../introduction/grafana-plugin-sdk-for-go.md) named [`httpadapter`](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter). This package provides support for handling resource calls using an [`http.Handler`](https://pkg.go.dev/net/http#Handler).

Using [`http.Handler`](https://pkg.go.dev/net/http#Handler) allows you to also use Go’s built-in router functionality called [`ServeMux`](https://pkg.go.dev/net/http#ServeMux) or your preferred HTTP router library (for example, [`gorilla/mux`](https://github.com/gorilla/mux)).

An alternative to using the `CallResource` method shown in the [above example](#implement-the-resource-handler-interface) is to use [`httpadapter`](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter) and [`ServeMux`](https://pkg.go.dev/net/http#ServeMux) as shown below:

```go
package mydatasource

import (
"context"
"net/http"

"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
)

type MyDatasource struct {
resourceHandler backend.CallResourceHandler
}

func New() *MyDatasource {
ds := &MyDatasource{}
mux := http.NewServeMux()
mux.HandleFunc("/namespaces", ds.handleNamespaces)
mux.HandleFunc("/projects", ds.handleProjects)
ds.resourceHandler := httpadapter.New(mux)
return ds
}

func (d *MyDatasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
return d.resourceHandler.CallResource(ctx, req)
}

func (d *MyDatasource) handleNamespaces(rw http.ResponseWriter, req *http.Request) {
_, err := rw.Write([]byte(`{ namespaces: ["ns-1", "ns-2"] }`))
if err != nil {
return
}
rw.WriteHeader(http.StatusOK)
}

func (d *MyDatasource) handleProjects(rw http.ResponseWriter, req *http.Request) {
_, err := rw.Write([]byte(`{ projects: ["project-1", "project-2"] }`))
if err != nil {
return
}
rw.WriteHeader(http.StatusOK)
}
```

:::note

Using some other HTTP router library with above example should be straightforward. Just replace the use of [`ServeMux`](https://pkg.go.dev/net/http#ServeMux) with another router.

:::

### What if you need access to the backend plugin context?

Use the `PluginConfigFromContext` function to access `backend.PluginContext`:

```
func (d *MyDatasource) handleNamespaces(rw http.ResponseWriter, req *http.Request) {
pCtx := httpadapter.PluginConfigFromContext(req.Context())
bytes, err := json.Marshal(pCtx.User)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
}
_, err := rw.Write(bytes)
if err != nil {
return
}
rw.WriteHeader(http.StatusOK)
}
```

## Additional examples

Some other examples of using resource handlers and the [`httpadapter`](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter) package:

- The [datasource-basic](https://github.com/grafana/grafana-plugin-examples/tree/main/examples/datasource-basic) example:
- [create resource handler](https://github.com/grafana/grafana-plugin-examples/blob/309228fffb09c092c08dbd3d17f45a656b2ec3c6/examples/datasource-basic/pkg/plugin/datasource.go#L39) and [register routes](https://github.com/grafana/grafana-plugin-examples/blob/main/examples/datasource-basic/pkg/plugin/resource_handler.go) in the backend.
- [fetch](https://github.com/grafana/grafana-plugin-examples/blob/309228fffb09c092c08dbd3d17f45a656b2ec3c6/examples/datasource-basic/src/components/QueryEditor/QueryEditor.tsx#L15) and [populate query types in a drop-down](https://github.com/grafana/grafana-plugin-examples/blob/309228fffb09c092c08dbd3d17f45a656b2ec3c6/examples/datasource-basic/src/components/QueryEditor/QueryEditor.tsx#L42) in the query editor component in the frontend. Fetching is done in a [separate function](https://github.com/grafana/grafana-plugin-examples/blob/309228fffb09c092c08dbd3d17f45a656b2ec3c6/examples/datasource-basic/src/components/QueryEditor/useQueryTypes.tsx#L13) which calls the [getAvailableQueryTypes function of the datasource](https://github.com/grafana/grafana-plugin-examples/blob/309228fffb09c092c08dbd3d17f45a656b2ec3c6/examples/datasource-basic/src/datasource.ts#L21-L23).
- Grafana's built-in TestData datasource, [create resource handler](https://github.com/grafana/grafana/blob/5687243d0b3bad06c4da809f925cfdf3d32c5a16/pkg/tsdb/grafana-testdata-datasource/testdata.go#L45) and [register routes](https://github.com/grafana/grafana/blob/5687243d0b3bad06c4da809f925cfdf3d32c5a16/pkg/tsdb/grafana-testdata-datasource/resource_handler.go#L17-L28).
27 changes: 26 additions & 1 deletion docusaurus/docs/e2e-test-a-plugin/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ To be able to interact with the Grafana UI, you need to be logged in to Grafana.

If your plugin doesn't use RBAC, you can use the default server administrator credentials to login.

In the following example, there's a [setup project](https://playwright.dev/docs/test-global-setup-teardown#setup-example) called `auth`. This project invokes a function in the `@grafana/plugin-e2e` package that logins to Grafana using `admin:admin`. The authenticated state is stored on disk with this file name pattern: `<plugin-root>/playwright/.auth/<username>.json`.
In the following example, there's a [setup project](https://playwright.dev/docs/test-global-setup-teardown#setup-example) called `auth`. This project invokes a function in the `@grafana/plugin-e2e` package that logs in to Grafana using `admin:admin`. The authenticated state is stored on disk with this file name pattern: `<plugin-root>/playwright/.auth/<username>.json`.

The second project, `run-tests`, runs all tests in the `./tests` directory. This project reuses the authentication state from the `auth` project. As a consequence, login only happens once, and all tests in the `run-tests` project start already authenticated.

Expand Down Expand Up @@ -94,3 +94,28 @@ export default defineConfig<PluginOptions>({
]
})
```

## Managing users

When a `user` is defined in a setup project (like in the RBAC example above) `plugin-e2e` will use the Grafana HTTP API to create the user account. This action requires elevated permissions, so by default the server administrator credentials `admin:admin` will be used. If the end-to-end tests are targeting the [development environment](../get-started/set-up-development-environment.mdx) scaffolded with `create-plugin`, this will work fine. However for other test environments the server administrator password may be different. In that case, you can provide the correct credentials by setting `grafanaAPICredentials` in the global options.

```ts title="playwright.config.ts"
import { dirname } from 'path';
import { defineConfig, devices } from '@playwright/test';

const pluginE2eAuth = `${dirname(require.resolve('@grafana/plugin-e2e'))}/auth`;

export default defineConfig<PluginOptions>({
testDir: './tests',
use: {
baseURL: 'http://localhost:3000',
grafanaAPICredentials: {
user: 'admin',
password: process.env.PASSWORD,
},
},
projects: [
...
]
})
```
15 changes: 15 additions & 0 deletions docusaurus/docs/e2e-test-a-plugin/setup-resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,18 @@ const variableEditPage = new VariableEditPage(
);
await variableEditPage.goto();
```

### readProvisionedAlertRule fixture

The `readProvisionedAlertRule` fixture allows you to read a file from your plugin's `provisioning/alerting` folder.

```ts title="alerting.spec.ts"
test('should evaluate to true when loading a provisioned query that is valid', async ({
gotoAlertRuleEditPage,
readProvisionedAlertRule,
}) => {
const alertRule = await readProvisionedAlertRule({ fileName: 'alerts.yml' });
const alertRuleEditPage = await gotoAlertRuleEditPage(alertRule);
await expect(alertRuleEditPage.evaluate()).toBeOK();
});
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
id: alert-queries
title: Test alert queries
description: Test alert queries to ensure the plugin is compatible with alerting
keywords:
- grafana
- plugins
- plugin
- testing
- e2e
- data-source
- alert queries
sidebar_position: 60
---

## Introduction

Backend data source plugins that have alerting [enabled](../../tutorials/build-a-data-source-backend-plugin.md#enable-grafana-alerting) can define alerts based on the data source queries. Before you can save an alert, the conditions for an alert definition are evalauted by the alert engine to ensure that the response from the data source is shaped correctly. If it is shaped correctly, then you can use the `alertRulePage` fixture to verify that alert rules can be created from the output of a query returned by the data source.

:::info
The APIs for end-to-end testing alert rules are only compatible with Grafana >=9.4.0.
:::

### Evaluating a new alert rule

The following example uses the `alertRulePage` fixture. With this fixture, the test starts in the page for adding a new alert rule. You then fill in the alert rule query and call the `evaluate` function. Evaluate clicks the `Preview` button which triggers a call to the `eval` endpoint to evaluate that the response of the data source query can be used to create an alert. The `toBeOK` matcher is used to verify that the evaluation was successful.

```ts
test('should evaluate to true if query is valid', async ({ page, alertRuleEditPage, selectors }) => {
const queryA = alertRuleEditPage.getAlertRuleQueryRow('A');
await queryA.datasource.set('gdev-prometheus');
await queryA.locator.getByLabel('Code').click();
await page.waitForFunction(() => window.monaco);
await queryA.getByGrafanaSelector(selectors.components.CodeEditor.container).click();
await page.keyboard.insertText('topk(5, max(scrape_duration_seconds) by (job))');
await expect(alertRuleEditPage.evaluate()).toBeOK();
});
```

### Evaluating a provisioned alert rule

You can also use a provisioned alert rule to test that your data source is compatible with alerting. For example:

```ts
test('should evaluate to true when loading a provisioned query that is valid', async ({
gotoAlertRuleEditPage,
readProvisionedAlertRule,
}) => {
const alertRule = await readProvisionedAlertRule({ fileName: 'alerts.yml' });
const alertRuleEditPage = await gotoAlertRuleEditPage(alertRule);
await expect(alertRuleEditPage.evaluate()).toBeOK();
});
```
Loading

0 comments on commit feafd59

Please sign in to comment.