From 5f8096ff0e38256ca830d68cb7b59e7d0ddab3fb Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 10 Sep 2024 15:27:18 +0200 Subject: [PATCH] Docs: Add/update resource handler topics for datasource and app plugins (#1081) --- .../app-plugins/add-resource-handler.md | 59 +++++++ .../add-resource-handler.md | 144 ++++-------------- .../docs/shared/implement-resource-handler.md | 112 ++++++++++++++ 3 files changed, 199 insertions(+), 116 deletions(-) create mode 100644 docusaurus/docs/how-to-guides/app-plugins/add-resource-handler.md create mode 100644 docusaurus/docs/shared/implement-resource-handler.md diff --git a/docusaurus/docs/how-to-guides/app-plugins/add-resource-handler.md b/docusaurus/docs/how-to-guides/app-plugins/add-resource-handler.md new file mode 100644 index 000000000..4eeb9a81d --- /dev/null +++ b/docusaurus/docs/how-to-guides/app-plugins/add-resource-handler.md @@ -0,0 +1,59 @@ +--- +id: add-resource-handler +title: Add resource handler for app plugins +description: Learn how to add a resource handler for app plugins. +keywords: + - grafana + - plugins + - plugin + - app + - resource + - resource handler +--- + +import ImplementResourceHandler from '@shared/implement-resource-handler.md'; + +# Add resource handler for app plugins + +You can add a resource handler to your app backend to extend the Grafana HTTP API with your own app-specific routes. This guide explains why you may want to add [resource](../../key-concepts/backend-plugins/#resources) handlers and some common ways for doing so. + +## Uses of resource handlers + +An app often integrates with a HTTP service of some kind, e.g. a 3rd party service, to retrieve and send data. For example, this service might have specific authentication and authorization needs or a response format not suitable to return to Grafana and the plugin frontend. + +In addition, you might want to [secure your resources](implement-rbac-in-app-plugins.md#secure-backend-resources) so that only users with a certain permission can access certain routes. + +Resource handlers are also useful for building control panels that allow the user to write back to the app. For example, you could add a resource handler to update the state of an IoT device. + + + +## Accessing app resources + +Once implemented you can access the resources using the Grafana HTTP API and from the frontend. + +### Using the Grafana HTTP API + +You can access the resources through the Grafana HTTP API by using the endpoint, `http://:/api/plugins//resources{/}`. The `PLUGIN_ID` is the plugin identifier that uniquely identifies your app and the `RESOURCE` depends on how the resource handler is implemented and what resources (routes) are supported. + +With the above example you can access the following resources: + +- HTTP GET `http://:/api/plugins//resources/namespaces` +- HTTP GET `http://:/api/plugins//resources/projects` + +### From the frontend + +You can access your resources in a compontent using the `get` function of the `backendSrv` runtime service to send a HTTP GET request to `http://:/api/plugins//resources/namespaces` + +```typescript +import { getBackendSrv } from '@grafana/runtime'; + +const namespaces = await getBackendSrv().get(`/api/plugins//resources/namespaces`); +``` + +## 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 [app-with-backend](https://github.com/grafana/grafana-plugin-examples/tree/main/examples/app-with-backend) example: + - [create resource handler](https://github.com/grafana/grafana-plugin-examples/blob/main/examples/app-with-backend/pkg/plugin/app.go) and [register routes](https://github.com/grafana/grafana-plugin-examples/blob/main/examples/app-with-backend/pkg/plugin/resources.go) in the backend. + - use [backendSrv](https://github.com/grafana/grafana-plugin-examples/blob/main/examples/app-with-backend/src/pages/PageOne/PageOne.tsx) to call resources. diff --git a/docusaurus/docs/how-to-guides/data-source-plugins/add-resource-handler.md b/docusaurus/docs/how-to-guides/data-source-plugins/add-resource-handler.md index b31cfdadb..299fa2fd6 100644 --- a/docusaurus/docs/how-to-guides/data-source-plugins/add-resource-handler.md +++ b/docusaurus/docs/how-to-guides/data-source-plugins/add-resource-handler.md @@ -12,6 +12,8 @@ keywords: - resource handler --- +import ImplementResourceHandler from '@shared/implement-resource-handler.md'; + # 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](../../key-concepts/backend-plugins/#resources) handlers and some common ways for doing so. @@ -22,22 +24,20 @@ The primary way for a data source to retrieve data from a backend is through the 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. +## Accessing datasource resources -```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!"), - }) -} -``` +Once implemented you can access the resources using the Grafana HTTP API and from the frontend. + +### Using the Grafana HTTP API -You can then access your resources through the following endpoint: `http://:/api/datasources/uid//resources` +You can access the resources through the Grafana HTTP API by using the endpoint, `http://:/api/datasources/uid//resources{/}`. The `DATASOURCE_UID` is the data source unique identifier (UID) that uniquely identifies your data source and the `RESOURCE` depends on how the resource handler is implemented and what resources (routes) are supported. -In this example code, `DATASOURCE_UID` is the data source unique identifier (UID) that uniquely identifies your data source. +With the above example you can access the following resources: + +- HTTP GET `http://:/api/datasources/uid//resources/namespaces` +- HTTP GET `http://:/api/datasources/uid//resources/projects` :::tip @@ -45,118 +45,30 @@ To verify the data source UID, you can enter `window.grafanaBootData.settings.da ::: -## 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](../../key-concepts/backend-plugins/grafana-plugin-sdk-for-go) 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: +### From the frontend -```go -package mydatasource +You can query your resources using the `getResource` and `postResource` helpers from the `DataSourceWithBackend` class. To provide a nicer and more convenient API for your components it's recommended to extend your datasource class and instance with functions for each route as shown in the following example: -import ( - "context" - "net/http" +```typescript +export class MyDataSource extends DataSourceWithBackend { + constructor(instanceSettings: DataSourceInstanceSettings) { + super(instanceSettings); + } - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter" -) + getNamespaces(): Promise { + return this.getResource('/namespaces'); + } -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) + getProjects(): Promise { + return this.getResource('/projects'); + } } ``` -:::note +For example, in your query editor component, you can access the data source instance from the `props` object and use `getNamespaces` to send a HTTP GET request to `http://:/api/datasources/uid//resources/namespaces`: -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`: - -```go -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) -} +```typescript +const namespaces = await props.datasource.getNamespaces(); ``` ## Additional examples diff --git a/docusaurus/docs/shared/implement-resource-handler.md b/docusaurus/docs/shared/implement-resource-handler.md new file mode 100644 index 000000000..2dec03570 --- /dev/null +++ b/docusaurus/docs/shared/implement-resource-handler.md @@ -0,0 +1,112 @@ +## Implement the resource handler interface + +To add a resource handler to your backend plugin, you need to implement the `backend.CallResourceHandler` interface. + +There are two ways you can implement this in your plugin, [using the `httpadapter` package](#using-the-httpadapter-package) or [manually implementing it](#manually-implementing-backendcallresourcehandler) in your plugin. + +### Using the `httpadapter` package + +The [`httpadapter`](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter) package provided by the [Grafana Plugin SDK for Go](../../key-concepts/backend-plugins/grafana-plugin-sdk-for-go) is the recommended way for handling resources. This package provides support for handling resource calls using using the [`http.Handler`](https://pkg.go.dev/net/http#Handler) interface and allows responding to HTTP requests in a more Go-agnostic way and makes it easier to support multiple routes and methods (GET, POST etc). + +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)). + +:::note + +Go 1.22 [includes routing enhancement](https://go.dev/blog/routing-enhancements) that adds support for method matching and wildcards using the [`ServeMux`](https://pkg.go.dev/net/http#ServeMux). + +::: + +In the following example we demonstrate using the `httpadapter` package, `ServeMux` and `http.Handler` to add support for retrieving namespaces (`/namespaces`), projects (`/projects`) and updating the state of some device (`/device`) : + +```go +package myplugin + +import ( + "context" + "net/http" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter" +) + +type MyPlugin struct { + resourceHandler backend.CallResourceHandler +} + +func New() *MyPlugin { + p := &MyPlugin{} + mux := http.NewServeMux() + mux.HandleFunc("/namespaces", p.handleNamespaces) + mux.HandleFunc("/projects", p.handleProjects) + p.resourceHandler := httpadapter.New(mux) + return p +} + +func (p *MyPlugin) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + return p.resourceHandler.CallResource(ctx, req) +} + +func (p *MyPlugin) handleNamespaces(rw http.ResponseWriter, req *http.Request) { + rw.Header().Add("Content-Type", "application/json") + _, err := rw.Write([]byte(`{ "namespaces": ["ns-1", "ns-2"] }`)) + if err != nil { + return + } + rw.WriteHeader(http.StatusOK) +} + +func (p *MyPlugin) handleProjects(rw http.ResponseWriter, req *http.Request) { + rw.Header().Add("Content-Type", "application/json") + _, err := rw.Write([]byte(`{ "projects": ["project-1", "project-2"] }`)) + if err != nil { + return + } + rw.WriteHeader(http.StatusOK) +} +``` + +#### Accessing the backend plugin context + +You can use the [backend.PluginConfigFromContext](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend#PluginConfigFromContext) function to access [backend.PluginContext](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend#PluginContext). This holds contextual information about a plugin request, such as the user performing the request: + +```go +func (p *MyPlugin) handleSomeRoute(rw http.ResponseWriter, req *http.Request) { + pCtx := backend.PluginConfigFromContext(req.Context()) + bytes, err := json.Marshal(pCtx.User) + if err != nil { + return + } + + rw.Header().Add("Content-Type", "application/json") + _, err := rw.Write(bytes) + if err != nil { + return + } + rw.WriteHeader(http.StatusOK) +} +``` + +### Manually implementing `backend.CallResourceHandler` + +Manually implementing the `backend.CallResourceHandler` interface might be enough for the basic needs. To support a couple of different routes retrieving data you can use a switch with the `req.Path`: + +```go +func (p *MyPlugin) 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, + }) + } +} +```