From 41ee7cad15349816a94045fb54401351b9da9f22 Mon Sep 17 00:00:00 2001 From: Lavender Shannon Date: Mon, 15 Jan 2024 23:26:13 -0600 Subject: [PATCH] Many changes. Go files moved around, interpolate variables function with test, retrodaredevil is organization, docker compose uses oss image --- .github/workflows/is-compatible.yml | 6 +- DEVELOPMENT.md | 27 ++++++++ README.md | 40 +++++------- docker-compose.yaml | 8 +-- pkg/main.go | 4 +- pkg/plugin/datasource.go | 63 ++++-------------- pkg/plugin/{util => parsing}/parsing.go | 12 +--- pkg/plugin/{util => parsing}/parsing_test.go | 2 +- pkg/plugin/querymodel/querymodel.go | 18 ++++++ pkg/plugin/queryvariables/variables.go | 68 ++++++++++++++++++++ pkg/plugin/queryvariables/variables_test.go | 9 +++ pkg/plugin/util/variables.go | 14 ---- pkg/{plugin/util => util/graphql}/graphql.go | 2 +- provisioning/datasources/datasources.yml | 2 +- src/components/QueryEditor.tsx | 18 ++---- src/datasource.ts | 6 +- src/plugin.json | 4 +- src/types.ts | 3 +- src/variables.test.ts | 66 +++++++++++++++++++ src/variables.ts | 51 +++++++++++++-- 20 files changed, 290 insertions(+), 133 deletions(-) rename pkg/plugin/{util => parsing}/parsing.go (96%) rename pkg/plugin/{util => parsing}/parsing_test.go (99%) create mode 100644 pkg/plugin/querymodel/querymodel.go create mode 100644 pkg/plugin/queryvariables/variables.go create mode 100644 pkg/plugin/queryvariables/variables_test.go delete mode 100644 pkg/plugin/util/variables.go rename pkg/{plugin/util => util/graphql}/graphql.go (99%) create mode 100644 src/variables.test.ts diff --git a/.github/workflows/is-compatible.yml b/.github/workflows/is-compatible.yml index 9a11534..46dfb82 100644 --- a/.github/workflows/is-compatible.yml +++ b/.github/workflows/is-compatible.yml @@ -1,5 +1,9 @@ name: Latest Grafana API compatibility check -on: [pull_request] +on: + pull_request: + push: + branches: + - main jobs: compatibilitycheck: diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 0ad562c..4550fec 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -103,3 +103,30 @@ This section contains notes about dependencies. * https://github.com/graphql/graphiql/issues/2405#issuecomment-1469851608 (yes as of writing it says it's closed, but it's not) * It's not a bad thing that we include this dependency because it gives us a couple of types that we end up using + +## To-Do + +* Queries that result in errors should display nice errors messages on the frontend +* Add ability to have multiple parsing options + * Add "advanced options" section that has "Partition by" and "Alias by" + * Including these might be necessary as you may want to partition by and alias by different things for different parts of a GraphQL query + * Advances options can also include the ability to add custom labels to the response - this allows different parsing options to be distinguished by Grafana +* Add support for secure variable data defined in the data source configuration + * The variables defined here cannot be overridden for any request - this is for security + * Also add support for secure HTTP headers +* See what minimum Grafana version we can support +* Add support for variables: https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/add-support-for-variables +* Add metrics to backend component: https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/add-logs-metrics-traces-for-backend-plugins#implement-metrics-in-your-plugin +* Support returning logs data: https://grafana.com/developers/plugin-tools/tutorials/build-a-logs-data-source-plugin + * We could just add `"logs": true` to `plugin.json`, however we need to support the renaming of fields because sometimes the `body` or `timestamp` fields will be nested +* Publish as a plugin + * https://grafana.com/developers/plugin-tools/publish-a-plugin/publish-a-plugin + * https://grafana.com/developers/plugin-tools/publish-a-plugin/sign-a-plugin#generate-an-access-policy-token + * https://grafana.com/legal/plugins/ + * https://grafana.com/developers/plugin-tools/publish-a-plugin/provide-test-environment +* Publish on a private plugin repository + * https://volkovlabs.io/blog/installing-grafana-plugins-from-a-private-repository-805b54a1add3/ +* Create a GraphQL button panel (or a SolarThing app) that has a button panel that can be used to + * If we create an app, we can follow https://github.com/RedisGrafana/grafana-redis-app + * https://github.com/RedisGrafana/grafana-redis-app/blob/e093d18a021bb28ba7df3a54d7ad17c2d8e38f88/src/redis-gears-panel/components/RedisGearsPanel/RedisGearsPanel.tsx#L314 +* Auto-populate the data path field by using `documentAST` to recognize the first path to an array diff --git a/README.md b/README.md index 05c9935..0784a29 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,15 @@ This datasource tries to reimagine how GraphQL queries should be made from Grafa Requests are made in the backend. Results are consistent between queries and alerting. +## Features + +* Complex GraphQL responses can be turned into timeseries data, or a simple table +* Includes [GraphiQL](https://github.com/graphql/graphiql) query editor. Autocompletion and documentation for the GraphQL schema available inside Grafana! +* This is a backend plugin, so alerting is supported +* `from` and `to` variables are given to the query via [native GraphQL variables](https://graphql.org/learn/queries/#variables) +* Variables section of the query editor supports interpolation of string values using [Grafana variables](https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/). (\*not supported in alerting or other backend-only queries) +* [Annotation support](https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/annotate-visualizations/) + ## Variables @@ -15,11 +24,13 @@ Requests are made in the backend. Results are consistent between queries and ale Certain variables are provided to every query. These include: -| Variable | Type | Description | Grafana counterpart | -|---------------|--------|------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------| -| `from` | Number | Epoch milliseconds of the "from" time. Passed as a number. | [$__from](https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#__from-and-__to) | -| `to` | Number | Epoch milliseconds of the "to" time. Passed as a number. | [$__to](https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#__from-and-__to) | -| `interval_ms` | Number | Milliseconds of the interval. Equivalent to `to - from`. | [$__interval_ms](https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#__interval) | +| Variable | Type | Description | Grafana counterpart | +|-----------------|--------|--------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------| +| `from` | Number | Epoch milliseconds of the "from" time | [$__from](https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#__from-and-__to) | +| `to` | Number | Epoch milliseconds of the "to" time | [$__to](https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#__from-and-__to) | +| `interval_ms` | Number | The suggested duration between time points in a time series query | [$__interval_ms](https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#__interval) | +| `maxDataPoints` | Number | Maximum number of data points that should be returned from a time series query | N/A | +| `refId` | String | Unique identifier of the query, set by the frontend call | N/A | An example usage is shown in the most basic query: @@ -106,22 +117,3 @@ References: * This error indicates that the query returns more fields than just the time and the datapoint. * For alerts, the response from the GraphQL query cannot contain more than the time and datapoint. At this time, you cannot use other attributes from the result to filter the data. -## To-Do - -* Add ability to have multiple parsing options - * Add "advanced options" section that has "Partition by" and "Alias by" - * Including these might be necessary as you may want to partition by and alias by different things for different parts of a GraphQL query - * Advances options can also include the ability to add custom labels to the response - this allows different parsing options to be distinguished by Grafana -* Add support for secure variable data defined in the data source configuration - * The variables defined here cannot be overridden for any request - this is for security - * Also add support for secure HTTP headers -* See what minimum Grafana version we can support -* Add support for variables: https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/add-support-for-variables -* Add metrics to backend component: https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/add-logs-metrics-traces-for-backend-plugins#implement-metrics-in-your-plugin -* Support returning logs data: https://grafana.com/developers/plugin-tools/tutorials/build-a-logs-data-source-plugin - * We could just add `"logs": true` to `plugin.json`, however we need to support the renaming of fields because sometimes the `body` or `timestamp` fields will be nested -* Publish as a plugin - * https://grafana.com/developers/plugin-tools/publish-a-plugin/publish-a-plugin - * https://grafana.com/developers/plugin-tools/publish-a-plugin/sign-a-plugin#generate-an-access-policy-token - * https://grafana.com/legal/plugins/ - * https://grafana.com/developers/plugin-tools/publish-a-plugin/provide-test-environment diff --git a/docker-compose.yaml b/docker-compose.yaml index 6ef3606..3d966fa 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,15 +2,15 @@ version: '3.0' services: grafana: - container_name: 'wildmountainfarms-wildgraphql-datasource' + container_name: 'grafana-dev-wildgraphql' platform: 'linux/amd64' build: context: ./.config args: - grafana_image: ${GRAFANA_IMAGE:-grafana-enterprise} - grafana_version: ${GRAFANA_VERSION:-10.0.3} + grafana_image: ${GRAFANA_IMAGE:-grafana-oss} + grafana_version: ${GRAFANA_VERSION:-10.0.10} ports: - 3000:3000/tcp volumes: - - ./dist:/var/lib/grafana/plugins/wildmountainfarms-wildgraphql-datasource + - ./dist:/var/lib/grafana/plugins/retrodaredevil-wildgraphql-datasource - ./provisioning:/etc/grafana/provisioning diff --git a/pkg/main.go b/pkg/main.go index 62b5eb0..9b5fc17 100644 --- a/pkg/main.go +++ b/pkg/main.go @@ -17,8 +17,8 @@ func main() { // from Grafana to create different instances of SampleDatasource (per datasource // ID). When datasource configuration changed Dispose method will be called and // new datasource instance created using NewSampleDatasource factory. - if err := datasource.Manage("wildmountainfarms-wildgraphql-datasource", plugin.NewDatasource, datasource.ManageOpts{}); err != nil { + if err := datasource.Manage("retrodaredevil-wildgraphql-datasource", plugin.NewDatasource, datasource.ManageOpts{}); err != nil { log.DefaultLogger.Error(err.Error()) os.Exit(1) } -} \ No newline at end of file +} diff --git a/pkg/plugin/datasource.go b/pkg/plugin/datasource.go index 805b1ab..f97dced 100644 --- a/pkg/plugin/datasource.go +++ b/pkg/plugin/datasource.go @@ -9,7 +9,10 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/backend/log" - "github.com/wildmountainfarms/wild-graphql-datasource/pkg/plugin/util" + "github.com/wildmountainfarms/wild-graphql-datasource/pkg/plugin/parsing" + "github.com/wildmountainfarms/wild-graphql-datasource/pkg/plugin/querymodel" + "github.com/wildmountainfarms/wild-graphql-datasource/pkg/plugin/queryvariables" + "github.com/wildmountainfarms/wild-graphql-datasource/pkg/util/graphql" "net/http" ) @@ -85,16 +88,6 @@ func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataReques return response, nil } -// queryModel represents data sent from the frontend to perform a query -type queryModel struct { - QueryText string `json:"queryText"` - // The name of the operation, or a blank string to let the GraphQL server infer the operation name - OperationName string `json:"operationName"` - // The variables for the operation. May either be a string or a map[string]interface{} - Variables interface{} `json:"variables"` - ParsingOptions []util.ParsingOption `json:"parsingOptions"` -} - func statusFromResponse(response http.Response) backend.Status { for _, status := range []backend.Status{} { if response.StatusCode == int(status) { @@ -112,8 +105,8 @@ func (d *Datasource) query(ctx context.Context, pCtx backend.PluginContext, quer log.DefaultLogger.Info(fmt.Sprintf("JSON is: %s", query.JSON)) - // Unmarshal the JSON into our queryModel. - var qm queryModel + // Unmarshal the JSON into our QueryModel. + var qm querymodel.QueryModel err := json.Unmarshal(query.JSON, &qm) if err != nil { @@ -128,40 +121,10 @@ func (d *Datasource) query(ctx context.Context, pCtx backend.PluginContext, quer } log.DefaultLogger.Info("Query text is: " + qm.QueryText) - // Although the frontend has access to global variable substitution (https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables) - // the backend does not. - // Because of this, it's beneficial to encourage users to write queries that rely as little on the frontend as possible. - // This allows us to support alerting later. - // These variable names that we are "hard coding" should be as similar to possible as those global variables that are available - // Forum post here: https://community.grafana.com/t/how-to-use-template-variables-in-your-data-source/63250#backend-data-sources-3 - // More information here: https://grafana.com/docs/grafana/latest/dashboards/variables/ - - var variables = map[string]interface{}{} - switch value := qm.Variables.(type) { - case string: - // This case happens when the frontend is not involved at all. This is likely an alert. - // Remember that these variables are not interpolated - - err := json.Unmarshal([]byte(value), &variables) - if err != nil { - log.DefaultLogger.Error("Got error while parsing variables! Error", err) - log.DefaultLogger.Info(fmt.Sprintf("Value of variables from parsing error is: %s", value)) - - // continue executing query without interpolated variables - // TODO consider if we want a flag in the options to prevent the query from continuing further in the case of an error - } - case map[string]interface{}: - // This case happens when the frontend is able to interpolate the variables before passing them to us - // or happens when someone has directly configured the variables option in the JSON itself - // and storing it as an object rather than a string like the QueryEditor does. - // If this is the ladder case, variables have not been interpolated - variables = value - default: - log.DefaultLogger.Error("Unable to parse variables for ref ID:" + query.RefID) - } - util.AutoPopulateVariables(query, &variables) + // use later: pCtx.AppInstanceSettings.DecryptedSecureJSONData + variables, _ := queryvariables.ParseVariables(query, qm.Variables) - graphQLRequest := util.GraphQLRequest{ + graphQLRequest := graphql.GraphQLRequest{ Query: qm.QueryText, OperationName: qm.OperationName, Variables: variables, @@ -183,7 +146,7 @@ func (d *Datasource) query(ctx context.Context, pCtx backend.PluginContext, quer } status := statusFromResponse(*resp) - graphQLResponse, responseParseError := util.ParseGraphQLResponse(resp.Body) + graphQLResponse, responseParseError := graphql.ParseGraphQLResponse(resp.Body) if responseParseError != nil { return &backend.DataResponse{ Error: err, @@ -222,7 +185,7 @@ func (d *Datasource) query(ctx context.Context, pCtx backend.PluginContext, quer // add the frames to the response. for _, parsingOption := range qm.ParsingOptions { - frame, err := util.ParseData( + frame, err := parsing.ParseData( graphQLResponse.Data, parsingOption, ) @@ -242,7 +205,7 @@ func (d *Datasource) query(ctx context.Context, pCtx backend.PluginContext, quer func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { // test command to do the same thing: // curl -X POST -H "Content-Type: application/json" -d '{"query":"{\n\t\t __schema{\n\t\t\tqueryType{name}\n\t\t }\n\t\t}"}' https://swapi-graphql.netlify.app/.netlify/functions/index - graphQLRequest := util.GraphQLRequest{ + graphQLRequest := graphql.GraphQLRequest{ Query: `{ __schema{ queryType{name} @@ -260,7 +223,7 @@ func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRe return nil, err } - graphQLResponse, responseParseError := util.ParseGraphQLResponse(resp.Body) + graphQLResponse, responseParseError := graphql.ParseGraphQLResponse(resp.Body) if responseParseError != nil { if resp.StatusCode == 200 { return &backend.CheckHealthResult{ diff --git a/pkg/plugin/util/parsing.go b/pkg/plugin/parsing/parsing.go similarity index 96% rename from pkg/plugin/util/parsing.go rename to pkg/plugin/parsing/parsing.go index 139b615..1d43c19 100644 --- a/pkg/plugin/util/parsing.go +++ b/pkg/plugin/parsing/parsing.go @@ -1,9 +1,10 @@ -package util +package parsing import ( "errors" "fmt" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/wildmountainfarms/wild-graphql-datasource/pkg/plugin/querymodel" "reflect" "strings" "time" @@ -11,14 +12,7 @@ import ( // the purpose of this file is to parse JSON data with configuration from a ParsingOption -type ParsingOption struct { - // The path from the root to the array. This is dot-delimited - DataPath string `json:"dataPath"` - // the time path relative to the data path. - TimePath string `json:"timePath"` -} - -func ParseData(graphQlResponseData map[string]interface{}, parsingOption ParsingOption) (*data.Frame, error) { +func ParseData(graphQlResponseData map[string]interface{}, parsingOption querymodel.ParsingOption) (*data.Frame, error) { if len(parsingOption.DataPath) == 0 { return nil, errors.New("data path cannot be empty") } diff --git a/pkg/plugin/util/parsing_test.go b/pkg/plugin/parsing/parsing_test.go similarity index 99% rename from pkg/plugin/util/parsing_test.go rename to pkg/plugin/parsing/parsing_test.go index 29f5167..be6be5e 100644 --- a/pkg/plugin/util/parsing_test.go +++ b/pkg/plugin/parsing/parsing_test.go @@ -1,4 +1,4 @@ -package util +package parsing import ( "bytes" diff --git a/pkg/plugin/querymodel/querymodel.go b/pkg/plugin/querymodel/querymodel.go new file mode 100644 index 0000000..3b6d88d --- /dev/null +++ b/pkg/plugin/querymodel/querymodel.go @@ -0,0 +1,18 @@ +package querymodel + +// QueryModel represents data sent from the frontend to perform a query +type QueryModel struct { + QueryText string `json:"queryText"` + // The name of the operation, or a blank string to let the GraphQL server infer the operation name + OperationName string `json:"operationName"` + // The variables for the operation. May either be a string or a map[string]interface{} + Variables interface{} `json:"variables"` + ParsingOptions []ParsingOption `json:"parsingOptions"` +} + +type ParsingOption struct { + // The path from the root to the array. This is dot-delimited + DataPath string `json:"dataPath"` + // the time path relative to the data path. + TimePath string `json:"timePath"` +} diff --git a/pkg/plugin/queryvariables/variables.go b/pkg/plugin/queryvariables/variables.go new file mode 100644 index 0000000..50affec --- /dev/null +++ b/pkg/plugin/queryvariables/variables.go @@ -0,0 +1,68 @@ +package queryvariables + +import ( + "encoding/json" + "fmt" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/log" +) + +func AutoPopulateVariables(query backend.DataQuery, variables map[string]interface{}) { + + // Although the frontend has access to global variable substitution (https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables) + // the backend does not. + // Because of this, it's beneficial to encourage users to write queries that rely as little on the frontend as possible. + // This allows alerting queries to be supported. + // These variable names that we are "hard coding" should be as similar to possible as those global variables that are available + // Forum post here: https://community.grafana.com/t/how-to-use-template-variables-in-your-data-source/63250#backend-data-sources-3 + // More information here: https://grafana.com/docs/grafana/latest/dashboards/variables/ + + variables["from"] = query.TimeRange.From.UnixMilli() + variables["to"] = query.TimeRange.To.UnixMilli() + variables["interval_ms"] = query.Interval.Milliseconds() + variables["maxDataPoints"] = query.MaxDataPoints + variables["refId"] = query.RefID +} + +// When we do get around to supporting variable substitution on the backend, we should make it as similar to the frontend as possible: +// https://github.com/grafana/grafana/blob/4b071f54529e24a2723eedf7ca4e7e989b3bd956/public/app/features/variables/utils.ts#L33 +// The reason we would want variable substitution at all is for annotation queries because you cannot transform the result of those queries in any way. +// It might be possible to deal with that on the frontend, though. + +func ParseVariables(query backend.DataQuery, rawVariables interface{}) (map[string]interface{}, bool) { + var noErrors = true + variables := map[string]interface{}{} + + // call AutoPopulateVariables first so that users can override variables if they see fit + // NOTE: When we add support for secure variables, overriding should NOT be supported + AutoPopulateVariables(query, variables) + + switch typedRawVariables := rawVariables.(type) { + case string: + // This case happens when the frontend is not involved at all. This is likely an alert. + // Remember that these variables are not interpolated + + err := json.Unmarshal([]byte(typedRawVariables), &variables) + if err != nil { + noErrors = false + log.DefaultLogger.Error("Got error while parsing variables! Error", err) + log.DefaultLogger.Info(fmt.Sprintf("Value of variables from parsing error is: %s", typedRawVariables)) + + // continue executing query without interpolated variables + // TODO consider if we want a flag in the options to prevent the query from continuing further in the case of an error + } + case map[string]interface{}: + // This case happens when the frontend is able to interpolate the variables before passing them to us + // or happens when someone has directly configured the variables option in the JSON itself + // and storing it as an object rather than a string like the QueryEditor does. + // If this is the ladder case, variables have not been interpolated + for key, value := range typedRawVariables { + variables[key] = value + } + default: + noErrors = false + log.DefaultLogger.Error("Unable to parse variables for ref ID:" + query.RefID) + } + + return variables, noErrors +} diff --git a/pkg/plugin/queryvariables/variables_test.go b/pkg/plugin/queryvariables/variables_test.go new file mode 100644 index 0000000..2935203 --- /dev/null +++ b/pkg/plugin/queryvariables/variables_test.go @@ -0,0 +1,9 @@ +package queryvariables + +import ( + "testing" +) + +func TestParseVariables(t *testing.T) { + +} diff --git a/pkg/plugin/util/variables.go b/pkg/plugin/util/variables.go deleted file mode 100644 index 09f591d..0000000 --- a/pkg/plugin/util/variables.go +++ /dev/null @@ -1,14 +0,0 @@ -package util - -import "github.com/grafana/grafana-plugin-sdk-go/backend" - -func AutoPopulateVariables(query backend.DataQuery, variables *map[string]interface{}) { - (*variables)["from"] = query.TimeRange.From.UnixMilli() - (*variables)["to"] = query.TimeRange.To.UnixMilli() - (*variables)["interval_ms"] = query.Interval.Milliseconds() -} - -// When we do get around to supporting variable substitution on the backend, we should make it as similar to the frontend as possible: -// https://github.com/grafana/grafana/blob/4b071f54529e24a2723eedf7ca4e7e989b3bd956/public/app/features/variables/utils.ts#L33 -// The reason we would want variable substitution at all is for annotation queries because you cannot transform the result of those queries in any way. -// It might be possible to deal with that on the frontend, though. diff --git a/pkg/plugin/util/graphql.go b/pkg/util/graphql/graphql.go similarity index 99% rename from pkg/plugin/util/graphql.go rename to pkg/util/graphql/graphql.go index aa37c03..510cbc3 100644 --- a/pkg/plugin/util/graphql.go +++ b/pkg/util/graphql/graphql.go @@ -1,4 +1,4 @@ -package util +package graphql import ( "bytes" diff --git a/provisioning/datasources/datasources.yml b/provisioning/datasources/datasources.yml index d8c331b..ab54faf 100644 --- a/provisioning/datasources/datasources.yml +++ b/provisioning/datasources/datasources.yml @@ -2,7 +2,7 @@ apiVersion: 1 datasources: - name: 'wild-graphql-datasource' - type: 'wildmountainfarms-wildgraphql-datasource' + type: 'retrodaredevil-wildgraphql-datasource' access: proxy isDefault: false orgId: 1 diff --git a/src/components/QueryEditor.tsx b/src/components/QueryEditor.tsx index b7ce8f0..6b580d9 100644 --- a/src/components/QueryEditor.tsx +++ b/src/components/QueryEditor.tsx @@ -20,7 +20,7 @@ import {firstValueFrom} from 'rxjs'; import 'graphiql/graphiql.css'; import './modify_graphiql.css' import {ExecutionResult} from "graphql-ws"; -import {AUTO_POPULATED_VARIABLES} from "../variables"; +import {getInterpolatedAutoPopulatedVariables, interpolateVariables} from "../variables"; type Props = QueryEditorProps; @@ -54,16 +54,9 @@ function createFetcher(url: string, withCredentials: boolean, basicAuth?: string const templateSrv = getTemplateSrv(); return async (graphQLParams: FetcherParams, opts?: FetcherOpts) => { const variables = { - ...graphQLParams.variables, // TODO warn user if we are overriding their variables with the autopopulated ones - // TODO also consider if we want to override user variables - ...AUTO_POPULATED_VARIABLES, + ...getInterpolatedAutoPopulatedVariables(templateSrv), + ...interpolateVariables(graphQLParams.variables, templateSrv), // remember one of the downsides here is that we cannot pass scopedVars here because we don't have access to it }; - for (const field in variables) { - const value = variables[field]; - if (typeof value === 'string') { - variables[field] = templateSrv.replace(value); - } - } const query = { ...graphQLParams, variables: variables @@ -75,7 +68,7 @@ function createFetcher(url: string, withCredentials: boolean, basicAuth?: string method: "POST", data: query, responseType: "json", - // TODO consider other options available here + // NOTE: Other options may be necessary here, but at the time of writing I have not tested the different scenarios that might warrant a need to alter these parameters }); // awaiting the observable may throw an exception, and that's OK, we can let that propagate up const response = await firstValueFrom(observable); @@ -179,13 +172,12 @@ function InnerQueryEditor({ query, onChange, onRunQuery, datasource }: Props) { }, [onChange, query, currentOperationName]); - // TODO add logic for editing parsing options. Right now we're just relying on the default query to supply a default that works with the default query - return ( <>

