mirror of https://github.com/grafana/grafana
TestData: Support for csv files & csv content (#34674)
* initial implementation of csv support for test data source * CSV file & content scenarios working * Removing categorical data * fixing handler names * Update pkg/tsdb/testdatasource/csv_data.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/tsdb/testdatasource/csv_data.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/tsdb/testdatasource/csv_data.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/tsdb/testdatasource/csv_data.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/tsdb/testdatasource/csv_data.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/tsdb/testdatasource/csv_data.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Update pkg/tsdb/testdatasource/csv_data.go Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> * Fixed lint issues * updated so it uses the same parsing * more CSV tests * lint fixes * more lint * lint * support time field * migrate manual entry to csv * more test output * more test output * missing file Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com> Co-authored-by: Ryan McKinley <ryantxu@gmail.com>pull/34712/head
parent
b4ce068f0e
commit
987bffe482
@ -0,0 +1,58 @@ |
||||
🌟 This was machine generated. Do not edit. 🌟 |
||||
|
||||
Frame[0] {} |
||||
Name: ingress |
||||
Dimensions: 2 Fields by 1 Rows |
||||
+-------------------------------+---------------------------------+ |
||||
| Name: _time | Name: file_size | |
||||
| Labels: | Labels: dead=true, train=350117 | |
||||
| Type: []*time.Time | Type: []*int64 | |
||||
+-------------------------------+---------------------------------+ |
||||
| 2020-11-09 17:27:00 +0000 UTC | 3339 | |
||||
+-------------------------------+---------------------------------+ |
||||
|
||||
|
||||
|
||||
Frame[1] |
||||
Name: ingress |
||||
Dimensions: 2 Fields by 1 Rows |
||||
+-------------------------------+---------------------------------+ |
||||
| Name: _time | Name: file_size | |
||||
| Labels: | Labels: dead=true, train=350125 | |
||||
| Type: []*time.Time | Type: []*int64 | |
||||
+-------------------------------+---------------------------------+ |
||||
| 2020-11-10 12:29:00 +0000 UTC | 3666 | |
||||
+-------------------------------+---------------------------------+ |
||||
|
||||
|
||||
|
||||
Frame[2] |
||||
Name: ingress |
||||
Dimensions: 2 Fields by 1 Rows |
||||
+-------------------------------+---------------------------------+ |
||||
| Name: _time | Name: file_size | |
||||
| Labels: | Labels: dead=true, train=350236 | |
||||
| Type: []*time.Time | Type: []*int64 | |
||||
+-------------------------------+---------------------------------+ |
||||
| 2020-11-09 19:01:00 +0000 UTC | 3570 | |
||||
+-------------------------------+---------------------------------+ |
||||
|
||||
|
||||
|
||||
Frame[3] |
||||
Name: ingress |
||||
Dimensions: 2 Fields by 1 Rows |
||||
+-------------------------------+---------------------------------+ |
||||
| Name: _time | Name: file_size | |
||||
| Labels: | Labels: dead=true, train=350410 | |
||||
| Type: []*time.Time | Type: []*int64 | |
||||
+-------------------------------+---------------------------------+ |
||||
| 2020-11-09 07:13:00 +0000 UTC | 2772 | |
||||
+-------------------------------+---------------------------------+ |
||||
|
||||
|
||||
====== TEST DATA RESPONSE (arrow base64) ====== |
||||
FRAME=QVJST1cxAAD/////6AEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAHgAAAADAAAAUAAAACgAAAAEAAAArP7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAADM/v//CAAAABAAAAAHAAAAaW5ncmVzcwAEAAAAbmFtZQAAAADw/v//CAAAAAwAAAACAAAAe30AAAQAAABtZXRhAAAAAAIAAADMAAAABAAAAE7///8UAAAAhAAAAIwAAAAAAAIBkAAAAAIAAAAwAAAABAAAAED///8IAAAAFAAAAAkAAABmaWxlX3NpemUAAAAEAAAAbmFtZQAAAABo////CAAAACwAAAAgAAAAeyJkZWFkIjoidHJ1ZSIsInRyYWluIjoiMzUwMTE3In0AAAAABgAAAGxhYmVscwAAAAAAAAgADAAIAAcACAAAAAAAAAFAAAAACQAAAGZpbGVfc2l6ZQASABgAFAATABIADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAKAUwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABQAAAF90aW1lAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAFAAAAX3RpbWUAAAAAAAAA/////7gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAAQAAAAAAAAABQAAAAAAAADAwAKABgADAAIAAQACgAAABQAAABYAAAAAQAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAOjpzv3mRRYLDQAAAAAAABAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA4AAAAAAADAAEAAAD4AQAAAAAAAMAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAeAAAAAMAAABQAAAAKAAAAAQAAACs/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAMz+//8IAAAAEAAAAAcAAABpbmdyZXNzAAQAAABuYW1lAAAAAPD+//8IAAAADAAAAAIAAAB7fQAABAAAAG1ldGEAAAAAAgAAAMwAAAAEAAAATv///xQAAACEAAAAjAAAAAAAAgGQAAAAAgAAADAAAAAEAAAAQP///wgAAAAUAAAACQAAAGZpbGVfc2l6ZQAAAAQAAABuYW1lAAAAAGj///8IAAAALAAAACAAAAB7ImRlYWQiOiJ0cnVlIiwidHJhaW4iOiIzNTAxMTcifQAAAAAGAAAAbGFiZWxzAAAAAAAACAAMAAgABwAIAAAAAAAAAUAAAAAJAAAAZmlsZV9zaXplABIAGAAUABMAEgAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAoBTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAFAAAAX3RpbWUAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAUAAABfdGltZQAAABACAABBUlJPVzE= |
||||
FRAME=QVJST1cxAAD/////wAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFQAAAACAAAAKAAAAAQAAADM/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAOz+//8IAAAAEAAAAAcAAABpbmdyZXNzAAQAAABuYW1lAAAAAAIAAADMAAAABAAAAE7///8UAAAAhAAAAIwAAAAAAAIBkAAAAAIAAAAwAAAABAAAAED///8IAAAAFAAAAAkAAABmaWxlX3NpemUAAAAEAAAAbmFtZQAAAABo////CAAAACwAAAAgAAAAeyJkZWFkIjoidHJ1ZSIsInRyYWluIjoiMzUwMTI1In0AAAAABgAAAGxhYmVscwAAAAAAAAgADAAIAAcACAAAAAAAAAFAAAAACQAAAGZpbGVfc2l6ZQASABgAFAATABIADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAKAUwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABQAAAF90aW1lAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAFAAAAX3RpbWUAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAABAAAAAAAAAAFAAAAAAAAAMDAAoAGAAMAAgABAAKAAAAFAAAAFgAAAABAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAeCxdTyVGFlIOAAAAAAAAEAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADwAAAAAAAMAAQAAANABAAAAAAAAwAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAVAAAAAIAAAAoAAAABAAAAMz+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAA7P7//wgAAAAQAAAABwAAAGluZ3Jlc3MABAAAAG5hbWUAAAAAAgAAAMwAAAAEAAAATv///xQAAACEAAAAjAAAAAAAAgGQAAAAAgAAADAAAAAEAAAAQP///wgAAAAUAAAACQAAAGZpbGVfc2l6ZQAAAAQAAABuYW1lAAAAAGj///8IAAAALAAAACAAAAB7ImRlYWQiOiJ0cnVlIiwidHJhaW4iOiIzNTAxMjUifQAAAAAGAAAAbGFiZWxzAAAAAAAACAAMAAgABwAIAAAAAAAAAUAAAAAJAAAAZmlsZV9zaXplABIAGAAUABMAEgAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAoBTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAFAAAAX3RpbWUAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAUAAABfdGltZQAAAPABAABBUlJPVzE= |
||||
FRAME=QVJST1cxAAD/////wAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFQAAAACAAAAKAAAAAQAAADM/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAOz+//8IAAAAEAAAAAcAAABpbmdyZXNzAAQAAABuYW1lAAAAAAIAAADMAAAABAAAAE7///8UAAAAhAAAAIwAAAAAAAIBkAAAAAIAAAAwAAAABAAAAED///8IAAAAFAAAAAkAAABmaWxlX3NpemUAAAAEAAAAbmFtZQAAAABo////CAAAACwAAAAgAAAAeyJkZWFkIjoidHJ1ZSIsInRyYWluIjoiMzUwMjM2In0AAAAABgAAAGxhYmVscwAAAAAAAAgADAAIAAcACAAAAAAAAAFAAAAACQAAAGZpbGVfc2l6ZQASABgAFAATABIADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAKAUwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABQAAAF90aW1lAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAFAAAAX3RpbWUAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAABAAAAAAAAAAFAAAAAAAAAMDAAoAGAAMAAgABAAKAAAAFAAAAFgAAAABAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAOBz5HuxFFvINAAAAAAAAEAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADwAAAAAAAMAAQAAANABAAAAAAAAwAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAVAAAAAIAAAAoAAAABAAAAMz+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAA7P7//wgAAAAQAAAABwAAAGluZ3Jlc3MABAAAAG5hbWUAAAAAAgAAAMwAAAAEAAAATv///xQAAACEAAAAjAAAAAAAAgGQAAAAAgAAADAAAAAEAAAAQP///wgAAAAUAAAACQAAAGZpbGVfc2l6ZQAAAAQAAABuYW1lAAAAAGj///8IAAAALAAAACAAAAB7ImRlYWQiOiJ0cnVlIiwidHJhaW4iOiIzNTAyMzYifQAAAAAGAAAAbGFiZWxzAAAAAAAACAAMAAgABwAIAAAAAAAAAUAAAAAJAAAAZmlsZV9zaXplABIAGAAUABMAEgAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAoBTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAFAAAAX3RpbWUAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAUAAABfdGltZQAAAPABAABBUlJPVzE= |
||||
FRAME=QVJST1cxAAD/////wAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFQAAAACAAAAKAAAAAQAAADM/v//CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAAOz+//8IAAAAEAAAAAcAAABpbmdyZXNzAAQAAABuYW1lAAAAAAIAAADMAAAABAAAAE7///8UAAAAhAAAAIwAAAAAAAIBkAAAAAIAAAAwAAAABAAAAED///8IAAAAFAAAAAkAAABmaWxlX3NpemUAAAAEAAAAbmFtZQAAAABo////CAAAACwAAAAgAAAAeyJkZWFkIjoidHJ1ZSIsInRyYWluIjoiMzUwNDEwIn0AAAAABgAAAGxhYmVscwAAAAAAAAgADAAIAAcACAAAAAAAAAFAAAAACQAAAGZpbGVfc2l6ZQASABgAFAATABIADAAAAAgABAASAAAAFAAAAEQAAABMAAAAAAAKAUwAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABQAAAF90aW1lAAAABAAAAG5hbWUAAAAAAAAAAAAABgAIAAYABgAAAAAAAwAFAAAAX3RpbWUAAAD/////uAAAABQAAAAAAAAADAAWABQAEwAMAAQADAAAABAAAAAAAAAAFAAAAAAAAAMDAAoAGAAMAAgABAAKAAAAFAAAAFgAAAABAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAAAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAA2MxTfMVFFtQKAAAAAAAAEAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADwAAAAAAAMAAQAAANABAAAAAAAAwAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAACgAMAAAACAAEAAoAAAAIAAAAVAAAAAIAAAAoAAAABAAAAMz+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAA7P7//wgAAAAQAAAABwAAAGluZ3Jlc3MABAAAAG5hbWUAAAAAAgAAAMwAAAAEAAAATv///xQAAACEAAAAjAAAAAAAAgGQAAAAAgAAADAAAAAEAAAAQP///wgAAAAUAAAACQAAAGZpbGVfc2l6ZQAAAAQAAABuYW1lAAAAAGj///8IAAAALAAAACAAAAB7ImRlYWQiOiJ0cnVlIiwidHJhaW4iOiIzNTA0MTAifQAAAAAGAAAAbGFiZWxzAAAAAAAACAAMAAgABwAIAAAAAAAAAUAAAAAJAAAAZmlsZV9zaXplABIAGAAUABMAEgAMAAAACAAEABIAAAAUAAAARAAAAEwAAAAAAAoBTAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAFAAAAX3RpbWUAAAAEAAAAbmFtZQAAAAAAAAAAAAAGAAgABgAGAAAAAAADAAUAAABfdGltZQAAAPABAABBUlJPVzE= |
||||
@ -0,0 +1,276 @@ |
||||
package testdatasource |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/csv" |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
"os" |
||||
"path/filepath" |
||||
"regexp" |
||||
"strconv" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend" |
||||
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||
"github.com/grafana/grafana/pkg/components/simplejson" |
||||
) |
||||
|
||||
func (p *testDataPlugin) handleCsvContentScenario(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { |
||||
resp := backend.NewQueryDataResponse() |
||||
|
||||
for _, q := range req.Queries { |
||||
model, err := simplejson.NewJson(q.JSON) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed to parse query json: %v", err) |
||||
} |
||||
|
||||
csvContent := model.Get("csvContent").MustString() |
||||
alias := model.Get("alias").MustString(q.RefID) |
||||
|
||||
frame, err := p.loadCsvContent(strings.NewReader(csvContent), alias) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
respD := resp.Responses[q.RefID] |
||||
respD.Frames = append(respD.Frames, frame) |
||||
resp.Responses[q.RefID] = respD |
||||
} |
||||
|
||||
return resp, nil |
||||
} |
||||
|
||||
func (p *testDataPlugin) handleCsvFileScenario(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { |
||||
resp := backend.NewQueryDataResponse() |
||||
|
||||
for _, q := range req.Queries { |
||||
model, err := simplejson.NewJson(q.JSON) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed to parse query json %v", err) |
||||
} |
||||
|
||||
fileName := model.Get("csvFileName").MustString() |
||||
|
||||
if len(fileName) == 0 { |
||||
continue |
||||
} |
||||
|
||||
frame, err := p.loadCsvFile(fileName) |
||||
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
respD := resp.Responses[q.RefID] |
||||
respD.Frames = append(respD.Frames, frame) |
||||
resp.Responses[q.RefID] = respD |
||||
} |
||||
|
||||
return resp, nil |
||||
} |
||||
|
||||
func (p *testDataPlugin) loadCsvFile(fileName string) (*data.Frame, error) { |
||||
validFileName := regexp.MustCompile(`([\w_]+)\.csv`) |
||||
|
||||
if !validFileName.MatchString(fileName) { |
||||
return nil, fmt.Errorf("invalid csv file name: %q", fileName) |
||||
} |
||||
|
||||
filePath := filepath.Join(p.Cfg.StaticRootPath, "testdata", fileName) |
||||
|
||||
// Can ignore gosec G304 here, because we check the file pattern above
|
||||
// nolint:gosec
|
||||
fileReader, err := os.Open(filePath) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed open file: %v", err) |
||||
} |
||||
|
||||
defer func() { |
||||
if err := fileReader.Close(); err != nil { |
||||
p.logger.Warn("Failed to close file", "err", err, "path", fileName) |
||||
} |
||||
}() |
||||
|
||||
return p.loadCsvContent(fileReader, fileName) |
||||
} |
||||
|
||||
func (p *testDataPlugin) loadCsvContent(ioReader io.Reader, name string) (*data.Frame, error) { |
||||
reader := csv.NewReader(ioReader) |
||||
|
||||
// Read the header records
|
||||
headerFields, err := reader.Read() |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed to read header line: %v", err) |
||||
} |
||||
|
||||
fields := []*data.Field{} |
||||
fieldNames := []string{} |
||||
fieldRawValues := [][]string{} |
||||
|
||||
for _, fieldName := range headerFields { |
||||
fieldNames = append(fieldNames, strings.Trim(fieldName, " ")) |
||||
fieldRawValues = append(fieldRawValues, []string{}) |
||||
} |
||||
|
||||
for { |
||||
lineValues, err := reader.Read() |
||||
if errors.Is(err, io.EOF) { |
||||
break // reached end of the file
|
||||
} else if err != nil { |
||||
return nil, fmt.Errorf("failed to read line: %v", err) |
||||
} |
||||
|
||||
for fieldIndex, value := range lineValues { |
||||
fieldRawValues[fieldIndex] = append(fieldRawValues[fieldIndex], strings.Trim(value, " ")) |
||||
} |
||||
} |
||||
|
||||
longest := 0 |
||||
for fieldIndex, rawValues := range fieldRawValues { |
||||
fieldName := fieldNames[fieldIndex] |
||||
field, err := csvValuesToField(rawValues) |
||||
if err == nil { |
||||
// Check if the values are actually a time field
|
||||
if strings.Contains(strings.ToLower(fieldName), "time") { |
||||
timeField := toTimeField(field) |
||||
if timeField != nil { |
||||
field = timeField |
||||
} |
||||
} |
||||
|
||||
field.Name = fieldName |
||||
fields = append(fields, field) |
||||
if field.Len() > longest { |
||||
longest = field.Len() |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Make all fields the same length
|
||||
for _, field := range fields { |
||||
delta := field.Len() - longest |
||||
if delta > 0 { |
||||
field.Extend(delta) |
||||
} |
||||
} |
||||
|
||||
frame := data.NewFrame(name, fields...) |
||||
return frame, nil |
||||
} |
||||
|
||||
func csvLineToField(stringInput string) (*data.Field, error) { |
||||
return csvValuesToField(strings.Split(strings.ReplaceAll(stringInput, " ", ""), ",")) |
||||
} |
||||
|
||||
func csvValuesToField(parts []string) (*data.Field, error) { |
||||
if len(parts) < 1 { |
||||
return nil, fmt.Errorf("csv must have at least one value") |
||||
} |
||||
|
||||
first := strings.ToUpper(parts[0]) |
||||
if first == "T" || first == "F" || first == "TRUE" || first == "FALSE" { |
||||
field := data.NewFieldFromFieldType(data.FieldTypeNullableBool, len(parts)) |
||||
for idx, strVal := range parts { |
||||
strVal = strings.ToUpper(strVal) |
||||
if strVal == "NULL" || strVal == "" { |
||||
continue |
||||
} |
||||
field.SetConcrete(idx, strVal == "T" || strVal == "TRUE") |
||||
} |
||||
return field, nil |
||||
} |
||||
|
||||
// Try parsing values as numbers
|
||||
ok := false |
||||
field := data.NewFieldFromFieldType(data.FieldTypeNullableInt64, len(parts)) |
||||
for idx, strVal := range parts { |
||||
if strVal == "null" || strVal == "" { |
||||
continue |
||||
} |
||||
|
||||
val, err := strconv.ParseInt(strVal, 10, 64) |
||||
if err != nil { |
||||
ok = false |
||||
break |
||||
} |
||||
field.SetConcrete(idx, val) |
||||
ok = true |
||||
} |
||||
if ok { |
||||
return field, nil |
||||
} |
||||
|
||||
// Maybe floats
|
||||
field = data.NewFieldFromFieldType(data.FieldTypeNullableFloat64, len(parts)) |
||||
for idx, strVal := range parts { |
||||
if strVal == "null" || strVal == "" { |
||||
continue |
||||
} |
||||
|
||||
val, err := strconv.ParseFloat(strVal, 64) |
||||
if err != nil { |
||||
ok = false |
||||
break |
||||
} |
||||
field.SetConcrete(idx, val) |
||||
ok = true |
||||
} |
||||
if ok { |
||||
return field, nil |
||||
} |
||||
|
||||
// Replace empty strings with null
|
||||
field = data.NewFieldFromFieldType(data.FieldTypeNullableString, len(parts)) |
||||
for idx, strVal := range parts { |
||||
if strVal == "null" || strVal == "" { |
||||
continue |
||||
} |
||||
field.SetConcrete(idx, strVal) |
||||
} |
||||
return field, nil |
||||
} |
||||
|
||||
// This will try to convert the values to a timestamp
|
||||
func toTimeField(field *data.Field) *data.Field { |
||||
found := false |
||||
count := field.Len() |
||||
timeField := data.NewFieldFromFieldType(data.FieldTypeNullableTime, count) |
||||
timeField.Config = field.Config |
||||
timeField.Name = field.Name |
||||
timeField.Labels = field.Labels |
||||
ft := field.Type() |
||||
if ft.Numeric() { |
||||
for i := 0; i < count; i++ { |
||||
v, err := field.FloatAt(i) |
||||
if err == nil { |
||||
t := time.Unix(0, int64(v)*int64(time.Millisecond)) |
||||
timeField.SetConcrete(i, t.UTC()) |
||||
found = true |
||||
} |
||||
} |
||||
if !found { |
||||
return nil |
||||
} |
||||
return timeField |
||||
} |
||||
if ft == data.FieldTypeNullableString || ft == data.FieldTypeString { |
||||
for i := 0; i < count; i++ { |
||||
v, ok := field.ConcreteAt(i) |
||||
if ok && v != nil { |
||||
t, err := time.Parse(time.RFC3339, v.(string)) |
||||
if err == nil { |
||||
timeField.SetConcrete(i, t.UTC()) |
||||
found = true |
||||
} |
||||
} |
||||
} |
||||
if !found { |
||||
return nil |
||||
} |
||||
return timeField |
||||
} |
||||
return nil |
||||
} |
||||
@ -0,0 +1,107 @@ |
||||
package testdatasource |
||||
|
||||
import ( |
||||
"os" |
||||
"path/filepath" |
||||
"testing" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend" |
||||
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||
"github.com/grafana/grafana-plugin-sdk-go/experimental" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestCSVFileScenario(t *testing.T) { |
||||
cfg := setting.NewCfg() |
||||
cfg.DataPath = t.TempDir() |
||||
cfg.StaticRootPath = "../../../public" |
||||
|
||||
p := &testDataPlugin{ |
||||
Cfg: cfg, |
||||
} |
||||
|
||||
t.Run("loadCsvFile", func(t *testing.T) { |
||||
files := []string{"population_by_state.csv", "city_stats.csv"} |
||||
for _, name := range files { |
||||
t.Run("Should load file and convert to DataFrame", func(t *testing.T) { |
||||
frame, err := p.loadCsvFile(name) |
||||
require.NoError(t, err) |
||||
require.NotNil(t, frame) |
||||
|
||||
dr := &backend.DataResponse{ |
||||
Frames: data.Frames{frame}, |
||||
} |
||||
err = experimental.CheckGoldenDataResponse( |
||||
filepath.Join("testdata", name+".golden.txt"), dr, true, |
||||
) |
||||
require.NoError(t, err) |
||||
}) |
||||
} |
||||
|
||||
files = []string{"simple", "mixed"} |
||||
for _, name := range files { |
||||
t.Run("Should load CSV Text: "+name, func(t *testing.T) { |
||||
filePath := filepath.Join("testdata", name+".csv") |
||||
// Can ignore gosec G304 here, because this is a constant defined above
|
||||
// nolint:gosec
|
||||
fileReader, err := os.Open(filePath) |
||||
require.NoError(t, err) |
||||
|
||||
defer func() { |
||||
_ = fileReader.Close() |
||||
}() |
||||
|
||||
frame, err := p.loadCsvContent(fileReader, name) |
||||
require.NoError(t, err) |
||||
require.NotNil(t, frame) |
||||
|
||||
dr := &backend.DataResponse{ |
||||
Frames: data.Frames{frame}, |
||||
} |
||||
err = experimental.CheckGoldenDataResponse( |
||||
filepath.Join("testdata", name+".golden.txt"), dr, true, |
||||
) |
||||
require.NoError(t, err) |
||||
}) |
||||
} |
||||
|
||||
t.Run("Should not allow non file name chars", func(t *testing.T) { |
||||
_, err := p.loadCsvFile("../population_by_state.csv") |
||||
require.Error(t, err) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func TestReadCSV(t *testing.T) { |
||||
fBool, err := csvLineToField("T, F,F,T ,") |
||||
require.NoError(t, err) |
||||
|
||||
fBool2, err := csvLineToField("true,false,T,F,F") |
||||
require.NoError(t, err) |
||||
|
||||
fNum, err := csvLineToField("1,null,,4,5") |
||||
require.NoError(t, err) |
||||
|
||||
fStr, err := csvLineToField("a,b,,,c") |
||||
require.NoError(t, err) |
||||
|
||||
frame := data.NewFrame("", fBool, fBool2, fNum, fStr) |
||||
frameToJSON, err := data.FrameToJSON(frame) |
||||
require.NoError(t, err) |
||||
out := frameToJSON.Bytes(data.IncludeAll) |
||||
|
||||
require.JSONEq(t, `{"schema":{ |
||||
"fields":[ |
||||
{"type":"boolean","typeInfo":{"frame":"bool","nullable":true}}, |
||||
{"type":"boolean","typeInfo":{"frame":"bool","nullable":true}}, |
||||
{"type":"number","typeInfo":{"frame":"int64","nullable":true}}, |
||||
{"type":"string","typeInfo":{"frame":"string","nullable":true}} |
||||
]},"data":{ |
||||
"values":[ |
||||
[true,false,false,true,null], |
||||
[true,false,true,false,false], |
||||
[1,null,null,4,5], |
||||
["a","b",null,null,"c"] |
||||
]}}`, string(out)) |
||||
} |
||||
@ -0,0 +1,17 @@ |
||||
🌟 This was machine generated. Do not edit. 🌟 |
||||
|
||||
Frame[0] |
||||
Name: city_stats.csv |
||||
Dimensions: 2 Fields by 2 Rows |
||||
+-----------------+------------------+ |
||||
| Name: City | Name: Population | |
||||
| Labels: | Labels: | |
||||
| Type: []*string | Type: []*int64 | |
||||
+-----------------+------------------+ |
||||
| Stockholm | 1000000 | |
||||
| New York | 13333300 | |
||||
+-----------------+------------------+ |
||||
|
||||
|
||||
====== TEST DATA RESPONSE (arrow base64) ====== |
||||
FRAME=QVJST1cxAAD/////gAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEDAAoADAAAAAgABAAKAAAACAAAAFwAAAACAAAAKAAAAAQAAAAE////CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAACT///8IAAAAGAAAAA4AAABjaXR5X3N0YXRzLmNzdgAABAAAAG5hbWUAAAAAAgAAAIwAAAAEAAAAjv///xQAAABAAAAASAAAAAAAAgFMAAAAAQAAAAQAAAB8////CAAAABQAAAAKAAAAUG9wdWxhdGlvbgAABAAAAG5hbWUAAAAAAAAAAAgADAAIAAcACAAAAAAAAAFAAAAACgAAAFBvcHVsYXRpb24AAAAAEgAYABQAEwASAAwAAAAIAAQAEgAAABQAAABEAAAASAAAAAAABQFEAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABDaXR5AAAAAAQAAABuYW1lAAAAAAAAAAAEAAQABAAAAAQAAABDaXR5AAAAAP/////IAAAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAOAAAAAAAAAAUAAAAAAAAAwMACgAYAAwACAAEAAoAAAAUAAAAaAAAAAIAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAGAAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAQAAAAAAAAAAAAAAACAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAACQAAABEAAAAAAAAAU3RvY2tob2xtTmV3IFlvcmsAAAAAAAAAQEIPAAAAAAA0c8sAAAAAABAAAAAMABQAEgAMAAgABAAMAAAAEAAAACwAAAA8AAAAAAADAAEAAACQAQAAAAAAANAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoADAAAAAgABAAKAAAACAAAAFwAAAACAAAAKAAAAAQAAAAE////CAAAAAwAAAAAAAAAAAAAAAUAAAByZWZJZAAAACT///8IAAAAGAAAAA4AAABjaXR5X3N0YXRzLmNzdgAABAAAAG5hbWUAAAAAAgAAAIwAAAAEAAAAjv///xQAAABAAAAASAAAAAAAAgFMAAAAAQAAAAQAAAB8////CAAAABQAAAAKAAAAUG9wdWxhdGlvbgAABAAAAG5hbWUAAAAAAAAAAAgADAAIAAcACAAAAAAAAAFAAAAACgAAAFBvcHVsYXRpb24AAAAAEgAYABQAEwASAAwAAAAIAAQAEgAAABQAAABEAAAASAAAAAAABQFEAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABDaXR5AAAAAAQAAABuYW1lAAAAAAAAAAAEAAQABAAAAAQAAABDaXR5AAAAALABAABBUlJPVzE= |
||||
|
@ -0,0 +1,17 @@ |
||||
🌟 This was machine generated. Do not edit. 🌟 |
||||
|
||||
Frame[0] |
||||
Name: mixed |
||||
Dimensions: 4 Fields by 2 Rows |
||||
+---------------+-----------------+-----------------+----------------+ |
||||
| Name: Field1 | Name: Field2 | Name: Field3 | Name: 123 | |
||||
| Labels: | Labels: | Labels: | Labels: | |
||||
| Type: []*bool | Type: []*string | Type: []*string | Type: []*int64 | |
||||
+---------------+-----------------+-----------------+----------------+ |
||||
| true | Hello | 6 | null | |
||||
| false | 6 | World | 6 | |
||||
+---------------+-----------------+-----------------+----------------+ |
||||
|
||||
|
||||
====== TEST DATA RESPONSE (arrow base64) ====== |
||||
FRAME=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 |
||||
@ -0,0 +1,18 @@ |
||||
🌟 This was machine generated. Do not edit. 🌟 |
||||
|
||||
Frame[0] |
||||
Name: population_by_state.csv |
||||
Dimensions: 4 Fields by 3 Rows |
||||
+-----------------+----------------+----------------+----------------+ |
||||
| Name: State | Name: 2020 | Name: 2000 | Name: 1980 | |
||||
| Labels: | Labels: | Labels: | Labels: | |
||||
| Type: []*string | Type: []*int64 | Type: []*int64 | Type: []*int64 | |
||||
+-----------------+----------------+----------------+----------------+ |
||||
| California | 39368078 | 33987977 | 23800800 | |
||||
| Texas | 29360759 | 20944499 | 14338208 | |
||||
| Florida | 21733312 | 16047515 | 9839835 | |
||||
+-----------------+----------------+----------------+----------------+ |
||||
|
||||
|
||||
====== TEST DATA RESPONSE (arrow base64) ====== |
||||
FRAME=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 |
||||
|
@ -0,0 +1,17 @@ |
||||
🌟 This was machine generated. Do not edit. 🌟 |
||||
|
||||
Frame[0] |
||||
Name: simple |
||||
Dimensions: 5 Fields by 2 Rows |
||||
+-----------------+----------------+----------------+------------------+-------------------------------+ |
||||
| Name: Field1 | Name: Field2 | Name: Field3 | Name: Float | Name: Time | |
||||
| Labels: | Labels: | Labels: | Labels: | Labels: | |
||||
| Type: []*string | Type: []*int64 | Type: []*int64 | Type: []*float64 | Type: []*time.Time | |
||||
+-----------------+----------------+----------------+------------------+-------------------------------+ |
||||
| A | 5 | 6 | 6.7 | 2021-05-25 23:56:40 +0000 UTC | |
||||
| B | 6 | 7 | 8.9 | 2021-05-26 00:13:20 +0000 UTC | |
||||
+-----------------+----------------+----------------+------------------+-------------------------------+ |
||||
|
||||
|
||||
====== TEST DATA RESPONSE (arrow base64) ====== |
||||
FRAME=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 |
||||
@ -0,0 +1,21 @@ |
||||
import React, { ChangeEvent } from 'react'; |
||||
import { InlineField, TextArea } from '@grafana/ui'; |
||||
import { EditorProps } from '../QueryEditor'; |
||||
|
||||
export const CSVContentEditor = ({ onChange, query }: EditorProps) => { |
||||
const onContent = (e: ChangeEvent<HTMLTextAreaElement>) => { |
||||
onChange({ ...query, csvContent: e.currentTarget.value }); |
||||
}; |
||||
|
||||
return ( |
||||
<InlineField label="CSV" labelWidth={14}> |
||||
<TextArea |
||||
width="100%" |
||||
rows={10} |
||||
onBlur={onContent} |
||||
placeholder="CSV content" |
||||
defaultValue={query.csvContent ?? ''} |
||||
/> |
||||
</InlineField> |
||||
); |
||||
}; |
||||
@ -0,0 +1,26 @@ |
||||
import React from 'react'; |
||||
import { InlineField, InlineFieldRow, Select } from '@grafana/ui'; |
||||
import { SelectableValue } from '@grafana/data'; |
||||
import { EditorProps } from '../QueryEditor'; |
||||
|
||||
export const CSVFileEditor = ({ onChange, query }: EditorProps) => { |
||||
const onChangeFileName = ({ value }: SelectableValue<string>) => { |
||||
onChange({ ...query, csvFileName: value }); |
||||
}; |
||||
|
||||
const files = ['population_by_state.csv', 'city_stats.csv'].map((name) => ({ label: name, value: name })); |
||||
|
||||
return ( |
||||
<InlineFieldRow> |
||||
<InlineField label="File" labelWidth={14}> |
||||
<Select |
||||
width={32} |
||||
onChange={onChangeFileName} |
||||
placeholder="Select csv file" |
||||
options={files} |
||||
value={files.find((f) => f.value === query.csvFileName)} |
||||
/> |
||||
</InlineField> |
||||
</InlineFieldRow> |
||||
); |
||||
}; |
||||
@ -1,87 +0,0 @@ |
||||
import React from 'react'; |
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'; |
||||
import userEvent from '@testing-library/user-event'; |
||||
import { ManualEntryEditor, Props } from './ManualEntryEditor'; |
||||
import { defaultQuery } from '../constants'; |
||||
|
||||
beforeEach(() => { |
||||
jest.clearAllMocks(); |
||||
}); |
||||
|
||||
const mockOnChange = jest.fn(); |
||||
const setup = (testProps?: Partial<Props>) => { |
||||
const props = { |
||||
onRunQuery: jest.fn(), |
||||
query: defaultQuery, |
||||
onChange: mockOnChange, |
||||
...testProps, |
||||
}; |
||||
|
||||
return render(<ManualEntryEditor {...props} />); |
||||
}; |
||||
|
||||
describe('ManualEntryEditor', () => { |
||||
it('should render', () => { |
||||
setup(); |
||||
|
||||
expect(screen.getByLabelText(/New value/i)).toBeInTheDocument(); |
||||
expect(screen.getByLabelText(/Time/i)).toBeInTheDocument(); |
||||
expect(screen.getByRole('button', { name: /Add/i })).toBeInTheDocument(); |
||||
expect(screen.getByText(/select point/i)).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('should add new point', async () => { |
||||
setup(); |
||||
|
||||
userEvent.type(screen.getByLabelText(/New value/i), '10'); |
||||
userEvent.clear(screen.getByLabelText(/Time/i)); |
||||
userEvent.type(screen.getByLabelText(/Time/i), '2020-11-01T14:19:30+00:00'); |
||||
userEvent.click(screen.getByRole('button', { name: /Add/i })); |
||||
|
||||
await waitFor(() => { |
||||
expect(mockOnChange).toHaveBeenCalledWith(expect.objectContaining({ points: [[10, 1604240370000]] })); |
||||
}); |
||||
}); |
||||
|
||||
it('should list selected points and delete selected ones', async () => { |
||||
const editor = setup({ |
||||
query: { |
||||
...defaultQuery, |
||||
points: [ |
||||
[10, 1604240370000], |
||||
[15, 1604340370000], |
||||
], |
||||
}, |
||||
}); |
||||
let select = screen.getByText('All values').nextSibling!; |
||||
await fireEvent.keyDown(select, { keyCode: 40 }); |
||||
const points = screen.getAllByLabelText('Select option'); |
||||
expect(points).toHaveLength(2); |
||||
expect(screen.queryByRole('button', { name: 'Delete' })).not.toBeInTheDocument(); |
||||
|
||||
await userEvent.click(points[0]); |
||||
|
||||
expect(screen.getByRole('button', { name: 'Delete' })).toBeInTheDocument(); |
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Delete' })); |
||||
await waitFor(() => { |
||||
expect(mockOnChange).toHaveBeenCalledWith(expect.objectContaining({ points: [[15, 1604340370000]] })); |
||||
}); |
||||
|
||||
editor.rerender( |
||||
<ManualEntryEditor |
||||
query={{ |
||||
...defaultQuery, |
||||
points: [[15, 1604340370000]], |
||||
}} |
||||
onChange={jest.fn()} |
||||
onRunQuery={jest.fn()} |
||||
/> |
||||
); |
||||
|
||||
select = screen.getByText('All values').nextSibling!; |
||||
await fireEvent.keyDown(select, { keyCode: 40 }); |
||||
expect(screen.getAllByLabelText('Select option')).toHaveLength(1); |
||||
expect(screen.queryByRole('button', { name: 'Delete' })).not.toBeInTheDocument(); |
||||
}); |
||||
}); |
||||
@ -1,92 +0,0 @@ |
||||
import React from 'react'; |
||||
import { dateMath, dateTime, SelectableValue } from '@grafana/data'; |
||||
import { Form, InlineField, InlineFieldRow, Input, InputControl, Select, Button } from '@grafana/ui'; |
||||
import { EditorProps } from '../QueryEditor'; |
||||
import { NewPoint } from '../types'; |
||||
|
||||
export interface Props extends EditorProps { |
||||
onRunQuery: () => void; |
||||
} |
||||
|
||||
export const ManualEntryEditor = ({ onChange, query, onRunQuery }: Props) => { |
||||
const points = query.points ?? []; |
||||
|
||||
const addPoint = (point: NewPoint) => { |
||||
const newPointTime = dateMath.parse(point.newPointTime); |
||||
const pointsUpdated = [...points, [Number(point.newPointValue), newPointTime!.valueOf()]].sort( |
||||
(a, b) => a[1] - b[1] |
||||
); |
||||
onChange({ ...query, points: pointsUpdated }); |
||||
onRunQuery(); |
||||
}; |
||||
|
||||
const deletePoint = (point: SelectableValue) => { |
||||
const pointsUpdated = points.filter((_, index) => index !== point.value); |
||||
onChange({ ...query, points: pointsUpdated }); |
||||
onRunQuery(); |
||||
}; |
||||
|
||||
const pointOptions = points.map((point, index) => { |
||||
return { |
||||
label: dateTime(point[1]).format('MMMM Do YYYY, H:mm:ss') + ' : ' + point[0], |
||||
value: index, |
||||
}; |
||||
}); |
||||
|
||||
return ( |
||||
<Form onSubmit={addPoint} maxWidth="none"> |
||||
{({ register, control, watch, setValue }) => { |
||||
const selectedPoint = watch('selectedPoint' as any) as SelectableValue; |
||||
return ( |
||||
<InlineFieldRow> |
||||
<InlineField label="New value" labelWidth={14}> |
||||
<Input |
||||
{...register('newPointValue')} |
||||
width={32} |
||||
type="number" |
||||
placeholder="value" |
||||
id={`newPointValue-${query.refId}`} |
||||
/> |
||||
</InlineField> |
||||
<InlineField label="Time" labelWidth={14}> |
||||
<Input |
||||
{...register('newPointTime')} |
||||
width={32} |
||||
id={`newPointTime-${query.refId}`} |
||||
placeholder="time" |
||||
defaultValue={dateTime().format()} |
||||
/> |
||||
</InlineField> |
||||
<InlineField> |
||||
<Button variant="secondary">Add</Button> |
||||
</InlineField> |
||||
<InlineField label="All values"> |
||||
<InputControl |
||||
name={'selectedPoint' as any} |
||||
control={control} |
||||
render={({ field: { ref, ...field } }) => ( |
||||
<Select {...field} options={pointOptions} width={32} placeholder="Select point" /> |
||||
)} |
||||
/> |
||||
</InlineField> |
||||
|
||||
{selectedPoint?.value !== undefined && ( |
||||
<InlineField> |
||||
<Button |
||||
type="button" |
||||
variant="destructive" |
||||
onClick={() => { |
||||
setValue('selectedPoint' as any, [{ value: undefined, label: 'Select value' }]); |
||||
deletePoint(selectedPoint); |
||||
}} |
||||
> |
||||
Delete |
||||
</Button> |
||||
</InlineField> |
||||
)} |
||||
</InlineFieldRow> |
||||
); |
||||
}} |
||||
</Form> |
||||
); |
||||
}; |
||||
@ -1,3 +1,2 @@ |
||||
export { StreamingClientEditor } from './StreamingClientEditor'; |
||||
export { ManualEntryEditor } from './ManualEntryEditor'; |
||||
export { RandomWalkEditor } from './RandomWalkEditor'; |
||||
|
||||
|
|
Loading…
Reference in new issue