-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixed errors with null values present
Can now serialize jsonnode.Node values. Fields are sorted in a "smart" way utilizing a topological sort. Data frames now only return json.RawMessage values or time.Time values. This (currently) has the downside of allowing string values to be interpreted numerically if the contents of a string are numeric.
- Loading branch information
1 parent
0eefc38
commit b33cb9c
Showing
15 changed files
with
2,534 additions
and
224 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package fieldsort | ||
|
||
type Order struct { | ||
data *graph[string] | ||
vertices map[string]bool | ||
} | ||
|
||
func New() *Order { | ||
return &Order{ | ||
data: &graph[string]{ | ||
edges: make(map[string][]string), | ||
}, | ||
vertices: make(map[string]bool), | ||
} | ||
} | ||
|
||
func (o *Order) State(ordering []string) { | ||
var last *string | ||
for _, element := range ordering { | ||
o.vertices[element] = true | ||
if last != nil { | ||
o.data.addEdge(*last, element) | ||
} | ||
|
||
elementCopy := element | ||
last = &elementCopy | ||
} | ||
} | ||
func (o *Order) GetOrder() []string { | ||
return o.data.topologicalSort(o.vertices) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package fieldsort | ||
|
||
import ( | ||
"strings" | ||
"testing" | ||
) | ||
|
||
func TestOrderRealistic(t *testing.T) { | ||
order := New() | ||
order.State([]string{"batteryVoltage", "dateMillis", "meta.name", "meta.displayName"}) | ||
order.State([]string{"batteryVoltage", "dateMillis", "meta"}) | ||
//println(strings.Join(order.GetOrder(), ", ")) | ||
} | ||
func TestOrderSimple(t *testing.T) { | ||
order := New() | ||
order.State([]string{"a", "b", "c", "f"}) | ||
order.State([]string{"b", "d", "e"}) | ||
order.State([]string{"c", "e", "f"}) | ||
//order.State([]string{"c", "d"}) | ||
result := order.GetOrder() | ||
//println(strings.Join(result, ", ")) | ||
if result[0] != "a" || result[1] != "b" || result[4] != "e" || result[5] != "f" { | ||
// Note that the order of c and d here is undetermined, which is OK | ||
t.Errorf("Incorrect result: %s", strings.Join(result, ", ")) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package fieldsort | ||
|
||
// inspiration from https://reintech.io/blog/topological-sorting-in-go | ||
|
||
type graph[Key comparable] struct { | ||
edges map[Key][]Key | ||
} | ||
|
||
func (g *graph[Key]) addEdge(u, v Key) { | ||
g.edges[u] = append(g.edges[u], v) | ||
} | ||
|
||
func (g *graph[Key]) topologicalSortUtil(v Key, visited map[Key]bool, stack *[]Key) { | ||
visited[v] = true | ||
|
||
for _, u := range g.edges[v] { | ||
if !visited[u] { | ||
g.topologicalSortUtil(u, visited, stack) | ||
} | ||
} | ||
|
||
*stack = append([]Key{v}, *stack...) | ||
} | ||
|
||
func (g *graph[Key]) topologicalSort(vertices map[Key]bool) []Key { | ||
var stack []Key | ||
visited := make(map[Key]bool) | ||
|
||
for vertex := range vertices { | ||
if !visited[vertex] { | ||
g.topologicalSortUtil(vertex, visited, &stack) | ||
} | ||
} | ||
|
||
return stack | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
package framemap | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"github.com/grafana/grafana-plugin-sdk-go/data" | ||
"github.com/wildmountainfarms/wild-graphql-datasource/pkg/plugin/parsing/fieldsort" | ||
"time" | ||
) | ||
|
||
func (node *frameNode) getAllFields() []string { | ||
order := fieldsort.New() | ||
|
||
for _, row := range node.rows { | ||
order.State(row.FieldOrder) | ||
} | ||
|
||
return order.GetOrder() | ||
} | ||
|
||
func (node *frameNode) isTimeField(field string) bool { | ||
for _, row := range node.rows { | ||
if _, exists := row.TimeMap[field]; exists { | ||
return true | ||
} | ||
if _, exists := row.FieldMap[field]; exists { | ||
return false | ||
} | ||
} | ||
return false | ||
} | ||
func (node *frameNode) createField(field string) *data.Field { | ||
if node.isTimeField(field) { | ||
var values []*time.Time | ||
for _, row := range node.rows { | ||
value, exists := row.TimeMap[field] | ||
if exists { | ||
values = append(values, value) | ||
} else { | ||
values = append(values, nil) | ||
} | ||
} | ||
return data.NewField(field, node.labels, values) | ||
} else { | ||
var values []*json.RawMessage | ||
for _, row := range node.rows { | ||
value, exists := row.FieldMap[field] | ||
if exists { | ||
values = append(values, &value) | ||
} else { | ||
values = append(values, nil) | ||
} | ||
} | ||
return data.NewField(field, node.labels, values) | ||
} | ||
} | ||
|
||
func (f *FrameMap) ToFrames() []*data.Frame { | ||
// create data frame response. | ||
// For an overview on data frames and how grafana handles them: | ||
// https://grafana.com/developers/plugin-tools/introduction/data-frames | ||
// The goal here is to output a long format. If needed, prepare time series can transform it | ||
// 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 | ||
// This is why we use a linkedhashmap.Map everywhere, as it maintains order. | ||
var r []*data.Frame | ||
frameMapIterator := f.data.Iterator() | ||
for frameMapIterator.Next() { | ||
node := frameMapIterator.Value() | ||
|
||
frameName := fmt.Sprintf("response %v", node.labels) | ||
frame := data.NewFrame(frameName) | ||
|
||
for _, field := range node.getAllFields() { | ||
frame.Fields = append(frame.Fields, node.createField(field)) | ||
} | ||
r = append(r, frame) | ||
} | ||
return r | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,75 +1,48 @@ | ||
package framemap | ||
|
||
import ( | ||
"fmt" | ||
"github.com/emirpasic/gods/v2/maps/linkedhashmap" | ||
"github.com/grafana/grafana-plugin-sdk-go/data" | ||
) | ||
|
||
type frameAndLabels struct { | ||
type frameNode struct { | ||
labels data.Labels | ||
// A map of field names to an array of the values of that given column | ||
fieldMap *linkedhashmap.Map[string, any] | ||
rows []*Row | ||
} | ||
|
||
func keyOfLabels(labels data.Labels) string { | ||
return labels.String() | ||
} | ||
|
||
type FrameMap struct { | ||
data *linkedhashmap.Map[string, frameAndLabels] | ||
data *linkedhashmap.Map[string, *frameNode] | ||
} | ||
|
||
func New() *FrameMap { | ||
return &FrameMap{ | ||
data: linkedhashmap.New[string, frameAndLabels](), | ||
data: linkedhashmap.New[string, *frameNode](), | ||
} | ||
} | ||
|
||
func (f *FrameMap) Get(labels data.Labels) (*linkedhashmap.Map[string, any], bool) { | ||
func (f *FrameMap) getOrCreateFrameNode(labels data.Labels) *frameNode { | ||
mapKey := keyOfLabels(labels) | ||
values, exists := f.data.Get(mapKey) | ||
if !exists { | ||
return nil, false | ||
if exists { | ||
return values | ||
} | ||
node := &frameNode{ | ||
labels: labels, | ||
} | ||
return values.fieldMap, true | ||
} | ||
func (f *FrameMap) Put(labels data.Labels, fieldMap *linkedhashmap.Map[string, any]) { | ||
mapKey := keyOfLabels(labels) | ||
f.data.Put( | ||
mapKey, | ||
frameAndLabels{ | ||
labels: labels, | ||
fieldMap: fieldMap, | ||
}, | ||
node, | ||
) | ||
return node | ||
} | ||
func (f *FrameMap) NewRow(labels data.Labels) *Row { | ||
node := f.getOrCreateFrameNode(labels) | ||
|
||
func (f *FrameMap) ToFrames() []*data.Frame { | ||
// create data frame response. | ||
// For an overview on data frames and how grafana handles them: | ||
// https://grafana.com/developers/plugin-tools/introduction/data-frames | ||
// The goal here is to output a long format. If needed, prepare time series can transform it | ||
// 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 | ||
// This is why we use a linkedhashmap.Map everywhere, as it maintains order. | ||
var r []*data.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) | ||
} | ||
return r | ||
row := newRow() | ||
node.rows = append(node.rows, row) | ||
return row | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package framemap | ||
|
||
import ( | ||
"encoding/json" | ||
"time" | ||
) | ||
|
||
type Row struct { | ||
FieldOrder []string | ||
FieldMap map[string]json.RawMessage | ||
TimeMap map[string]*time.Time | ||
} | ||
|
||
func newRow() *Row { | ||
row := Row{ | ||
FieldMap: map[string]json.RawMessage{}, | ||
TimeMap: map[string]*time.Time{}, | ||
} | ||
return &row | ||
} |
Oops, something went wrong.