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>
This commit is contained in:
Torkel Ödegaard 2021-05-26 10:42:42 +02:00 committed by GitHub
parent b4ce068f0e
commit 987bffe482
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 883 additions and 699 deletions

View File

@ -15,7 +15,6 @@
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": 441,
"links": [],
"panels": [
{
@ -56,19 +55,21 @@
"overrides": []
},
"gridPos": {
"h": 13,
"w": 24,
"h": 10,
"w": 12,
"x": 0,
"y": 0
},
"id": 9,
"options": {
"barWidth": 1,
"groupWidth": 1,
"groupWidth": 0.82,
"legend": {
"calcs": [],
"calcs": [
"max"
],
"displayMode": "list",
"placement": "bottom"
"placement": "right"
},
"orientation": "auto",
"showValue": "auto",
@ -79,11 +80,85 @@
},
"targets": [
{
"csvContent": "Name,Stat1,Stat2\nStockholm, 10, 15\nNew York, 19, 5\nLondon, 10, 1\nNegative, 15, -5\nLong value, 15,10",
"refId": "A",
"scenarioId": "categorical_data"
"scenarioId": "csv_content"
}
],
"title": "Plenty od data, automatic value sizing",
"title": "Auto sizing & auto show values",
"type": "barchart"
},
{
"datasource": null,
"description": "Should be smaller given the longer value",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"axisSoftMin": 0,
"fillOpacity": 80,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false
},
"lineWidth": 0
},
"decimals": 2,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 10,
"w": 12,
"x": 12,
"y": 0
},
"id": 15,
"options": {
"barWidth": 1,
"groupWidth": 0.82,
"legend": {
"calcs": [
"max"
],
"displayMode": "list",
"placement": "right"
},
"orientation": "auto",
"showValue": "auto",
"text": {},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"csvContent": "Name,Stat1,Stat2\nStockholm, 10, 15\nNew York, 19, 5\nLondon, 10, 1\nNegative, 15, -5\nLong value, 15,10",
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Auto sizing & auto show values",
"type": "barchart"
},
{
@ -124,10 +199,152 @@
"overrides": []
},
"gridPos": {
"h": 12,
"w": 24,
"h": 11,
"w": 8,
"x": 0,
"y": 13
"y": 10
},
"id": 16,
"options": {
"barWidth": 1,
"groupWidth": 0.89,
"legend": {
"calcs": [
"max"
],
"displayMode": "list",
"placement": "right"
},
"orientation": "auto",
"showValue": "auto",
"text": {},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"csvContent": "Name,Stat1,Stat2,Stat3,Stat4,Stat5,Stat6,Stat7,Stat8,Stat9,Stat10\nA, 10, 15,8,3,4,12,14,1,5,10\nB, 19, 5,8,3,4,12,14,6,7,2\nC, 15, 5,8,3,4,10,4,6,7,2\n",
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "auto show values & No room for value",
"type": "barchart"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"axisSoftMin": 0,
"fillOpacity": 80,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false
},
"lineWidth": 0
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 8,
"x": 8,
"y": 10
},
"id": 17,
"options": {
"barWidth": 1,
"groupWidth": 0.89,
"legend": {
"calcs": [
"max"
],
"displayMode": "list",
"placement": "right"
},
"orientation": "auto",
"showValue": "always",
"text": {},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"csvContent": "Name,Stat1,Stat2,Stat3,Stat4,Stat5,Stat6,Stat7,Stat8,Stat9,Stat10\nA, 10, 15,8,3,4,12,14,1,5,10\nB, 19, 5,8,3,4,12,14,6,7,2\nC, 15, 5,8,3,4,10,4,6,7,2\n",
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "auto show values & Always show value",
"type": "barchart"
},
{
"datasource": "-- Dashboard --",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"axisSoftMin": 0,
"fillOpacity": 80,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false
},
"lineWidth": 0
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 8,
"x": 16,
"y": 10
},
"id": 10,
"options": {
@ -142,7 +359,7 @@
"showValue": "auto",
"text": {
"size": 10,
"valueSize": 10
"valueSize": 25
},
"tooltip": {
"mode": "single"
@ -150,11 +367,11 @@
},
"targets": [
{
"refId": "A",
"scenarioId": "categorical_data"
"panelId": 9,
"refId": "A"
}
],
"title": "Plenty od data, fixed value sizing",
"title": "Fixed value sizing",
"type": "barchart"
},
{
@ -195,267 +412,42 @@
"overrides": []
},
"gridPos": {
"h": 12,
"h": 11,
"w": 12,
"x": 0,
"y": 25
"y": 21
},
"id": 11,
"id": 18,
"options": {
"barWidth": 1,
"groupWidth": 1,
"groupWidth": 0.82,
"legend": {
"calcs": [],
"calcs": [
"max"
],
"displayMode": "list",
"placement": "bottom"
},
"orientation": "auto",
"showValue": "auto",
"text": {
"size": 10
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"refId": "A",
"scenarioId": "categorical_data"
}
],
"title": "Auto font size, value auto visible",
"transformations": [
{
"id": "filterByValue",
"options": {
"filters": [
{
"config": {
"id": "equal",
"options": {
"value": "Bedroom"
}
},
"fieldName": "location"
},
{
"config": {
"id": "equal",
"options": {
"value": "Cellar"
}
},
"fieldName": "location"
}
],
"match": "any",
"type": "include"
}
}
],
"type": "barchart"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"axisSoftMin": 0,
"fillOpacity": 80,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false
},
"lineWidth": 0
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 12,
"w": 12,
"x": 12,
"y": 25
},
"id": 12,
"options": {
"barWidth": 1,
"groupWidth": 1,
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"orientation": "auto",
"showValue": "always",
"text": {
"size": 10
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"refId": "A",
"scenarioId": "categorical_data"
}
],
"title": "Auto font size, value always visible",
"transformations": [
{
"id": "filterByValue",
"options": {
"filters": [
{
"config": {
"id": "equal",
"options": {
"value": "Bedroom"
}
},
"fieldName": "location"
},
{
"config": {
"id": "equal",
"options": {
"value": "Cellar"
}
},
"fieldName": "location"
}
],
"match": "any",
"type": "include"
}
}
],
"type": "barchart"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"axisSoftMin": 0,
"fillOpacity": 80,
"gradientMode": "none",
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false
},
"lineWidth": 0
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 12,
"w": 12,
"x": 0,
"y": 37
},
"id": 13,
"options": {
"barWidth": 1,
"groupWidth": 1,
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
"placement": "right"
},
"orientation": "horizontal",
"showValue": "auto",
"text": {
"size": 10
},
"text": {},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"csvContent": "Name,Stat1,Stat2\nStockholm, 10, 15\nNew York, 19, 5\nLondon, 10, 1\nLong value, 15,10",
"refId": "A",
"scenarioId": "categorical_data"
}
],
"title": "Auto font size, value auto visible",
"transformations": [
{
"id": "filterByValue",
"options": {
"filters": [
{
"config": {
"id": "equal",
"options": {
"value": "Bedroom"
}
},
"fieldName": "location"
},
{
"config": {
"id": "equal",
"options": {
"value": "Cellar"
}
},
"fieldName": "location"
}
],
"match": "any",
"type": "include"
}
"scenarioId": "csv_content"
}
],
"title": "Auto sizing & auto show values",
"type": "barchart"
},
{
"datasource": null,
"description": "",
"fieldConfig": {
"defaults": {
"color": {
@ -492,65 +484,37 @@
"overrides": []
},
"gridPos": {
"h": 12,
"h": 11,
"w": 12,
"x": 12,
"y": 37
"y": 21
},
"id": 14,
"id": 19,
"options": {
"barWidth": 1,
"groupWidth": 1,
"groupWidth": 0.89,
"legend": {
"calcs": [],
"calcs": [
"max"
],
"displayMode": "list",
"placement": "bottom"
"placement": "right"
},
"orientation": "horizontal",
"showValue": "always",
"text": {
"size": 10
},
"showValue": "auto",
"text": {},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"csvContent": "Name,Stat1,Stat2,Stat3,Stat4,Stat5,Stat6,Stat7,Stat8,Stat9,Stat10\nA, 10, 15,8,3,4,12,14,1,5,10\nB, 19, 5,8,3,4,12,14,6,7,2\nC, 15, 5,8,3,4,10,4,6,7,2\n",
"refId": "A",
"scenarioId": "categorical_data"
}
],
"title": "Auto font size, value always visible",
"transformations": [
{
"id": "filterByValue",
"options": {
"filters": [
{
"config": {
"id": "equal",
"options": {
"value": "Bedroom"
}
},
"fieldName": "location"
},
{
"config": {
"id": "equal",
"options": {
"value": "Cellar"
}
},
"fieldName": "location"
}
],
"match": "any",
"type": "include"
}
"scenarioId": "csv_content"
}
],
"title": "auto show values & little room",
"type": "barchart"
}
],
@ -571,7 +535,7 @@
},
"timepicker": {},
"timezone": "",
"title": "BarChart - value text sizing",
"title": "BarChart - Panel Tests - Value sizing",
"uid": "WFlOM-jM1",
"version": 9
}
"version": 3
}

