Skip to content

Commit

Permalink
Data keeps field order of GraphQL response JSON
Browse files Browse the repository at this point in the history
  • Loading branch information
retrodaredevil committed Mar 12, 2024
1 parent e1183d4 commit 0e46ce1
Show file tree
Hide file tree
Showing 14 changed files with 311 additions and 196 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ toolchain go1.21.5

require github.com/grafana/grafana-plugin-sdk-go v0.198.0

require github.com/emirpasic/gods/v2 v2.0.0-alpha

require (
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/apache/arrow/go/v13 v13.0.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027/go.mod h1:Ro8st/El
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
github.com/elazarl/goproxy/ext v0.0.0-20220115173737-adb46da277ac h1:9yrT5tmn9Zc0ytWPASlaPwQfQMQYnRf0RSDe1XvHw0Q=
github.com/elazarl/goproxy/ext v0.0.0-20220115173737-adb46da277ac/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU=
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
Expand Down
10 changes: 4 additions & 6 deletions pkg/plugin/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"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/parsing"
"github.com/wildmountainfarms/wild-graphql-datasource/pkg/plugin/querymodel"
"github.com/wildmountainfarms/wild-graphql-datasource/pkg/plugin/queryvariables"
Expand Down Expand Up @@ -103,7 +102,7 @@ func statusFromResponse(response http.Response) backend.Status {
// In these cases, you can assume that something is seriously wrong, as we didn't intend to recover from that specific situation.
func (d *Datasource) query(ctx context.Context, pCtx backend.PluginContext, query backend.DataQuery) (*backend.DataResponse, error) {

log.DefaultLogger.Info(fmt.Sprintf("JSON is: %s", query.JSON))
//log.DefaultLogger.Info(fmt.Sprintf("JSON is: %s", query.JSON))

// Unmarshal the JSON into our QueryModel.
var qm querymodel.QueryModel
Expand All @@ -119,7 +118,7 @@ func (d *Datasource) query(ctx context.Context, pCtx backend.PluginContext, quer
Status: backend.StatusBadRequest,
}, nil
}
log.DefaultLogger.Info("Query text is: " + qm.QueryText)
//log.DefaultLogger.Info("Query text is: " + qm.QueryText)

// use later: pCtx.AppInstanceSettings.DecryptedSecureJSONData
variables, _ := queryvariables.ParseVariables(query, qm.Variables)
Expand Down Expand Up @@ -178,7 +177,7 @@ func (d *Datasource) query(ctx context.Context, pCtx backend.PluginContext, quer
// add the frames to the response.
for _, parsingOption := range qm.ParsingOptions {
frames, err, errorType := parsing.ParseData(
graphQLResponse.Data,
&graphQLResponse.Data,
parsingOption,
)
if err != nil {
Expand Down Expand Up @@ -246,8 +245,7 @@ func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRe
Message: "Something went wrong: " + resp.Status,
}, nil
}
_, schemaExists := graphQLResponse.Data["__schema"]
if !schemaExists {
if !graphQLResponse.Data.KeyExists("__schema") {
return &backend.CheckHealthResult{
Status: backend.HealthStatusError,
Message: "Unexpected GraphQL response!",
Expand Down
118 changes: 38 additions & 80 deletions pkg/plugin/parsing/framemap/framemap.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,90 +2,47 @@ package framemap

import (
"fmt"
"github.com/emirpasic/gods/v2/maps/linkedhashmap"
"github.com/grafana/grafana-plugin-sdk-go/data"
"hash/fnv"
"slices"
"sort"
)

type FrameAndLabels struct {
labels data.Labels
fieldMap map[string]interface{}
labels data.Labels
// A map of field names to an array of the values of that given column
fieldMap *linkedhashmap.Map[string, any]
}

func labelsEqual(labelsA data.Labels, labelsB data.Labels) bool {
if len(labelsA) != len(labelsB) {
return false
}
for key, value := range labelsA {
otherValue, exists := labelsB[key]
if !exists || value != otherValue {
return false
}
}
return true
}
func labelsHash(labels data.Labels) uint64 {
h := fnv.New64a()
// remember, iteration over entries in a map in go is not defined! (That's dumb! Why are you like this Go!)
keys := make([]string, 0, len(labels))
for key := range labels {
keys = append(keys, key)
}
slices.Sort(keys)
for _, key := range keys {
_, err := h.Write([]byte(key))
if err != nil {
panic(fmt.Sprintf("Error writing to hash: %v", err))
}
_, err = h.Write([]byte(labels[key]))
if err != nil {
panic(fmt.Sprintf("Error writing to hash: %v", err))
}
}
return h.Sum64()
func keyOfLabels(labels data.Labels) string {
return labels.String()
}

type FrameMap struct {
data map[uint64][]FrameAndLabels
data *linkedhashmap.Map[string, FrameAndLabels]
}

func CreateFrameMap() FrameMap {
return FrameMap{
data: map[uint64][]FrameAndLabels{},
func New() *FrameMap {
return &FrameMap{
data: linkedhashmap.New[string, FrameAndLabels](),
}
}

func (f *FrameMap) Get(labels data.Labels) (map[string]interface{}, bool) {
hash := labelsHash(labels)
values, exists := f.data[hash]
func (f *FrameMap) Get(labels data.Labels) (*linkedhashmap.Map[string, any], bool) {
mapKey := keyOfLabels(labels)
values, exists := f.data.Get(mapKey)
if !exists {
return nil, false
}

for _, value := range values {
if labelsEqual(value.labels, labels) {
return value.fieldMap, true
}
}
return nil, false
return values.fieldMap, true
}
func (f *FrameMap) Put(labels data.Labels, fieldMap map[string]interface{}) {
hash := labelsHash(labels)
values, exists := f.data[hash]
if !exists {
f.data[hash] = []FrameAndLabels{{labels: labels, fieldMap: fieldMap}}
return
}
for index, value := range values {
if labelsEqual(value.labels, labels) {
values[index] = FrameAndLabels{labels: labels, fieldMap: fieldMap}
return
}
}

newValues := append(values, FrameAndLabels{labels: labels, fieldMap: fieldMap})
f.data[hash] = newValues
func (f *FrameMap) Put(labels data.Labels, fieldMap *linkedhashmap.Map[string, any]) {
mapKey := keyOfLabels(labels)
f.data.Put(
mapKey,
FrameAndLabels{
labels: labels,
fieldMap: fieldMap,
},
)
}

func (f *FrameMap) ToFrames() []*data.Frame {
Expand All @@ -96,22 +53,23 @@ func (f *FrameMap) ToFrames() []*data.Frame {
// https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/transform-data/#prepare-time-series

// NOTE: The order of the frames here determines the order they appear in the legend in Grafana
// A workaround on the frontend is to make the legend in "Table" mode and then sort the "Name" column: https://github.com/grafana/grafana/pull/69490
// This is why we use a linkedhashmap.Map everywhere, as it maintains order.
var r []*data.Frame
for _, values := range f.data {
for _, value := range values {
frameName := fmt.Sprintf("response %v", value.labels)
frame := data.NewFrame(frameName)
for key, values := range value.fieldMap {
frame.Fields = append(frame.Fields,
data.NewField(key, value.labels, values),
)
}
r = append(r, frame)
frameMapIterator := f.data.Iterator()
for frameMapIterator.Next() {
frameAndLabels := frameMapIterator.Value()

frameName := fmt.Sprintf("response %v", frameAndLabels.labels)
frame := data.NewFrame(frameName)
fieldMapIterator := frameAndLabels.fieldMap.Iterator()
for fieldMapIterator.Next() {
key := fieldMapIterator.Key()
values := fieldMapIterator.Value()
frame.Fields = append(frame.Fields,
data.NewField(key, frameAndLabels.labels, values),
)
}
r = append(r, frame)
}
sort.Slice(r, func(i, j int) bool {
return r[i].Name < r[j].Name // sort alphabetically by name
})
return r
}
2 changes: 1 addition & 1 deletion pkg/plugin/parsing/framemap/framemap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func TestEqualityAndHash(t *testing.T) {
}

func TestFrameMap(t *testing.T) {
fm := CreateFrameMap()
fm := New()
labelsA := data.Labels{
"asdf": "a",
}
Expand Down
Loading

0 comments on commit 0e46ce1

Please sign in to comment.