Query

+ {/*TODO allow this to be resized*/} { settings: DataSourceInstanceSettings; @@ -47,10 +48,7 @@ export class DataSource extends DataSourceWithBackend { const variables = getQueryVariablesAsJson(target); - const newVariables: any = { }; - for (const variableName in variables) { - newVariables[variableName] = templateSrv.replace(variables[variableName], request.scopedVars); - } + const newVariables = interpolateVariables(variables, templateSrv, request.scopedVars); return { ...target, variables: newVariables, diff --git a/src/plugin.json b/src/plugin.json index 995bf39..90302ef 100644 --- a/src/plugin.json +++ b/src/plugin.json @@ -2,7 +2,7 @@ "$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json", "type": "datasource", "name": "Wild GraphQL Datasource", - "id": "wildmountainfarms-wildgraphql-datasource", + "id": "retrodaredevil-wildgraphql-datasource", "metrics": true, "backend": true, "alerting": true, @@ -24,7 +24,7 @@ "updated": "%TODAY%" }, "dependencies": { - "grafanaDependency": ">=10.0.3", + "grafanaDependency": ">=9.3.0", "plugins": [] } } diff --git a/src/types.ts b/src/types.ts index a99595a..c849879 100644 --- a/src/types.ts +++ b/src/types.ts @@ -43,8 +43,9 @@ export function getQueryVariablesAsJson(query: WildGraphQLCommonQuery): Record `replaced:${value}`), + getVariables: jest.fn() + }; +} + +describe("interpolateVariables", () => { + // Good example here: https://github.com/RedisGrafana/grafana-redis-datasource/blob/33915a452abcd0016447fb8a881575252605c10e/src/datasource/datasource.test.ts#L221 + const scopedVars: ScopedVars | undefined = { keyName: { value: 'key', text: '' } }; + + it("Flat interpolations", () => { + const templateSrv: TemplateSrv = createTemplateSrv(); + const variables = { + "sourceId": "source_id_value_here" + }; + const result = interpolateVariables(variables, templateSrv, scopedVars); + expect(result).toEqual({ + "sourceId": "replaced:source_id_value_here" + }); + expect(templateSrv.replace).toHaveBeenCalledWith("source_id_value_here", scopedVars) + expect(templateSrv.replace).toHaveBeenCalledTimes(1) + }); + it("No interpolations", () => { + const templateSrv: TemplateSrv = createTemplateSrv(); + const variables = { + "someValue": 5 + }; + const result = interpolateVariables(variables, templateSrv, scopedVars); + expect(result).toEqual(variables); + expect(templateSrv.replace).toHaveBeenCalledTimes(0) + }); + it("Complex interpolations", () => { + const templateSrv: TemplateSrv = createTemplateSrv(); + const variables = { + "someValue": 5, + "coolObject": { + "foo": 6, + "fee": "fi", + "bar": { + "boo": "asdf" + } + } + }; + const result = interpolateVariables(variables, templateSrv, scopedVars); + expect(result).toEqual({ + "someValue": 5, + "coolObject": { + "foo": 6, + "fee": "replaced:fi", + "bar": { + "boo": "replaced:asdf" + } + } + }); + expect(templateSrv.replace).toHaveBeenCalledTimes(2) + }); +}); diff --git a/src/variables.ts b/src/variables.ts index 57b2c79..0b9f097 100644 --- a/src/variables.ts +++ b/src/variables.ts @@ -1,4 +1,5 @@ -import {getTemplateSrv} from "@grafana/runtime"; +import {getTemplateSrv, TemplateSrv} from "@grafana/runtime"; +import {ScopedVars} from "@grafana/data"; /** * This represents variables that are automatically populated. @@ -13,10 +14,48 @@ import {getTemplateSrv} from "@grafana/runtime"; * but we want to reduce the dependency on the frontend here specifically because the backend cannot use {@link getTemplateSrv}. * Remember THE VALUES HERE ARE NOT USED BY THE BACKEND AND ARE ONLY USED FOR DEBUGGING QUERIES IN THE FRONTEND BY THE RUN BUTTON. */ -export const AUTO_POPULATED_VARIABLES: Record = { - // TODO pass these values as numbers after interpolating them - "from": "$__from", - "to": "$__to", - "interval_ms": "$__interval_ms", +const AUTO_POPULATED_VARIABLES: Record any> = { + "from": templateSrv => Number(templateSrv.replace("$__from")), + "to": templateSrv => Number(templateSrv.replace("$__to")), + "interval_ms": templateSrv => Number(templateSrv.replace("$__interval_ms")), }; +/** + * This should only be used for client-side only queries, such as the Execute button. + * Remember that this implementation is not meant to be perfect, but an approximation of how the backend functions + */ +export function getInterpolatedAutoPopulatedVariables(templateSrv: TemplateSrv): Record { + const variables: any = {}; + for (const variableName in AUTO_POPULATED_VARIABLES) { + const func = AUTO_POPULATED_VARIABLES[variableName]; + const result = func(templateSrv); + if (isNaN(result)) { + console.error("Could not add interpolation for variable: " + variableName + ". Will not pass as a variable."); + } else { + variables[variableName] = result; + } + } + + return variables; +} + + + +function doInterpolate(object: any, templateSrv: TemplateSrv, scopedVars?: ScopedVars): any { + switch (typeof object) { + case 'string': + return templateSrv.replace(object, scopedVars) + case 'object': + const newObject: any = {}; + for (const field in object) { + newObject[field] = doInterpolate(object[field], templateSrv, scopedVars); + } + return newObject; + } + return object; +} + +export function interpolateVariables(variables: Record, templateSrv: TemplateSrv, scopedVars?: ScopedVars): Record { + return doInterpolate(variables, templateSrv, scopedVars); +} +