View File

@ -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=

View File

@ -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
}

View File

@ -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))
}

View File

@ -27,7 +27,6 @@ const (
noDataPointsQuery queryType = "no_data_points"
datapointsOutsideRangeQuery queryType = "datapoints_outside_range"
csvMetricValuesQuery queryType = "csv_metric_values"
manualEntryQuery queryType = "manual_entry"
predictablePulseQuery queryType = "predictable_pulse"
predictableCSVWaveQuery queryType = "predictable_csv_wave"
streamingClientQuery queryType = "streaming_client"
@ -39,7 +38,8 @@ const (
serverError500Query queryType = "server_error_500"
logsQuery queryType = "logs"
nodeGraphQuery queryType = "node_graph"
categoricalDataQuery queryType = "categorical_data"
csvFileQueryType queryType = "csv_file"
csvContentQueryType queryType = "csv_content"
)
type queryType string
@ -117,12 +117,6 @@ Timestamps will line up evenly on timeStepSeconds (For example, 60 seconds means
handler: p.handleDatapointsOutsideRangeScenario,
})
p.registerScenario(&Scenario{
ID: string(manualEntryQuery),
Name: "Manual Entry",
handler: p.handleManualEntryScenario,
})
p.registerScenario(&Scenario{
ID: string(csvMetricValuesQuery),
Name: "CSV Metric Values",
@ -190,9 +184,15 @@ Timestamps will line up evenly on timeStepSeconds (For example, 60 seconds means
})
p.registerScenario(&Scenario{
ID: string(categoricalDataQuery),
Name: "Categorical Data",
handler: p.handleCategoricalDataScenario,
ID: string(csvFileQueryType),
Name: "CSV File",
handler: p.handleCsvFileScenario,
})
p.registerScenario(&Scenario{
ID: string(csvContentQueryType),
Name: "CSV Content",
handler: p.handleCsvContentScenario,
})
p.queryMux.HandleFunc("", p.handleFallbackScenario)
@ -286,97 +286,6 @@ func (p *testDataPlugin) handleDatapointsOutsideRangeScenario(ctx context.Contex
return resp, nil
}
func (p *testDataPlugin) handleManualEntryScenario(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("error reading query")
}
points := model.Get("points").MustArray()
frame := newSeriesForQuery(q, model, 0)
timeField := data.NewFieldFromFieldType(data.FieldTypeTime, 0)
valueField := data.NewFieldFromFieldType(data.FieldTypeNullableFloat64, 0)
timeField.Name = data.TimeSeriesTimeFieldName
valueField.Name = data.TimeSeriesValueFieldName
for _, val := range points {
pointValues := val.([]interface{})
var value *float64
if pointValues[0] != nil {
if valueFloat, err := strconv.ParseFloat(string(pointValues[0].(json.Number)), 64); err == nil {
value = &valueFloat
}
}
timeInt, err := strconv.ParseInt(string(pointValues[1].(json.Number)), 10, 64)
if err != nil {
continue
}
t := time.Unix(timeInt/int64(1e+3), (timeInt%int64(1e+3))*int64(1e+6))
timeField.Append(t)
valueField.Append(value)
}
frame.Fields = data.Fields{timeField, valueField}
respD := resp.Responses[q.RefID]
respD.Frames = append(respD.Frames, frame)
resp.Responses[q.RefID] = respD
}
return resp, nil
}
func csvToFieldValues(stringInput string) (*data.Field, error) {
parts := strings.Split(strings.ReplaceAll(stringInput, " ", ""), ",")
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
}
// If we can not parse the first value as a number, assume strings
_, err := strconv.ParseFloat(first, 64)
if err != nil {
field := data.NewFieldFromFieldType(data.FieldTypeNullableString, len(parts))
for idx, strVal := range parts {
if strVal == "null" || strVal == "" {
continue
}
field.SetConcrete(idx, strVal)
}
return field, nil
}
// Set any valid numbers
field := data.NewFieldFromFieldType(data.FieldTypeNullableFloat64, len(parts))
for idx, strVal := range parts {
if val, err := strconv.ParseFloat(strVal, 64); err == nil {
field.SetConcrete(idx, val)
}
}
return field, nil
}
func (p *testDataPlugin) handleCSVMetricValuesScenario(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
resp := backend.NewQueryDataResponse()
@ -388,7 +297,7 @@ func (p *testDataPlugin) handleCSVMetricValuesScenario(ctx context.Context, req
stringInput := model.Get("stringInput").MustString()
valueField, err := csvToFieldValues(stringInput)
valueField, err := csvLineToField(stringInput)
if err != nil {
return nil, err
}
@ -695,27 +604,6 @@ func (p *testDataPlugin) handleLogsScenario(ctx context.Context, req *backend.Qu
return resp, nil
}
func (p *testDataPlugin) handleCategoricalDataScenario(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
resp := backend.NewQueryDataResponse()
for _, q := range req.Queries {
frame := data.NewFrame(q.RefID,
data.NewField("location", nil, []string{}),
data.NewField("temperature", nil, []int64{}),
data.NewField("humidity", nil, []int64{}),
data.NewField("pressure", nil, []int64{}),
)
for i := 0; i < len(houseLocations); i++ {
frame.AppendRow(houseLocations[i], rand.Int63n(40+40)-40, rand.Int63n(100), rand.Int63n(1020-900)+900)
}
respD := resp.Responses[q.RefID]
respD.Frames = append(respD.Frames, frame)
resp.Responses[q.RefID] = respD
}
return resp, nil
}
func randomWalk(query backend.DataQuery, model *simplejson.Json, index int) *data.Frame {
timeWalkerMs := query.TimeRange.From.UnixNano() / int64(time.Millisecond)
to := query.TimeRange.To.UnixNano() / int64(time.Millisecond)

View File

@ -185,57 +185,6 @@ func TestTestdataScenarios(t *testing.T) {
require.True(t, maxNil)
})
})
t.Run("manual entry ", func(t *testing.T) {
t.Run("should support nulls and return all data", func(t *testing.T) {
timeRange := plugins.DataTimeRange{From: "5m", To: "now", Now: time.Now()}
query := backend.DataQuery{
RefID: "A",
TimeRange: backend.TimeRange{
From: timeRange.MustGetFrom(),
To: timeRange.MustGetTo(),
},
JSON: []byte(`{ "points": [
[
4, 1616557148000
],
[
null, 1616558756000
],
[
4, 1616561658000
]] }`),
}
req := &backend.QueryDataRequest{
PluginContext: backend.PluginContext{},
Queries: []backend.DataQuery{query},
}
resp, err := p.handleManualEntryScenario(context.Background(), req)
require.NoError(t, err)
require.NotNil(t, resp)
dResp, exists := resp.Responses[query.RefID]
require.True(t, exists)
require.NoError(t, dResp.Error)
require.Len(t, dResp.Frames, 1)
frame := dResp.Frames[0]
require.Len(t, frame.Fields, 2)
require.Equal(t, "Time", frame.Fields[0].Name)
require.Equal(t, "Value", frame.Fields[1].Name)
require.Equal(t, 3, frame.Rows())
vals := frame.Fields[1]
v, _ := vals.ConcreteAt(0)
require.Equal(t, float64(4), v)
require.Nil(t, vals.At(1))
v, _ = vals.ConcreteAt(2)
require.Equal(t, float64(4), v)
})
})
}
func TestParseLabels(t *testing.T) {
@ -263,38 +212,3 @@ func TestParseLabels(t *testing.T) {
assert.Equal(t, expectedTags, parseLabels(model), fmt.Sprintf("Actual tags in test case %d doesn't match expected tags", i+1))
}
}
func TestReadCSV(t *testing.T) {
fBool, err := csvToFieldValues("T, F,F,T ,")
require.NoError(t, err)
fBool2, err := csvToFieldValues("true,false,T,F,F")
require.NoError(t, err)
fNum, err := csvToFieldValues("1,2,,4,5")
require.NoError(t, err)
fStr, err := csvToFieldValues("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.Equal(t, "", string(out))
require.JSONEq(t, `{"schema":{
"fields":[
{"type":"boolean","typeInfo":{"frame":"bool","nullable":true}},
{"type":"boolean","typeInfo":{"frame":"bool","nullable":true}},
{"type":"number","typeInfo":{"frame":"float64","nullable":true}},
{"type":"string","typeInfo":{"frame":"string","nullable":true}}
]},"data":{
"values":[
[true,false,false,true,null],
[true,false,true,false,false],
[1,2,null,4,5],
["a","b",null,null,"c"]
]}}`, string(out))
}

View File

@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/setting"
)
func init() {
@ -19,6 +20,7 @@ func init() {
type testDataPlugin struct {
BackendPluginManager backendplugin.Manager `inject:""`
Cfg *setting.Cfg `inject:""`
logger log.Logger
scenarios map[string]*Scenario
queryMux *datasource.QueryTypeMux

View File

@ -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=

View File

@ -0,0 +1,3 @@
Field1,Field2,Field3,123
True,Hello,6,
False,6,World,6
1 Field1 Field2 Field3 123
2 True Hello 6
3 False 6 World 6

View File

@ -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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

View File

@ -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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

View File

@ -0,0 +1,3 @@
Field1,Field2,Field3,Float,Time
A,5,6,6.7,1621987000000
B,6,7,8.9,1621988000000
1 Field1 Field2 Field3 Float Time
2 A 5 6 6.7 1621987000000
3 B 6 7 8.9 1621988000000

View File

@ -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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

View File

@ -6,7 +6,7 @@ import { useAsync } from 'react-use';
import { selectors as editorSelectors } from '@grafana/e2e-selectors';
import { Input, InlineFieldRow, InlineField, Select, TextArea, InlineSwitch } from '@grafana/ui';
import { QueryEditorProps, SelectableValue } from '@grafana/data';
import { StreamingClientEditor, ManualEntryEditor, RandomWalkEditor } from './components';
import { StreamingClientEditor, RandomWalkEditor } from './components';
// Types
import { TestDataDataSource } from './datasource';
@ -17,6 +17,8 @@ import { defaultCSVWaveQuery, defaultPulseQuery, defaultQuery } from './constant
import { GrafanaLiveEditor } from './components/GrafanaLiveEditor';
import { NodeGraphEditor } from './components/NodeGraphEditor';
import { defaultStreamQuery } from './runStreams';
import { CSVFileEditor } from './components/CSVFileEditor';
import { CSVContentEditor } from './components/CSVContentEditor';
const showLabelsFor = ['random_walk', 'predictable_pulse'];
const endpoints = [
@ -38,6 +40,20 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props)
query = { ...defaultQuery, ...query };
const { loading, value: scenarioList } = useAsync<Scenario[]>(async () => {
// migrate manual_entry (unusable since 7, removed in 8)
if (query.scenarioId === 'manual_entry' && (query as any).points) {
let csvContent = 'Time,Value\n';
for (const point of (query as any).points) {
csvContent += `${point[1]},${point[0]}\n`;
}
onChange({
refId: query.refId,
datasource: query.datasource,
scenarioId: 'csv_content',
csvContent,
});
}
return datasource.getScenarios();
}, []);
@ -205,10 +221,11 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props)
)}
</InlineFieldRow>
{scenarioId === 'manual_entry' && <ManualEntryEditor onChange={onUpdate} query={query} onRunQuery={onRunQuery} />}
{scenarioId === 'random_walk' && <RandomWalkEditor onChange={onInputChange} query={query} />}
{scenarioId === 'streaming_client' && <StreamingClientEditor onChange={onStreamClientChange} query={query} />}
{scenarioId === 'live' && <GrafanaLiveEditor onChange={onUpdate} query={query} />}
{scenarioId === 'csv_file' && <CSVFileEditor onChange={onUpdate} query={query} />}
{scenarioId === 'csv_content' && <CSVContentEditor onChange={onUpdate} query={query} />}
{scenarioId === 'logs' && (
<InlineFieldRow>
<InlineField label="Lines" labelWidth={14}>

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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();
});
});

