From 6f2557995f59e4cb64d65427247522d753528b3b Mon Sep 17 00:00:00 2001 From: Lavender Shannon Date: Wed, 13 Mar 2024 00:38:14 -0500 Subject: [PATCH] swapi dashboard, flattenData ignores nulls --- DEVELOPMENT.md | 18 +- docker-compose.yaml | 1 + pkg/plugin/datasource.go | 6 +- pkg/plugin/parsing/framemap/framemap.go | 8 +- pkg/plugin/parsing/framemap/framemap_test.go | 31 +- pkg/plugin/parsing/parsing.go | 65 ++- pkg/plugin/parsing/parsing_test.go | 5 +- pkg/util/graphql/graphql.go | 24 +- pkg/util/jsonnode/json_test.go | 22 +- pkg/util/jsonnode/node.go | 2 - pkg/util/jsonnode/primitives.go | 101 +--- provisioned-dashboards/swapi-dashboard.json | 502 +++++++++++++++++++ provisioning/dashboards/dashboards.yaml | 11 + provisioning/datasources/datasources.yml | 10 + src/datasource.ts | 12 +- 15 files changed, 651 insertions(+), 167 deletions(-) create mode 100644 provisioned-dashboards/swapi-dashboard.json create mode 100644 provisioning/dashboards/dashboards.yaml diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index bfb93b5..56d7c68 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -123,14 +123,18 @@ This section contains notes about dependencies. ## To-Do -* Make data returned in the same order as the JSON specified it was - * GoDS seems like a good solution, but there is a problem with its linked hash map deserializer: https://github.com/emirpasic/gods/issues/171 +* Add a provisioned dashboard and datasource that test a publicly available API +* Updated `src/README.md` +* Decide on a simpler default query (https://github.com/wildmountainfarms/wild-graphql-datasource/issues/1) +* Make annotation queries more intuitive * Add ability to move parsing options up and down * 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 + +Lower priority To-Dos + +* Add support for variables: https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/add-support-for-variables#add-support-for-query-variables-to-your-data-source * 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 @@ -139,9 +143,11 @@ This section contains notes about dependencies. * 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 +* Look into Apollo GraphQL + * https://studio.apollographql.com/sandbox/explorer + * https://www.apollographql.com/docs/graphos/explorer/ + * https://www.npmjs.com/package/@apollo/explorer diff --git a/docker-compose.yaml b/docker-compose.yaml index c8386f0..2ca6565 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -15,3 +15,4 @@ services: volumes: - ./dist:/var/lib/grafana/plugins/retrodaredevil-wildgraphql-datasource - ./provisioning:/etc/grafana/provisioning + - ./provisioned-dashboards:/provisioned-dashboards diff --git a/pkg/plugin/datasource.go b/pkg/plugin/datasource.go index fd0df52..32cb65f 100644 --- a/pkg/plugin/datasource.go +++ b/pkg/plugin/datasource.go @@ -123,14 +123,14 @@ func (d *Datasource) query(ctx context.Context, pCtx backend.PluginContext, quer // use later: pCtx.AppInstanceSettings.DecryptedSecureJSONData variables, _ := queryvariables.ParseVariables(query, qm.Variables) - graphQLRequest := graphql.GraphQLRequest{ + graphQLRequest := graphql.Request{ Query: qm.QueryText, OperationName: qm.OperationName, Variables: variables, } request, err := graphQLRequest.ToRequest(ctx, d.settings.URL) if err != nil { - // We don't expect the conversion of the GraphQLRequest into a http.Request to fail + // We don't expect the conversion of the graphql.Request into a http.Request to fail return nil, err } @@ -202,7 +202,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 := graphql.GraphQLRequest{ + graphQLRequest := graphql.Request{ Query: `{ __schema{ queryType{name} diff --git a/pkg/plugin/parsing/framemap/framemap.go b/pkg/plugin/parsing/framemap/framemap.go index cc132bd..742ef28 100644 --- a/pkg/plugin/parsing/framemap/framemap.go +++ b/pkg/plugin/parsing/framemap/framemap.go @@ -6,7 +6,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" ) -type FrameAndLabels struct { +type frameAndLabels struct { labels data.Labels // A map of field names to an array of the values of that given column fieldMap *linkedhashmap.Map[string, any] @@ -17,12 +17,12 @@ func keyOfLabels(labels data.Labels) string { } type FrameMap struct { - data *linkedhashmap.Map[string, FrameAndLabels] + data *linkedhashmap.Map[string, frameAndLabels] } func New() *FrameMap { return &FrameMap{ - data: linkedhashmap.New[string, FrameAndLabels](), + data: linkedhashmap.New[string, frameAndLabels](), } } @@ -38,7 +38,7 @@ func (f *FrameMap) Put(labels data.Labels, fieldMap *linkedhashmap.Map[string, a mapKey := keyOfLabels(labels) f.data.Put( mapKey, - FrameAndLabels{ + frameAndLabels{ labels: labels, fieldMap: fieldMap, }, diff --git a/pkg/plugin/parsing/framemap/framemap_test.go b/pkg/plugin/parsing/framemap/framemap_test.go index 9b8e152..dedb71f 100644 --- a/pkg/plugin/parsing/framemap/framemap_test.go +++ b/pkg/plugin/parsing/framemap/framemap_test.go @@ -1,27 +1,13 @@ package framemap import ( + "github.com/emirpasic/gods/v2/maps/linkedhashmap" "github.com/grafana/grafana-plugin-sdk-go/data" "reflect" "testing" "time" ) -func TestEqualityAndHash(t *testing.T) { - labelsA := data.Labels{ - "asdf": "a", - } - labelsB := data.Labels{ - "asdf": "b", - } - if labelsEqual(labelsA, labelsB) { - t.Error("labelsA should not equal labelsB") - } - if labelsHash(labelsA) == labelsHash(labelsB) { - t.Error("Should not have the same hashes!") - } -} - func TestFrameMap(t *testing.T) { fm := New() labelsA := data.Labels{ @@ -30,14 +16,13 @@ func TestFrameMap(t *testing.T) { labelsB := data.Labels{ "asdf": "b", } - dataA := map[string]interface{}{ - "batteryVoltage": []float64{22.4, 22.5}, - "dateMillis": []time.Time{time.UnixMilli(1705974650887), time.UnixMilli(1705974659884)}, - } - dataB := map[string]interface{}{ - "batteryVoltage": []float64{22.43, 22.51}, - "dateMillis": []time.Time{time.UnixMilli(1705974650888), time.UnixMilli(1705974659885)}, - } + dataA := linkedhashmap.New[string, any]() + dataA.Put("batteryVoltage", []float64{22.4, 22.5}) + dataA.Put("dateMillis", []time.Time{time.UnixMilli(1705974650887), time.UnixMilli(1705974659884)}) + + dataB := linkedhashmap.New[string, any]() + dataB.Put("batteryVoltage", []float64{22.43, 22.51}) + dataB.Put("dateMillis", []time.Time{time.UnixMilli(1705974650888), time.UnixMilli(1705974659885)}) _, exists := fm.Get(labelsA) if exists { diff --git a/pkg/plugin/parsing/parsing.go b/pkg/plugin/parsing/parsing.go index f9779f5..6374738 100644 --- a/pkg/plugin/parsing/parsing.go +++ b/pkg/plugin/parsing/parsing.go @@ -81,7 +81,12 @@ func ParseData(graphQlResponseData *jsonnode.Object, parsingOption querymodel.Pa for _, dataElement := range dataArray { flatData := jsonnode.NewObject() - flattenData(dataElement, "", flatData) + if !flattenData(dataElement, "", flatData) { + // If we cannot flatten the data, we must skip this entry + // TODO consider logging this + // Data may not be able to be flattened if a null value is present in the data + continue + } labels, err := getLabelsFromFlatData(flatData, parsingOption) if err != nil { return nil, err, FRIENDLY_ERROR // getLabelsFromFlatData must always return a friendly error @@ -99,7 +104,7 @@ func ParseData(graphQlResponseData *jsonnode.Object, parsingOption querymodel.Pa if key == parsingOption.TimePath { var timeValue time.Time switch typedValue := value.(type) { - case *jsonnode.String: + case jsonnode.String: // TODO allow user to customize time format // Look at https://stackoverflow.com/questions/522251/whats-the-difference-between-iso-8601-and-rfc-3339-date-formats // and also consider using time.RFC339Nano instead @@ -108,7 +113,7 @@ func ParseData(graphQlResponseData *jsonnode.Object, parsingOption querymodel.Pa return nil, errors.New(fmt.Sprintf("Time could not be parsed! Time: %s", typedValue)), FRIENDLY_ERROR } timeValue = parsedTime - case *jsonnode.Number: + case jsonnode.Number: epochMillis, err := typedValue.Int64() if err != nil { return nil, err, UNKNOWN_ERROR @@ -136,7 +141,7 @@ func ParseData(graphQlResponseData *jsonnode.Object, parsingOption querymodel.Pa switch typedExistingFieldValues := existingFieldValues.(type) { case []float64: switch typedValue := value.(type) { - case *jsonnode.Number: + case jsonnode.Number: number, err := typedValue.Float64() if err != nil { return nil, err, UNKNOWN_ERROR @@ -147,14 +152,14 @@ func ParseData(graphQlResponseData *jsonnode.Object, parsingOption querymodel.Pa } case []string: switch typedValue := value.(type) { - case *jsonnode.String: + case jsonnode.String: fieldMap.Put(key, append(typedExistingFieldValues, typedValue.String())) default: return nil, errors.New(fmt.Sprintf("Existing field values for key: %s is string, but got value with type: %v", key, reflect.TypeOf(value))), FRIENDLY_ERROR } case []bool: switch typedValue := value.(type) { - case *jsonnode.Boolean: + case jsonnode.Boolean: fieldMap.Put(key, append(typedExistingFieldValues, typedValue.Bool())) default: return nil, errors.New(fmt.Sprintf("Existing field values for key: %s is bool, but got value with type: %v", key, reflect.TypeOf(value))), FRIENDLY_ERROR @@ -164,18 +169,16 @@ func ParseData(graphQlResponseData *jsonnode.Object, parsingOption querymodel.Pa } } else { switch typedValue := value.(type) { - case *jsonnode.Number: + case jsonnode.Number: number, err := typedValue.Float64() if err != nil { return nil, err, UNKNOWN_ERROR } fieldMap.Put(key, []float64{number}) - case *jsonnode.String: + case jsonnode.String: fieldMap.Put(key, []string{typedValue.String()}) - case *jsonnode.Boolean: + case jsonnode.Boolean: fieldMap.Put(key, []bool{typedValue.Bool()}) - case jsonnode.Null: - // do nothing for null default: return nil, errors.New(fmt.Sprintf("Unsupported and unexpected type for key: %s. Type is: %v", key, reflect.TypeOf(value))), UNKNOWN_ERROR } @@ -200,7 +203,7 @@ func getLabelsFromFlatData(flatData *jsonnode.Object, parsingOption querymodel.P return nil, errors.New(fmt.Sprintf("Label option: %s could not be satisfied as key %s does not exist", labelOption.Name, labelOption.Value)) } switch typedFieldValue := fieldValue.(type) { - case *jsonnode.String: + case jsonnode.String: labels[labelOption.Name] = typedFieldValue.String() default: return nil, errors.New(fmt.Sprintf("Label option: %s could not be satisfied as key %s is not a string. It's type is %v", labelOption.Name, labelOption.Value, reflect.TypeOf(typedFieldValue))) @@ -210,25 +213,47 @@ func getLabelsFromFlatData(flatData *jsonnode.Object, parsingOption querymodel.P return labels, nil } -func flattenArray(array *jsonnode.Array, prefix string, flattenedData *jsonnode.Object) { +func flattenArray(array *jsonnode.Array, prefix string, flattenedData *jsonnode.Object) bool { for key, value := range *array { - flattenedData.Put( - fmt.Sprintf("%s%d", prefix, key), - value, - ) + baseKey := fmt.Sprintf("%s%d", prefix, key) + switch typedValue := value.(type) { + case *jsonnode.Object: + if !flattenData(typedValue, baseKey+".", flattenedData) { + return false + } + case *jsonnode.Array: + if !flattenArray(typedValue, baseKey+".", flattenedData) { + return false + } + case jsonnode.Null: + return false + default: + flattenedData.Put( + baseKey, + value, + ) + } } + return true } -func flattenData(originalData *jsonnode.Object, prefix string, flattenedData *jsonnode.Object) { +func flattenData(originalData *jsonnode.Object, prefix string, flattenedData *jsonnode.Object) bool { for _, key := range originalData.Keys() { value := originalData.Get(key) switch typedValue := value.(type) { case *jsonnode.Object: - flattenData(typedValue, prefix+key+".", flattenedData) + if !flattenData(typedValue, prefix+key+".", flattenedData) { + return false + } case *jsonnode.Array: - flattenArray(typedValue, prefix+key+".", flattenedData) + if !flattenArray(typedValue, prefix+key+".", flattenedData) { + return false + } + case jsonnode.Null: + return false default: flattenedData.Put(prefix+key, typedValue) } } + return true } diff --git a/pkg/plugin/parsing/parsing_test.go b/pkg/plugin/parsing/parsing_test.go index be6be5e..a5ce7a3 100644 --- a/pkg/plugin/parsing/parsing_test.go +++ b/pkg/plugin/parsing/parsing_test.go @@ -3,7 +3,6 @@ package parsing import ( "bytes" "encoding/json" - "fmt" "reflect" "testing" ) @@ -62,8 +61,8 @@ func TestParsingJsonNumbersAsNumbersGood(t *testing.T) { if !aExists || !bExists { t.Fatal("something does not exist") } - t.Log(fmt.Sprintf("Values of a: %d , b: %d", a, b)) - t.Log(fmt.Sprintf("Types of a: %v , b: %v", reflect.TypeOf(a), reflect.TypeOf(b))) + //t.Log(fmt.Sprintf("Values of a: %d , b: %d", a, b)) + //t.Log(fmt.Sprintf("Types of a: %v , b: %v", reflect.TypeOf(a), reflect.TypeOf(b))) if a != json.Number("9223372036854775807") { t.Error("We expect a to be represented precisely") diff --git a/pkg/util/graphql/graphql.go b/pkg/util/graphql/graphql.go index babb938..23cc07f 100644 --- a/pkg/util/graphql/graphql.go +++ b/pkg/util/graphql/graphql.go @@ -10,19 +10,17 @@ import ( "net/http" ) -// NOTE: We may consider using this library in the future instead: https://github.com/machinebox/graphql - -type GraphQLRequest struct { +type Request struct { Query string `json:"query"` // A map of variable names to the value of that variable. Allowed value types are strings, numeric types, and booleans Variables map[string]interface{} `json:"variables,omitempty"` OperationName string `json:"operationName,omitempty"` } -func (request *GraphQLRequest) ToBody() ([]byte, error) { +func (request *Request) ToBody() ([]byte, error) { return json.Marshal(request) } -func (request *GraphQLRequest) ToRequest(ctx context.Context, url string) (*http.Request, error) { +func (request *Request) ToRequest(ctx context.Context, url string) (*http.Request, error) { body, err := request.ToBody() if err != nil { return nil, err @@ -37,34 +35,34 @@ func (request *GraphQLRequest) ToRequest(ctx context.Context, url string) (*http return httpReq, nil } -type GraphQLResponse struct { +type Response struct { // We use a jsonnode.Object here because it maintains the order of the keys in a JSON object Data jsonnode.Object `json:"data"` - Errors []GraphQLError `json:"errors"` + Errors []Error `json:"errors"` } -type GraphQLError struct { +type Error struct { Message string `json:"message"` - Locations []GraphQLErrorLocation `json:"locations,omitempty"` + Locations []ErrorLocation `json:"locations,omitempty"` Path []interface{} `json:"path,omitempty"` Extensions map[string]interface{} `json:"extensions,omitempty"` } -type GraphQLErrorLocation struct { +type ErrorLocation struct { Line int `json:"line"` Column int `json:"column"` } -func ParseGraphQLResponse(body io.ReadCloser) (*GraphQLResponse, error) { +func ParseGraphQLResponse(body io.ReadCloser) (*Response, error) { bodyAsBytes, err := io.ReadAll(body) if err != nil { log.DefaultLogger.Error("We don't expect this!") return nil, err } - var graphQLResponse GraphQLResponse + var graphQLResponse Response err = json.Unmarshal(bodyAsBytes, &graphQLResponse) if err != nil { - log.DefaultLogger.Error("Error while parsing GraphQL response to GraphQLResponse") + log.DefaultLogger.Error("Error while parsing GraphQL response to graphql.Response") return nil, err } return &graphQLResponse, nil diff --git a/pkg/util/jsonnode/json_test.go b/pkg/util/jsonnode/json_test.go index c192ff5..3f0127a 100644 --- a/pkg/util/jsonnode/json_test.go +++ b/pkg/util/jsonnode/json_test.go @@ -26,7 +26,7 @@ func TestPrimitivesAndNestedArray(t *testing.T) { t.Error("Empty array") return } - if value, ok := (*array)[0].(*Boolean); !ok || *value != true { + if value, ok := (*array)[0].(Boolean); !ok || value != true { if !ok { t.Error("First element is not a boolean") return @@ -34,7 +34,7 @@ func TestPrimitivesAndNestedArray(t *testing.T) { t.Error("First element's value is not true!") return } - if value, ok := (*array)[1].(*String); !ok || *value != "asdf" { + if value, ok := (*array)[1].(String); !ok || value != "asdf" { t.Error("Second element's value is not asdf!") return } @@ -42,7 +42,7 @@ func TestPrimitivesAndNestedArray(t *testing.T) { t.Error("Third element's value is not null!") return } - if value, ok := (*array)[3].(*Number); !ok || value.Number() != "1.1" { + if value, ok := (*array)[3].(Number); !ok || value.Number() != "1.1" { t.Error("Forth element's value is not 1.1!") return } @@ -56,11 +56,11 @@ func TestPrimitivesAndNestedArray(t *testing.T) { t.Error("Incorrect data") return } - if value, ok := (*nestedArray)[0].(*Number); !ok || value.Number() != "1" { + if value, ok := (*nestedArray)[0].(Number); !ok || value.Number() != "1" { t.Error("Incorrect data") return } - if value, ok := (*nestedArray)[1].(*Number); !ok || value.Number() != "2" { + if value, ok := (*nestedArray)[1].(Number); !ok || value.Number() != "2" { t.Error("Incorrect data") return } @@ -90,12 +90,12 @@ func TestObject(t *testing.T) { t.Error("No value for key a") return } - number, ok := nodeValue.(*Number) + number, ok := nodeValue.(Number) if !ok { t.Error("Value is not a number") return } - value, err := number.Int() + value, err := number.Int64() if err != nil { t.Fatal(err) return @@ -110,12 +110,12 @@ func TestObject(t *testing.T) { t.Error("No value for key b") return } - number, ok := nodeValue.(*Number) + number, ok := nodeValue.(Number) if !ok { t.Error("Value is not a number") return } - value, err := number.Int() + value, err := number.Int64() if err != nil { t.Fatal(err) return @@ -130,12 +130,12 @@ func TestObject(t *testing.T) { t.Error("No value for key c") return } - number, ok := nodeValue.(*Number) + number, ok := nodeValue.(Number) if !ok { t.Error("Value is not a number") return } - value, err := number.Int() + value, err := number.Int64() if err != nil { t.Fatal(err) return diff --git a/pkg/util/jsonnode/node.go b/pkg/util/jsonnode/node.go index ee9e431..0c6aa7c 100644 --- a/pkg/util/jsonnode/node.go +++ b/pkg/util/jsonnode/node.go @@ -4,6 +4,4 @@ type Node interface { sealed() // make this a sealed interface by having unexported methods String() string - // json.Unmarshaler - UnmarshalJSON(data []byte) error } diff --git a/pkg/util/jsonnode/primitives.go b/pkg/util/jsonnode/primitives.go index d5ac02f..a921ce7 100644 --- a/pkg/util/jsonnode/primitives.go +++ b/pkg/util/jsonnode/primitives.go @@ -1,90 +1,52 @@ package jsonnode import ( - "bytes" "encoding/json" - "errors" - "fmt" "strconv" ) -type Number struct { - number json.Number -} - -func (_ *Number) sealed() {} -func (n *Number) String() string { - return n.number.String() -} +type Number json.Number -func (n *Number) UnmarshalJSON(data []byte) error { - d := json.NewDecoder(bytes.NewReader(data)) - d.UseNumber() - var number json.Number - err := d.Decode(&number) - if err != nil { - return err - } - n.number = number - return nil +func (_ Number) sealed() {} +func (n Number) String() string { + return n.Number().String() } -func (n *Number) Number() json.Number { - return n.number +func (n Number) Number() json.Number { + return json.Number(n) } -func (n *Number) Float64() (float64, error) { - return n.number.Float64() +func (n Number) Float64() (float64, error) { + return n.Number().Float64() } -func (n *Number) Int64() (int64, error) { - return n.number.Int64() +func (n Number) Int64() (int64, error) { + return n.Number().Int64() } -func (n *Number) Int() (int, error) { - return strconv.Atoi(string(n.number)) +func (n Number) Uint64() (uint64, error) { + return strconv.ParseUint(n.String(), 10, 64) } type Boolean bool -func (b *Boolean) sealed() {} +func (b Boolean) sealed() {} -func (b *Boolean) String() string { - if *b { +func (b Boolean) String() string { + if b { return "true" } return "false" } -func (b *Boolean) UnmarshalJSON(data []byte) error { - var value bool - err := json.Unmarshal(data, &value) - if err != nil { - return err - } - *b = Boolean(value) - return nil -} -func (b *Boolean) Bool() bool { - if *b { - return true - } - return false +func (b Boolean) Bool() bool { + return bool(b) } type String string -func (_ *String) sealed() {} +func (_ String) sealed() {} -func (s *String) String() string { - return string(*s) -} -func (s *String) UnmarshalJSON(data []byte) error { - var value string - err := json.Unmarshal(data, &value) - if err != nil { - return err - } - *s = String(value) - return nil +func (s String) String() string { + return string(s) } type Null bool @@ -96,32 +58,15 @@ func (_ Null) sealed() {} func (n Null) String() string { return "null" } -func (n Null) UnmarshalJSON(data []byte) error { - d := createDecoder(bytes.NewReader(data)) - token, err := d.Token() - if err != nil { - return err - } - if token != nil { - return errors.New(fmt.Sprintf("token is not a null token. token: %v", token)) - } - return nil -} func parsePrimitive(token json.Token) Node { switch typedToken := token.(type) { case json.Number: - number := Number{typedToken} - var node Node = &number - return node + return Number(typedToken) case bool: - value := Boolean(typedToken) - var node Node = &value - return node + return Boolean(typedToken) case string: - value := String(typedToken) - var node Node = &value - return node + return String(typedToken) case nil: return NULL } diff --git a/provisioned-dashboards/swapi-dashboard.json b/provisioned-dashboards/swapi-dashboard.json new file mode 100644 index 0000000..248863a --- /dev/null +++ b/provisioned-dashboards/swapi-dashboard.json @@ -0,0 +1,502 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 2, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "retrodaredevil-wildgraphql-datasource", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "10.3.3", + "targets": [ + { + "datasource": { + "type": "retrodaredevil-wildgraphql-datasource", + "uid": "${datasource}" + }, + "parsingOptions": [ + { + "dataPath": "allFilms.films", + "labelOptions": [], + "timePath": "" + } + ], + "queryText": "query {\n allFilms {\n films {\n Title:title\n Directory:director\n Release:releaseDate\n }\n }\n}\n", + "refId": "A" + } + ], + "title": "Film Information", + "type": "table" + }, + { + "datasource": { + "type": "retrodaredevil-wildgraphql-datasource", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "10.3.3", + "targets": [ + { + "datasource": { + "type": "retrodaredevil-wildgraphql-datasource", + "uid": "${datasource}" + }, + "parsingOptions": [ + { + "dataPath": "film", + "labelOptions": [], + "timePath": "" + } + ], + "queryText": "query {film(id: \"ZmlsbXM6MQ==\") {openingCrawl}}\n", + "refId": "A" + } + ], + "title": "A New Hope Opening Crawl", + "type": "table" + }, + { + "datasource": { + "type": "retrodaredevil-wildgraphql-datasource", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Episode ID", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 74, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.3.3", + "targets": [ + { + "datasource": { + "type": "retrodaredevil-wildgraphql-datasource", + "uid": "${datasource}" + }, + "parsingOptions": [ + { + "dataPath": "allFilms.films", + "labelOptions": [], + "timePath": "created" + } + ], + "queryText": "query {\n allFilms {\n films {\n Title:title\n episodeID\n created\n }\n }\n}\n", + "refId": "A" + } + ], + "title": "Database Activity December 2014", + "type": "timeseries" + }, + { + "datasource": { + "type": "retrodaredevil-wildgraphql-datasource", + "uid": "${datasource}" + }, + "description": "This shows the populations of different planets. It is also a great test to understand what happens when the GraphQL response emits null values. Wild GraphQL datasource currently has logic to remove any entry with a null value in one of its fields.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineWidth": 1, + "scaleDistribution": { + "type": "linear" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "People", + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 9 + }, + "id": 4, + "options": { + "barRadius": 0, + "barWidth": 0.97, + "fullHighlight": false, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "orientation": "auto", + "showValue": "auto", + "stacking": "none", + "tooltip": { + "mode": "single", + "sort": "none" + }, + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0 + }, + "pluginVersion": "10.3.3", + "targets": [ + { + "datasource": { + "type": "retrodaredevil-wildgraphql-datasource", + "uid": "${datasource}" + }, + "parsingOptions": [ + { + "dataPath": "allPlanets.planets", + "labelOptions": [], + "timePath": "" + } + ], + "queryText": "query {\n allPlanets {\n planets {\n name\n population\n }\n }\n}\n", + "refId": "A" + } + ], + "title": "Most Populus Planets", + "transformations": [ + { + "id": "filterByValue", + "options": { + "filters": [ + { + "config": { + "id": "lower", + "options": { + "value": 4500000000 + } + }, + "fieldName": "population" + } + ], + "match": "any", + "type": "exclude" + } + } + ], + "type": "barchart" + }, + { + "datasource": { + "type": "retrodaredevil-wildgraphql-datasource", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unitScale": true + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 18 + }, + "id": 5, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "10.3.3", + "targets": [ + { + "datasource": { + "type": "retrodaredevil-wildgraphql-datasource", + "uid": "${datasource}" + }, + "parsingOptions": [ + { + "dataPath": "allStarships.starships", + "labelOptions": [], + "timePath": "" + } + ], + "queryText": "query {\n allStarships {\n starships {\n name\n passengers\n filmConnection {\n totalCount\n }\n }\n }\n}\n", + "refId": "A" + } + ], + "title": "Starships", + "type": "table" + } + ], + "refresh": false, + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "Star Wars GraphQL", + "value": "9c6ae4eb-3ec7-4aed-a2da-ee51f9303dc8" + }, + "hide": 0, + "includeAll": false, + "label": "Data source", + "multi": false, + "name": "datasource", + "options": [], + "query": "retrodaredevil-wildgraphql-datasource", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + } + ] + }, + "time": { + "from": "2014-12-01T06:00:00.000Z", + "to": "2015-01-01T05:59:59.000Z" + }, + "timepicker": {}, + "timezone": "", + "title": "Star Wars Film Info", + "uid": "cf9216bb-502f-41c6-ba11-44999a07a37b", + "version": 6, + "weekStart": "" +} diff --git a/provisioning/dashboards/dashboards.yaml b/provisioning/dashboards/dashboards.yaml new file mode 100644 index 0000000..b50e4af --- /dev/null +++ b/provisioning/dashboards/dashboards.yaml @@ -0,0 +1,11 @@ +# https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards +apiVersion: 1 + +providers: + - name: dashboards + type: file + updateIntervalSeconds: 30 + disableDeletion: false + allowUiUpdates: false + options: + path: /provisioned-dashboards diff --git a/provisioning/datasources/datasources.yml b/provisioning/datasources/datasources.yml index ab54faf..137e52c 100644 --- a/provisioning/datasources/datasources.yml +++ b/provisioning/datasources/datasources.yml @@ -8,3 +8,13 @@ datasources: orgId: 1 version: 1 editable: true + - name: 'Star Wars GraphQL' # Accesses https://github.com/graphql/swapi-graphql + type: 'retrodaredevil-wildgraphql-datasource' + uid: '9c6ae4eb-3ec7-4aed-a2da-ee51f9303dc8' # A consistent ID allows this to be referenced in the provisioned dashboard + # https://studio.apollographql.com/public/star-wars-swapi/variant/current/home + url: 'https://swapi-graphql.netlify.app/.netlify/functions/index' + access: proxy + isDefault: false + orgId: 1 + version: 1 + editable: true diff --git a/src/datasource.ts b/src/datasource.ts index 812127b..b51551b 100644 --- a/src/datasource.ts +++ b/src/datasource.ts @@ -3,17 +3,20 @@ import { CoreApp, DataQueryRequest, DataQueryResponse, - DataSourceInstanceSettings + DataSourceInstanceSettings, } from '@grafana/data'; import {DataSourceWithBackend, getTemplateSrv} from '@grafana/runtime'; import {Observable} from 'rxjs'; import { - DEFAULT_ALERTING_QUERY, DEFAULT_ANNOTATION_QUERY, + DEFAULT_ALERTING_QUERY, + DEFAULT_ANNOTATION_QUERY, DEFAULT_QUERY, - getQueryVariablesAsJson, WildGraphQLAnnotationQuery, + getQueryVariablesAsJson, + WildGraphQLAnnotationQuery, WildGraphQLAnyQuery, - WildGraphQLDataSourceOptions, WildGraphQLMainQuery + WildGraphQLDataSourceOptions, + WildGraphQLMainQuery } from './types'; import {interpolateVariables} from "./variables"; @@ -66,5 +69,6 @@ export class DataSource extends DataSourceWithBackend