From b2f7fff4df2875d27dcc38e12b74727bbd140f8b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 28 Aug 2024 20:25:09 +0200 Subject: [PATCH 1/4] Docs: Add/update resource handler topics for datasource and plugins --- .../app-plugins/add-resource-handler.md | 265 ++++++++++++++++++ .../add-resource-handler.md | 139 +++++++-- 2 files changed, 386 insertions(+), 18 deletions(-) create mode 100644 docusaurus/docs/how-to-guides/app-plugins/add-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..a030900bc --- /dev/null +++ b/docusaurus/docs/how-to-guides/app-plugins/add-resource-handler.md @@ -0,0 +1,265 @@ +--- +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 +--- + +# 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 + +The primary way for a data source to retrieve data from a backend is through the [query method](../../tutorials/build-a-data-source-plugin#define-a-query). 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 app plugin, you need to implement the `backend.CallResourceHandler` interface for your app struct. + +```go +func (a *MyApp) 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://:/api/plugins//resources` + +In this example code, `PLUGIN_ID` is the plugin identifier that uniquely identifies your app. + +## Add support for multiple routes + +To support multiple routes in your app plugin you have a couple of options depending on requirements and needs. +If you only need basic support for a couple of different routes retrieving data you can use a switch with the `req.Path`: + +```go +func (a *MyApp) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + if req.Method != http.MethodGet { + return sender.Send(&backend.CallResourceResponse{ + Status: http.StatusNotFound, + }) + } + + 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, + }) + } +} +``` + +Supporting additional routes and methods (GET, POST etc) pretty quickly gets cumbersome and creates code that might be hard to maintain, test and read. Here we recommend to use the more Go-agnostic approach for handling resources and 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)). + +:::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). + +::: + +Lets change and extend the above example by using [`httpadapter`](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter) and [`ServeMux`](https://pkg.go.dev/net/http#ServeMux) and introduce a new `/device` route for updating the state of some device: + +```go +package myapp + +import ( + "context" + "net/http" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter" +) + +type MyApp struct { + resourceHandler backend.CallResourceHandler +} + +func New() *MyApp { + app := &MyApp{} + mux := http.NewServeMux() + mux.HandleFunc("/namespaces", app.handleNamespaces) + mux.HandleFunc("/projects", app.handleProjects) + mux.HandleFunc("/device", app.updateDevice) + app.resourceHandler := httpadapter.New(mux) + return app +} + +func (a *MyApp) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + return d.resourceHandler.CallResource(ctx, req) +} + +func (a *MyApp) handleNamespaces(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + return + } + + ctxLogger := backend.Logger.FromContext(req.Context()) + + _, err := rw.Write([]byte(`{ "namespaces": ["ns-1", "ns-2"] }`)) + if err != nil { + ctxLogger.Error("Failed to write response", "error", err) + return + } + rw.WriteHeader(http.StatusOK) +} + +func (a *MyApp) handleProjects(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + return + } + + ctxLogger := backend.Logger.FromContext(req.Context()) + + _, err := rw.Write([]byte(`{ "projects": ["project-1", "project-2"] }`)) + if err != nil { + ctxLogger.Error("Failed to write response", "error", err) + return + } + rw.WriteHeader(http.StatusOK) +} + +func (a *MyApp) updateDevice(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + return + } + + if req.Body == nil { + rw.WriteHeader(http.StatusBadRequest) + return + } + + ctxLogger := backend.Logger.FromContext(req.Context()) + + defer func() { + if err := req.Body.Close(); err != nil { + ctxLogger.Warn("Failed to close response body", "error", err) + } + }() + + var payload map[string]any + b, err := io.ReadAll(req.Body) + if err != nil { + ctxLogger.Error("Failed to read request body to bytes", "error", err) + rw.WriteHeader(http.StatusInternalServerError) + return + } + + err := json.Unmarshal(b, &payload) + if err != nil { + ctxLogger.Error("Failed to unmarshal request body to JSON", "error", err) + rw.WriteHeader(http.StatusInternalServerError) + return + } + + state := payload["state"] + // update device with state... + + _, err := rw.Write([]byte(`{ "message": "device updated"] }`)) + if err != nil { + ctxLogger.Error("Failed to write response", "error", err) + 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 `backend.PluginConfigFromContext` function to access `backend.PluginContext`: + +```go +func (a *MyApp) handleNamespaces(rw http.ResponseWriter, req *http.Request) { + pCtx := backend.PluginConfigFromContext(req.Context()) + ctxLogger := backend.Logger.FromContext(req.Context()) + + bytes, err := json.Marshal(pCtx.User) + if err != nil { + ctxLogger.Error("Failed to marshal user to JSON bytes", "error", err) + rw.WriteHeader(http.StatusInternalServerError) + return + } + + _, err := rw.Write(bytes) + if err != nil { + ctxLogger.Error("Failed to write response", "error", err) + return + } + + rw.WriteHeader(http.StatusOK) +} +``` + +## Accessing app resources from the frontend + +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: + +```typescript +export class MyDataSource extends DataSourceWithBackend { + constructor(instanceSettings: DataSourceInstanceSettings) { + super(instanceSettings); + } + + getNamespaces(): Promise { + return this.getResource('/namespaces'); + } + + getProjects(): Promise { + return this.getResource('/projects'); + } + + updateDevice(state: string): Promise { + return this.postResource('device', { state: state }); + } +} +``` + +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`: + +```typescript +const namespaces = await props.datasource.getNamespaces(); +``` + +As another example, you can use `updateDevice` to send a HTTP POST request to `http://:/api/datasources/uid//resources/device` with the provided JSON payload as the second argument: + +```typescript +const result = await props.datasource.updateDevice('on'); +``` + +## 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/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). 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..e087a892d 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 @@ -47,20 +47,27 @@ 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`: +To support multiple routes in your data source plugin you have a couple of options depending on requirements and needs. +If you only need basic support for a couple of different routes retrieving data you can use a switch with the `req.Path`: ```go func (d *MyDatasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + if req.Method != http.MethodGet { + return sender.Send(&backend.CallResourceResponse{ + Status: http.StatusNotFound, + }) + } + switch req.Path { case "namespaces": return sender.Send(&backend.CallResourceResponse{ Status: http.StatusOK, - Body: []byte(`{ namespaces: ["ns-1", "ns-2"] }`), + Body: []byte(`{ "namespaces": ["ns-1", "ns-2"] }`), }) case "projects": return sender.Send(&backend.CallResourceResponse{ Status: http.StatusOK, - Body: []byte(`{ projects: ["project-1", "project-2"] }`), + Body: []byte(`{ "projects": ["project-1", "project-2"] }`), }) default: return sender.Send(&backend.CallResourceResponse{ @@ -70,22 +77,17 @@ func (d *MyDatasource) CallResource(ctx context.Context, req *backend.CallResour } ``` -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: +Supporting additional routes and methods (GET, POST etc) pretty quickly gets cumbersome and creates code that might be hard to maintain, test and read. Here we recommend to use the more Go-agnostic approach for handling resources and 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). -``` -const namespaces = await props.datasource.getResource('namespaces'); -props.datasource.postResource('device', { state: "on" }); -``` +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)). -## Advanced use cases +:::note -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). +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). -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: +Lets change and extend the above example by using [`httpadapter`](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter) and [`ServeMux`](https://pkg.go.dev/net/http#ServeMux) and introduce a new `/device` route for updating the state of some device: ```go package mydatasource @@ -107,6 +109,7 @@ func New() *MyDatasource { mux := http.NewServeMux() mux.HandleFunc("/namespaces", ds.handleNamespaces) mux.HandleFunc("/projects", ds.handleProjects) + mux.HandleFunc("/device", ds.updateDevice) ds.resourceHandler := httpadapter.New(mux) return ds } @@ -116,18 +119,77 @@ func (d *MyDatasource) CallResource(ctx context.Context, req *backend.CallResour } func (d *MyDatasource) handleNamespaces(rw http.ResponseWriter, req *http.Request) { - _, err := rw.Write([]byte(`{ namespaces: ["ns-1", "ns-2"] }`)) + if req.Method != http.MethodGet { + return + } + + ctxLogger := backend.Logger.FromContext(req.Context()) + + _, err := rw.Write([]byte(`{ "namespaces": ["ns-1", "ns-2"] }`)) if err != nil { + ctxLogger.Error("Failed to write response", "error", err) 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 req.Method != http.MethodGet { + return + } + + ctxLogger := backend.Logger.FromContext(req.Context()) + + _, err := rw.Write([]byte(`{ "projects": ["project-1", "project-2"] }`)) + if err != nil { + ctxLogger.Error("Failed to write response", "error", err) + return + } + rw.WriteHeader(http.StatusOK) +} + +func (d *MyDatasource) updateDevice(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + return + } + + if req.Body == nil { + rw.WriteHeader(http.StatusBadRequest) + return + } + + ctxLogger := backend.Logger.FromContext(req.Context()) + + defer func() { + if err := req.Body.Close(); err != nil { + ctxLogger.Warn("Failed to close response body", "error", err) + } + }() + + var payload map[string]any + b, err := io.ReadAll(req.Body) + if err != nil { + ctxLogger.Error("Failed to read request body to bytes", "error", err) + rw.WriteHeader(http.StatusInternalServerError) + return + } + + err := json.Unmarshal(b, &payload) if err != nil { + ctxLogger.Error("Failed to unmarshal request body to JSON", "error", err) + rw.WriteHeader(http.StatusInternalServerError) + return + } + + state := payload["state"] + // update device with state... + + _, err := rw.Write([]byte(`{ "message": "device updated"] }`)) + if err != nil { + ctxLogger.Error("Failed to write response", "error", err) return } + rw.WriteHeader(http.StatusOK) } ``` @@ -140,25 +202,66 @@ Using some other HTTP router library with above example should be straightforwar ### What if you need access to the backend plugin context? -Use the `PluginConfigFromContext` function to access `backend.PluginContext`: +Use the `backend.PluginConfigFromContext` function to access `backend.PluginContext`: ```go func (d *MyDatasource) handleNamespaces(rw http.ResponseWriter, req *http.Request) { - pCtx := httpadapter.PluginConfigFromContext(req.Context()) + pCtx := backend.PluginConfigFromContext(req.Context()) + ctxLogger := backend.Logger.FromContext(req.Context()) bytes, err := json.Marshal(pCtx.User) if err != nil { + ctxLogger.Error("Failed to marshal user to JSON bytes", "error", err) rw.WriteHeader(http.StatusInternalServerError) + return } _, err := rw.Write(bytes) if err != nil { + ctxLogger.Error("Failed to write response", "error", err) return } + rw.WriteHeader(http.StatusOK) } ``` +## Accessing datasource resources from the frontend + +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: + +```typescript +export class MyDataSource extends DataSourceWithBackend { + constructor(instanceSettings: DataSourceInstanceSettings) { + super(instanceSettings); + } + + getNamespaces(): Promise { + return this.getResource('/namespaces'); + } + + getProjects(): Promise { + return this.getResource('/projects'); + } + + updateDevice(state: string): Promise { + return this.postResource('device', { state: state }); + } +} +``` + +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`: + +```typescript +const namespaces = await props.datasource.getNamespaces(); +``` + +As another example, you can use `updateDevice` to send a HTTP POST request to `http://:/api/datasources/uid//resources/device` with the provided JSON payload as the second argument: + +```typescript +const result = await props.datasource.updateDevice('on'); +``` + ## 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: From 9288065c2c63004e9e96a32936f8e3cda701bca4 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 9 Sep 2024 21:00:54 +0200 Subject: [PATCH 2/4] create shared page and restructure content --- .../app-plugins/add-resource-handler.md | 234 +++--------------- .../add-resource-handler.md | 208 ++-------------- .../docs/shared/implement-resource-handler.md | 189 ++++++++++++++ 3 files changed, 231 insertions(+), 400 deletions(-) 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 index a030900bc..e0f9ecd7d 100644 --- a/docusaurus/docs/how-to-guides/app-plugins/add-resource-handler.md +++ b/docusaurus/docs/how-to-guides/app-plugins/add-resource-handler.md @@ -11,6 +11,8 @@ keywords: - 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. @@ -21,238 +23,59 @@ 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 app plugin, you need to implement the `backend.CallResourceHandler` interface for your app struct. - -```go -func (a *MyApp) 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://:/api/plugins//resources` - -In this example code, `PLUGIN_ID` is the plugin identifier that uniquely identifies your app. - -## Add support for multiple routes - -To support multiple routes in your app plugin you have a couple of options depending on requirements and needs. -If you only need basic support for a couple of different routes retrieving data you can use a switch with the `req.Path`: - -```go -func (a *MyApp) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - if req.Method != http.MethodGet { - return sender.Send(&backend.CallResourceResponse{ - Status: http.StatusNotFound, - }) - } - - 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, - }) - } -} -``` - -Supporting additional routes and methods (GET, POST etc) pretty quickly gets cumbersome and creates code that might be hard to maintain, test and read. Here we recommend to use the more Go-agnostic approach for handling resources and 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)). +## Accessing app resources -:::note +Once implemented you can access the resources using the Grafana HTTP API and from the frontend. -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). +### 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. -Lets change and extend the above example by using [`httpadapter`](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter) and [`ServeMux`](https://pkg.go.dev/net/http#ServeMux) and introduce a new `/device` route for updating the state of some device: +With the above example you can access the following resources: -```go -package myapp +- HTTP GET `http://:/api/plugins//resources/namespaces` +- HTTP GET `http://:/api/plugins//resources/projects` +- HTTP POST `http://:/api/plugins//resources/device` -import ( - "context" - "net/http" +### From the frontend - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter" -) +You can access your resources using the `get` and `post` functions from the `backendSrv` runtime service. To provide a nicer and more convenient API for your components it's recommended to provide a helper class with functions for each route as shown in the following example: -type MyApp struct { - resourceHandler backend.CallResourceHandler -} - -func New() *MyApp { - app := &MyApp{} - mux := http.NewServeMux() - mux.HandleFunc("/namespaces", app.handleNamespaces) - mux.HandleFunc("/projects", app.handleProjects) - mux.HandleFunc("/device", app.updateDevice) - app.resourceHandler := httpadapter.New(mux) - return app -} - -func (a *MyApp) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - return d.resourceHandler.CallResource(ctx, req) -} - -func (a *MyApp) handleNamespaces(rw http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { - return - } - - ctxLogger := backend.Logger.FromContext(req.Context()) - - _, err := rw.Write([]byte(`{ "namespaces": ["ns-1", "ns-2"] }`)) - if err != nil { - ctxLogger.Error("Failed to write response", "error", err) - return - } - rw.WriteHeader(http.StatusOK) -} - -func (a *MyApp) handleProjects(rw http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { - return - } - - ctxLogger := backend.Logger.FromContext(req.Context()) - - _, err := rw.Write([]byte(`{ "projects": ["project-1", "project-2"] }`)) - if err != nil { - ctxLogger.Error("Failed to write response", "error", err) - return - } - rw.WriteHeader(http.StatusOK) -} - -func (a *MyApp) updateDevice(rw http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodPost { - return - } - - if req.Body == nil { - rw.WriteHeader(http.StatusBadRequest) - return - } - - ctxLogger := backend.Logger.FromContext(req.Context()) - - defer func() { - if err := req.Body.Close(); err != nil { - ctxLogger.Warn("Failed to close response body", "error", err) - } - }() - - var payload map[string]any - b, err := io.ReadAll(req.Body) - if err != nil { - ctxLogger.Error("Failed to read request body to bytes", "error", err) - rw.WriteHeader(http.StatusInternalServerError) - return - } - - err := json.Unmarshal(b, &payload) - if err != nil { - ctxLogger.Error("Failed to unmarshal request body to JSON", "error", err) - rw.WriteHeader(http.StatusInternalServerError) - return - } - - state := payload["state"] - // update device with state... - - _, err := rw.Write([]byte(`{ "message": "device updated"] }`)) - if err != nil { - ctxLogger.Error("Failed to write response", "error", err) - 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 `backend.PluginConfigFromContext` function to access `backend.PluginContext`: - -```go -func (a *MyApp) handleNamespaces(rw http.ResponseWriter, req *http.Request) { - pCtx := backend.PluginConfigFromContext(req.Context()) - ctxLogger := backend.Logger.FromContext(req.Context()) - - bytes, err := json.Marshal(pCtx.User) - if err != nil { - ctxLogger.Error("Failed to marshal user to JSON bytes", "error", err) - rw.WriteHeader(http.StatusInternalServerError) - return - } - - _, err := rw.Write(bytes) - if err != nil { - ctxLogger.Error("Failed to write response", "error", err) - return - } - - rw.WriteHeader(http.StatusOK) -} -``` - -## Accessing app resources from the frontend +```typescript +import { getBackendSrv } from '@grafana/runtime'; -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: +export class API { + private backend = getBackendSrv(); -```typescript -export class MyDataSource extends DataSourceWithBackend { - constructor(instanceSettings: DataSourceInstanceSettings) { - super(instanceSettings); - } + constructor(public PluginId: string) {} getNamespaces(): Promise { - return this.getResource('/namespaces'); + return this.backend.get(`/api/plugins/${this.PluginID}/resources/namespaces`); } getProjects(): Promise { - return this.getResource('/projects'); + return this.backend.get(`/api/plugins/${this.PluginID}/resources/projects`); } updateDevice(state: string): Promise { - return this.postResource('device', { state: state }); + return this.backend.post(`/api/plugins/${this.PluginID}/resources/device`, { state: state }); } } ``` -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`: +For example, in your app or component you can instantiate your API class and use `getNamespaces` to send a HTTP GET request to `http://:/api/plugins//resources/namespaces` ```typescript -const namespaces = await props.datasource.getNamespaces(); +const api = new API('my-app-id'); +const namespaces = await api.getNamespaces(); ``` -As another example, you can use `updateDevice` to send a HTTP POST request to `http://:/api/datasources/uid//resources/device` with the provided JSON payload as the second argument: +As another example, you can use `updateDevice` to send a HTTP POST request to `http://:/api/plugins//resources/device` with the provided JSON payload as the second argument: ```typescript -const result = await props.datasource.updateDevice('on'); +const result = await api.updateDevice('on'); ``` ## Additional examples @@ -260,6 +83,5 @@ const result = await props.datasource.updateDevice('on'); 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/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). + - [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 e087a892d..cc719d319 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,211 +24,29 @@ 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. - -```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://:/api/datasources/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 have a couple of options depending on requirements and needs. -If you only need basic support for a couple of different routes retrieving data you can use a switch with the `req.Path`: - -```go -func (d *MyDatasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - if req.Method != http.MethodGet { - return sender.Send(&backend.CallResourceResponse{ - Status: http.StatusNotFound, - }) - } - - 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, - }) - } -} -``` - -Supporting additional routes and methods (GET, POST etc) pretty quickly gets cumbersome and creates code that might be hard to maintain, test and read. Here we recommend to use the more Go-agnostic approach for handling resources and 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)). - -:::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). - -::: - -Lets change and extend the above example by using [`httpadapter`](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter) and [`ServeMux`](https://pkg.go.dev/net/http#ServeMux) and introduce a new `/device` route for updating the state of some device: - -```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) - mux.HandleFunc("/device", ds.updateDevice) - 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) { - if req.Method != http.MethodGet { - return - } - - ctxLogger := backend.Logger.FromContext(req.Context()) - - _, err := rw.Write([]byte(`{ "namespaces": ["ns-1", "ns-2"] }`)) - if err != nil { - ctxLogger.Error("Failed to write response", "error", err) - return - } - rw.WriteHeader(http.StatusOK) -} - -func (d *MyDatasource) handleProjects(rw http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { - return - } - - ctxLogger := backend.Logger.FromContext(req.Context()) - - _, err := rw.Write([]byte(`{ "projects": ["project-1", "project-2"] }`)) - if err != nil { - ctxLogger.Error("Failed to write response", "error", err) - return - } - rw.WriteHeader(http.StatusOK) -} - -func (d *MyDatasource) updateDevice(rw http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodPost { - return - } - - if req.Body == nil { - rw.WriteHeader(http.StatusBadRequest) - return - } +## Accessing datasource resources - ctxLogger := backend.Logger.FromContext(req.Context()) +Once implemented you can access the resources using the Grafana HTTP API and from the frontend. - defer func() { - if err := req.Body.Close(); err != nil { - ctxLogger.Warn("Failed to close response body", "error", err) - } - }() +### Using the Grafana HTTP API - var payload map[string]any - b, err := io.ReadAll(req.Body) - if err != nil { - ctxLogger.Error("Failed to read request body to bytes", "error", err) - rw.WriteHeader(http.StatusInternalServerError) - return - } +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. - err := json.Unmarshal(b, &payload) - if err != nil { - ctxLogger.Error("Failed to unmarshal request body to JSON", "error", err) - rw.WriteHeader(http.StatusInternalServerError) - return - } +With the above example you can access the following resources: - state := payload["state"] - // update device with state... +- HTTP GET `http://:/api/datasources/uid//resources/namespaces` +- HTTP GET `http://:/api/datasources/uid//resources/projects` +- HTTP POST `http://:/api/datasources/uid//resources/device` - _, err := rw.Write([]byte(`{ "message": "device updated"] }`)) - if err != nil { - ctxLogger.Error("Failed to write response", "error", err) - return - } - - rw.WriteHeader(http.StatusOK) -} -``` - -:::note +:::tip -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. +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. ::: -### What if you need access to the backend plugin context? - -Use the `backend.PluginConfigFromContext` function to access `backend.PluginContext`: - -```go -func (d *MyDatasource) handleNamespaces(rw http.ResponseWriter, req *http.Request) { - pCtx := backend.PluginConfigFromContext(req.Context()) - ctxLogger := backend.Logger.FromContext(req.Context()) - - bytes, err := json.Marshal(pCtx.User) - if err != nil { - ctxLogger.Error("Failed to marshal user to JSON bytes", "error", err) - rw.WriteHeader(http.StatusInternalServerError) - return - } - - _, err := rw.Write(bytes) - if err != nil { - ctxLogger.Error("Failed to write response", "error", err) - return - } - - rw.WriteHeader(http.StatusOK) -} -``` - -## Accessing datasource resources from the frontend +### From the frontend 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: diff --git a/docusaurus/docs/shared/implement-resource-handler.md b/docusaurus/docs/shared/implement-resource-handler.md new file mode 100644 index 000000000..7b2416d7e --- /dev/null +++ b/docusaurus/docs/shared/implement-resource-handler.md @@ -0,0 +1,189 @@ +## 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) + mux.HandleFunc("/device", p.updateDevice) + 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) { + if req.Method != http.MethodGet { + rw.WriteHeader(http.StatusNotFound) + return + } + + ctxLogger := backend.Logger.FromContext(req.Context()) + + rw.Header().Add("Content-Type", "application/json") + _, err := rw.Write([]byte(`{ "namespaces": ["ns-1", "ns-2"] }`)) + if err != nil { + ctxLogger.Error("Failed to write response", "error", err) + return + } + rw.WriteHeader(http.StatusOK) +} + +func (p *MyPlugin) handleProjects(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet { + rw.WriteHeader(http.StatusNotFound) + return + } + + ctxLogger := backend.Logger.FromContext(req.Context()) + + rw.Header().Add("Content-Type", "application/json") + _, err := rw.Write([]byte(`{ "projects": ["project-1", "project-2"] }`)) + if err != nil { + ctxLogger.Error("Failed to write response", "error", err) + return + } + rw.WriteHeader(http.StatusOK) +} + +func (p *MyPlugin) updateDevice(rw http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodPost { + rw.WriteHeader(http.StatusNotFound) + return + } + + if req.Body == nil { + rw.WriteHeader(http.StatusBadRequest) + return + } + + ctxLogger := backend.Logger.FromContext(req.Context()) + + defer func() { + if err := req.Body.Close(); err != nil { + ctxLogger.Warn("Failed to close response body", "error", err) + } + }() + + var payload map[string]any + b, err := io.ReadAll(req.Body) + if err != nil { + ctxLogger.Error("Failed to read request body to bytes", "error", err) + rw.WriteHeader(http.StatusInternalServerError) + return + } + + err = json.Unmarshal(b, &payload) + if err != nil { + ctxLogger.Error("Failed to unmarshal request body to JSON", "error", err) + rw.WriteHeader(http.StatusInternalServerError) + return + } + + state := payload["state"] + // update device with state... + + rw.Header().Add("Content-Type", "application/json") + _, err = rw.Write([]byte(fmt.Sprintf(`{ "message": "device updated", "state": "%s" }`, state))) + if err != nil { + ctxLogger.Error("Failed to write response", "error", err) + return + } + + rw.WriteHeader(http.StatusOK) +} +``` + +#### Accessing the backend plugin context + +Use the `backend.PluginConfigFromContext` function to access `backend.PluginContext`: + +```go +func (p *MyPlugin) handleNamespaces(rw http.ResponseWriter, req *http.Request) { + pCtx := backend.PluginConfigFromContext(req.Context()) + ctxLogger := backend.Logger.FromContext(req.Context()) + + bytes, err := json.Marshal(pCtx.User) + if err != nil { + ctxLogger.Error("Failed to marshal user to JSON bytes", "error", err) + rw.WriteHeader(http.StatusInternalServerError) + return + } + + rw.Header().Add("Content-Type", "application/json") + _, err := rw.Write(bytes) + if err != nil { + ctxLogger.Error("Failed to write response", "error", err) + rw.WriteHeader(http.StatusInternalServerError) + 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 { + if req.Method != http.MethodGet { + return sender.Send(&backend.CallResourceResponse{ + Status: http.StatusNotFound, + }) + } + + 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, + }) + } +} +``` From 91e2bd5d1a8bc5e71a37bfea8b99f0fb3bb6969f Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 9 Sep 2024 21:17:33 +0200 Subject: [PATCH 3/4] additional changes --- .../how-to-guides/app-plugins/add-resource-handler.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 index e0f9ecd7d..8d39333a1 100644 --- a/docusaurus/docs/how-to-guides/app-plugins/add-resource-handler.md +++ b/docusaurus/docs/how-to-guides/app-plugins/add-resource-handler.md @@ -19,9 +19,14 @@ You can add a resource handler to your app backend to extend the Grafana HTTP AP ## Uses of resource handlers -The primary way for a data source to retrieve data from a backend is through the [query method](../../tutorials/build-a-data-source-plugin#define-a-query). 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. +The use case and functionality for an app is very broad and therefore also for uses of resource handlers. But in general, an app normally 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 -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. +- specific authentication and authorization needs. +- a 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 those. + +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. From 8933532e5e0d47d27d23fc8b26468471dd08bcbc Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 10 Sep 2024 15:15:10 +0200 Subject: [PATCH 4/4] simplify and changes after review --- .../app-plugins/add-resource-handler.md | 41 +--------- .../add-resource-handler.md | 11 --- .../docs/shared/implement-resource-handler.md | 81 +------------------ 3 files changed, 6 insertions(+), 127 deletions(-) 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 index 8d39333a1..4eeb9a81d 100644 --- a/docusaurus/docs/how-to-guides/app-plugins/add-resource-handler.md +++ b/docusaurus/docs/how-to-guides/app-plugins/add-resource-handler.md @@ -19,12 +19,9 @@ You can add a resource handler to your app backend to extend the Grafana HTTP AP ## Uses of resource handlers -The use case and functionality for an app is very broad and therefore also for uses of resource handlers. But in general, an app normally 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 +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. -- specific authentication and authorization needs. -- a 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 those. +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. @@ -42,45 +39,15 @@ With the above example you can access the following resources: - HTTP GET `http://:/api/plugins//resources/namespaces` - HTTP GET `http://:/api/plugins//resources/projects` -- HTTP POST `http://:/api/plugins//resources/device` ### From the frontend -You can access your resources using the `get` and `post` functions from the `backendSrv` runtime service. To provide a nicer and more convenient API for your components it's recommended to provide a helper class with functions for each route as shown in the following example: +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'; -export class API { - private backend = getBackendSrv(); - - constructor(public PluginId: string) {} - - getNamespaces(): Promise { - return this.backend.get(`/api/plugins/${this.PluginID}/resources/namespaces`); - } - - getProjects(): Promise { - return this.backend.get(`/api/plugins/${this.PluginID}/resources/projects`); - } - - updateDevice(state: string): Promise { - return this.backend.post(`/api/plugins/${this.PluginID}/resources/device`, { state: state }); - } -} -``` - -For example, in your app or component you can instantiate your API class and use `getNamespaces` to send a HTTP GET request to `http://:/api/plugins//resources/namespaces` - -```typescript -const api = new API('my-app-id'); -const namespaces = await api.getNamespaces(); -``` - -As another example, you can use `updateDevice` to send a HTTP POST request to `http://:/api/plugins//resources/device` with the provided JSON payload as the second argument: - -```typescript -const result = await api.updateDevice('on'); +const namespaces = await getBackendSrv().get(`/api/plugins//resources/namespaces`); ``` ## Additional examples 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 cc719d319..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 @@ -38,7 +38,6 @@ 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` -- HTTP POST `http://:/api/datasources/uid//resources/device` :::tip @@ -63,10 +62,6 @@ export class MyDataSource extends DataSourceWithBackend { return this.getResource('/projects'); } - - updateDevice(state: string): Promise { - return this.postResource('device', { state: state }); - } } ``` @@ -76,12 +71,6 @@ For example, in your query editor component, you can access the data source inst const namespaces = await props.datasource.getNamespaces(); ``` -As another example, you can use `updateDevice` to send a HTTP POST request to `http://:/api/datasources/uid//resources/device` with the provided JSON payload as the second argument: - -```typescript -const result = await props.datasource.updateDevice('on'); -``` - ## 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: diff --git a/docusaurus/docs/shared/implement-resource-handler.md b/docusaurus/docs/shared/implement-resource-handler.md index 7b2416d7e..2dec03570 100644 --- a/docusaurus/docs/shared/implement-resource-handler.md +++ b/docusaurus/docs/shared/implement-resource-handler.md @@ -38,7 +38,6 @@ func New() *MyPlugin { mux := http.NewServeMux() mux.HandleFunc("/namespaces", p.handleNamespaces) mux.HandleFunc("/projects", p.handleProjects) - mux.HandleFunc("/device", p.updateDevice) p.resourceHandler := httpadapter.New(mux) return p } @@ -48,111 +47,41 @@ func (p *MyPlugin) CallResource(ctx context.Context, req *backend.CallResourceRe } func (p *MyPlugin) handleNamespaces(rw http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { - rw.WriteHeader(http.StatusNotFound) - return - } - - ctxLogger := backend.Logger.FromContext(req.Context()) - rw.Header().Add("Content-Type", "application/json") _, err := rw.Write([]byte(`{ "namespaces": ["ns-1", "ns-2"] }`)) if err != nil { - ctxLogger.Error("Failed to write response", "error", err) return } rw.WriteHeader(http.StatusOK) } func (p *MyPlugin) handleProjects(rw http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodGet { - rw.WriteHeader(http.StatusNotFound) - return - } - - ctxLogger := backend.Logger.FromContext(req.Context()) - rw.Header().Add("Content-Type", "application/json") _, err := rw.Write([]byte(`{ "projects": ["project-1", "project-2"] }`)) if err != nil { - ctxLogger.Error("Failed to write response", "error", err) return } rw.WriteHeader(http.StatusOK) } - -func (p *MyPlugin) updateDevice(rw http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodPost { - rw.WriteHeader(http.StatusNotFound) - return - } - - if req.Body == nil { - rw.WriteHeader(http.StatusBadRequest) - return - } - - ctxLogger := backend.Logger.FromContext(req.Context()) - - defer func() { - if err := req.Body.Close(); err != nil { - ctxLogger.Warn("Failed to close response body", "error", err) - } - }() - - var payload map[string]any - b, err := io.ReadAll(req.Body) - if err != nil { - ctxLogger.Error("Failed to read request body to bytes", "error", err) - rw.WriteHeader(http.StatusInternalServerError) - return - } - - err = json.Unmarshal(b, &payload) - if err != nil { - ctxLogger.Error("Failed to unmarshal request body to JSON", "error", err) - rw.WriteHeader(http.StatusInternalServerError) - return - } - - state := payload["state"] - // update device with state... - - rw.Header().Add("Content-Type", "application/json") - _, err = rw.Write([]byte(fmt.Sprintf(`{ "message": "device updated", "state": "%s" }`, state))) - if err != nil { - ctxLogger.Error("Failed to write response", "error", err) - return - } - - rw.WriteHeader(http.StatusOK) -} ``` #### Accessing the backend plugin context -Use the `backend.PluginConfigFromContext` function to access `backend.PluginContext`: +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) handleNamespaces(rw http.ResponseWriter, req *http.Request) { +func (p *MyPlugin) handleSomeRoute(rw http.ResponseWriter, req *http.Request) { pCtx := backend.PluginConfigFromContext(req.Context()) - ctxLogger := backend.Logger.FromContext(req.Context()) - bytes, err := json.Marshal(pCtx.User) if err != nil { - ctxLogger.Error("Failed to marshal user to JSON bytes", "error", err) - rw.WriteHeader(http.StatusInternalServerError) return } rw.Header().Add("Content-Type", "application/json") _, err := rw.Write(bytes) if err != nil { - ctxLogger.Error("Failed to write response", "error", err) - rw.WriteHeader(http.StatusInternalServerError) return } - rw.WriteHeader(http.StatusOK) } ``` @@ -163,12 +92,6 @@ Manually implementing the `backend.CallResourceHandler` interface might be enoug ```go func (p *MyPlugin) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - if req.Method != http.MethodGet { - return sender.Send(&backend.CallResourceResponse{ - Status: http.StatusNotFound, - }) - } - switch req.Path { case "namespaces": return sender.Send(&backend.CallResourceResponse{