View File

@ -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>
);
};

View File

@ -1,3 +1,2 @@
export { StreamingClientEditor } from './StreamingClientEditor';
export { ManualEntryEditor } from './ManualEntryEditor';
export { RandomWalkEditor } from './RandomWalkEditor';

View File

@ -61,6 +61,19 @@ export class TestDataDataSource extends DataSourceWithBackend<TestDataQuery> {
case 'node_graph':
streams.push(this.nodesQuery(target, options));
break;
// Unusable since 7, removed in 8
case 'manual_entry': {
let csvContent = 'Time,Value\n';
if ((target as any).points) {
for (const point of (target as any).points) {
csvContent += `${point[1]},${point[0]}\n`;
}
}
target.scenarioId = 'csv_content';
target.csvContent = csvContent;
}
default:
if (target.alias) {
target.alias = this.templateSrv.replace(target.alias, options.scopedVars);

View File

@ -6,19 +6,10 @@ export interface Scenario {
stringInput: string;
}
export type PointValue = number;
export interface NewPoint {
newPointValue: string;
newPointTime: string;
}
export type Points = PointValue[][];
export interface TestDataQuery extends DataQuery {
alias?: string;
scenarioId: string;
stringInput?: string;
points?: Points;
stream?: StreamingQuery;
pulseWave?: PulseWaveQuery;
csvWave?: CSVWave[];
@ -27,6 +18,8 @@ export interface TestDataQuery extends DataQuery {
levelColumn?: boolean;
channel?: string; // for grafana live
nodes?: NodesQuery;
csvFileName?: string;
csvContent?: string;
}
export interface NodesQuery {

View File

@ -10,7 +10,6 @@ export class TestDataVariableSupport extends StandardVariableSupport<TestDataDat
stringInput: query.query,
scenarioId: 'variables-query',
csvWave: undefined,
points: [],
};
}
}

5
public/testdata/city_stats.csv vendored Normal file
View File

@ -0,0 +1,5 @@
City, Population
Stockholm, 1000000
New York, 13333300
1 City Population
2 Stockholm 1000000
3 New York 13333300

View File

@ -0,0 +1,6 @@
State, 2020, 2000, 1980
California, 39368078,33987977,23800800
Texas, 29360759, 20944499, 14338208
Florida, 21733312, 16047515, 9839835
1 State 2020 2000 1980
2 California 39368078 33987977 23800800
3 Texas 29360759 20944499 14338208
4 Florida 21733312 16047515 9839835