mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Visualizations: Dynamically set any config (min, max, unit, color, thresholds) from query results (#36548)
* initial steps for config from data * Moving to core and separate transforms * Progress * Rows to fields are starting to work * Config from query transform working * UI progress * More scenarios working * Update public/app/core/components/TransformersUI/rowsToFields/rowsToFields.ts Co-authored-by: Ryan McKinley <ryantxu@gmail.com> * transform all * Refactor * UI starting to work * Add matcher UI to transform * Apply to self * Adding a reducer option * Value mapping via new all values reducer * value mappings workg add -A * Minor changes * Improving UI and adding test dashboards * RowsToFieldsTransformerEditor tests * Added tests for FieldToConfigMapping Editor * Added test for ConfigFromQueryTransformerEditor * Minor UI tweaks * Added missing test * Added label extraction * unified mapping * Progress refactoring * Updates * UI tweaks * Rename * Updates Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
parent
e06335ffe9
commit
702fd1cad9
@ -8,6 +8,12 @@
|
|||||||
"hide": true,
|
"hide": true,
|
||||||
"iconColor": "rgba(0, 211, 255, 1)",
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
"name": "Annotations & Alerts",
|
"name": "Annotations & Alerts",
|
||||||
|
"target": {
|
||||||
|
"limit": 100,
|
||||||
|
"matchAny": false,
|
||||||
|
"tags": [],
|
||||||
|
"type": "dashboard"
|
||||||
|
},
|
||||||
"type": "dashboard"
|
"type": "dashboard"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -18,21 +24,26 @@
|
|||||||
"links": [],
|
"links": [],
|
||||||
"panels": [
|
"panels": [
|
||||||
{
|
{
|
||||||
"folderId": null,
|
"datasource": null,
|
||||||
"gridPos": {
|
"gridPos": {
|
||||||
"h": 26,
|
"h": 26,
|
||||||
"w": 6,
|
"w": 6,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0
|
"y": 0
|
||||||
},
|
},
|
||||||
"headings": true,
|
|
||||||
"id": 7,
|
"id": 7,
|
||||||
"limit": 100,
|
|
||||||
"links": [],
|
"links": [],
|
||||||
"query": "",
|
"options": {
|
||||||
"recent": true,
|
"folderId": null,
|
||||||
"search": false,
|
"maxItems": 100,
|
||||||
"starred": true,
|
"query": "",
|
||||||
|
"showHeadings": true,
|
||||||
|
"showRecentlyViewed": true,
|
||||||
|
"showSearch": false,
|
||||||
|
"showStarred": true,
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.1.0-pre",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"timeFrom": null,
|
"timeFrom": null,
|
||||||
"timeShift": null,
|
"timeShift": null,
|
||||||
@ -40,95 +51,167 @@
|
|||||||
"type": "dashlist"
|
"type": "dashlist"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"folderId": null,
|
"datasource": null,
|
||||||
"gridPos": {
|
"gridPos": {
|
||||||
"h": 13,
|
"h": 13,
|
||||||
"w": 6,
|
"w": 6,
|
||||||
"x": 6,
|
"x": 6,
|
||||||
"y": 0
|
"y": 0
|
||||||
},
|
},
|
||||||
"headings": false,
|
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"limit": 1000,
|
|
||||||
"links": [],
|
"links": [],
|
||||||
"query": "",
|
"options": {
|
||||||
"recent": false,
|
"maxItems": 1000,
|
||||||
"search": true,
|
"query": "",
|
||||||
"starred": false,
|
"showHeadings": false,
|
||||||
"tags": ["panel-tests"],
|
"showRecentlyViewed": false,
|
||||||
|
"showSearch": true,
|
||||||
|
"showStarred": false,
|
||||||
|
"tags": [
|
||||||
|
"panel-tests"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.1.0-pre",
|
||||||
|
"tags": [
|
||||||
|
"panel-tests"
|
||||||
|
],
|
||||||
"timeFrom": null,
|
"timeFrom": null,
|
||||||
"timeShift": null,
|
"timeShift": null,
|
||||||
"title": "tag: panel-tests",
|
"title": "tag: panel-tests",
|
||||||
"type": "dashlist"
|
"type": "dashlist"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"folderId": null,
|
"datasource": null,
|
||||||
"gridPos": {
|
"gridPos": {
|
||||||
"h": 26,
|
"h": 13,
|
||||||
"w": 6,
|
"w": 6,
|
||||||
"x": 12,
|
"x": 12,
|
||||||
"y": 0
|
"y": 0
|
||||||
},
|
},
|
||||||
"headings": false,
|
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"limit": 1000,
|
|
||||||
"links": [],
|
"links": [],
|
||||||
"query": "",
|
"options": {
|
||||||
"recent": false,
|
"folderId": null,
|
||||||
"search": true,
|
"maxItems": 1000,
|
||||||
"starred": false,
|
"query": "",
|
||||||
"tags": ["gdev", "demo"],
|
"showHeadings": false,
|
||||||
|
"showRecentlyViewed": false,
|
||||||
|
"showSearch": true,
|
||||||
|
"showStarred": false,
|
||||||
|
"tags": [
|
||||||
|
"gdev",
|
||||||
|
"demo"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.1.0-pre",
|
||||||
|
"tags": [
|
||||||
|
"gdev",
|
||||||
|
"demo"
|
||||||
|
],
|
||||||
"timeFrom": null,
|
"timeFrom": null,
|
||||||
"timeShift": null,
|
"timeShift": null,
|
||||||
"title": "tag: dashboard-demo",
|
"title": "tag: dashboard-demo",
|
||||||
"type": "dashlist"
|
"type": "dashlist"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"folderId": null,
|
"datasource": null,
|
||||||
"gridPos": {
|
"gridPos": {
|
||||||
"h": 26,
|
"h": 26,
|
||||||
"w": 6,
|
"w": 6,
|
||||||
"x": 18,
|
"x": 18,
|
||||||
"y": 0
|
"y": 0
|
||||||
},
|
},
|
||||||
"headings": false,
|
|
||||||
"id": 5,
|
"id": 5,
|
||||||
"limit": 1000,
|
|
||||||
"links": [],
|
"links": [],
|
||||||
"query": "",
|
"options": {
|
||||||
"recent": false,
|
"folderId": null,
|
||||||
"search": true,
|
"maxItems": 1000,
|
||||||
"starred": false,
|
"query": "",
|
||||||
"tags": ["gdev", "datasource-test"],
|
"showHeadings": false,
|
||||||
|
"showRecentlyViewed": false,
|
||||||
|
"showSearch": true,
|
||||||
|
"showStarred": false,
|
||||||
|
"tags": [
|
||||||
|
"gdev",
|
||||||
|
"datasource-test"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.1.0-pre",
|
||||||
|
"tags": [
|
||||||
|
"gdev",
|
||||||
|
"datasource-test"
|
||||||
|
],
|
||||||
"timeFrom": null,
|
"timeFrom": null,
|
||||||
"timeShift": null,
|
"timeShift": null,
|
||||||
"title": "Data source tests",
|
"title": "Data source tests",
|
||||||
"type": "dashlist"
|
"type": "dashlist"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"folderId": null,
|
"datasource": null,
|
||||||
"gridPos": {
|
"gridPos": {
|
||||||
"h": 13,
|
"h": 13,
|
||||||
"w": 6,
|
"w": 6,
|
||||||
"x": 6,
|
"x": 6,
|
||||||
"y": 13
|
"y": 13
|
||||||
},
|
},
|
||||||
"headings": false,
|
|
||||||
"id": 4,
|
"id": 4,
|
||||||
"limit": 1000,
|
|
||||||
"links": [],
|
"links": [],
|
||||||
"query": "",
|
"options": {
|
||||||
"recent": false,
|
"maxItems": 1000,
|
||||||
"search": true,
|
"query": "",
|
||||||
"starred": false,
|
"showHeadings": false,
|
||||||
"tags": ["templating", "gdev"],
|
"showRecentlyViewed": false,
|
||||||
|
"showSearch": true,
|
||||||
|
"showStarred": false,
|
||||||
|
"tags": [
|
||||||
|
"templating",
|
||||||
|
"gdev"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.1.0-pre",
|
||||||
|
"tags": [
|
||||||
|
"templating",
|
||||||
|
"gdev"
|
||||||
|
],
|
||||||
"timeFrom": null,
|
"timeFrom": null,
|
||||||
"timeShift": null,
|
"timeShift": null,
|
||||||
"title": "tag: templating ",
|
"title": "tag: templating ",
|
||||||
"type": "dashlist"
|
"type": "dashlist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": null,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 13,
|
||||||
|
"w": 6,
|
||||||
|
"x": 12,
|
||||||
|
"y": 13
|
||||||
|
},
|
||||||
|
"id": 8,
|
||||||
|
"links": [],
|
||||||
|
"options": {
|
||||||
|
"maxItems": 1000,
|
||||||
|
"query": "",
|
||||||
|
"showHeadings": false,
|
||||||
|
"showRecentlyViewed": false,
|
||||||
|
"showSearch": true,
|
||||||
|
"showStarred": false,
|
||||||
|
"tags": [
|
||||||
|
"gdev",
|
||||||
|
"transform"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.1.0-pre",
|
||||||
|
"tags": [
|
||||||
|
"gdev",
|
||||||
|
"demo"
|
||||||
|
],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "tag: transforms",
|
||||||
|
"type": "dashlist"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"schemaVersion": 18,
|
"schemaVersion": 30,
|
||||||
"style": "dark",
|
"style": "dark",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"templating": {
|
"templating": {
|
||||||
@ -139,11 +222,32 @@
|
|||||||
"to": "now"
|
"to": "now"
|
||||||
},
|
},
|
||||||
"timepicker": {
|
"timepicker": {
|
||||||
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
|
"refresh_intervals": [
|
||||||
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
|
"5s",
|
||||||
|
"10s",
|
||||||
|
"30s",
|
||||||
|
"1m",
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"30m",
|
||||||
|
"1h",
|
||||||
|
"2h",
|
||||||
|
"1d"
|
||||||
|
],
|
||||||
|
"time_options": [
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"1h",
|
||||||
|
"6h",
|
||||||
|
"12h",
|
||||||
|
"24h",
|
||||||
|
"2d",
|
||||||
|
"7d",
|
||||||
|
"30d"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"timezone": "",
|
"timezone": "",
|
||||||
"title": "Grafana Dev Overview & Home",
|
"title": "Grafana Dev Overview & Home",
|
||||||
"uid": "j6T00KRZz",
|
"uid": "j6T00KRZz",
|
||||||
"version": 2
|
"version": 2
|
||||||
}
|
}
|
568
devenv/dev-dashboards/transforms/config-from-query.json
Normal file
568
devenv/dev-dashboards/transforms/config-from-query.json
Normal file
@ -0,0 +1,568 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": "-- Grafana --",
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"target": {
|
||||||
|
"limit": 100,
|
||||||
|
"matchAny": false,
|
||||||
|
"tags": [],
|
||||||
|
"type": "dashboard"
|
||||||
|
},
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "",
|
||||||
|
"editable": true,
|
||||||
|
"gnetId": null,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"datasource": null,
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "line",
|
||||||
|
"fillOpacity": 0,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "auto",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "line"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 2,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": [],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"hide": false,
|
||||||
|
"max": 100,
|
||||||
|
"min": 1,
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "random_walk",
|
||||||
|
"startValue": 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "",
|
||||||
|
"csvContent": "min,max,threshold1\n1000,1000,8000\n0,100,80\n\n",
|
||||||
|
"refId": "config",
|
||||||
|
"scenarioId": "csv_content"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Min, max, threshold from separate query",
|
||||||
|
"transformations": [
|
||||||
|
{
|
||||||
|
"id": "configFromData",
|
||||||
|
"options": {
|
||||||
|
"configRefId": "config",
|
||||||
|
"mappings": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": null,
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"align": "left",
|
||||||
|
"displayMode": "auto"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "SensorA"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.displayMode",
|
||||||
|
"value": "color-text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 5,
|
||||||
|
"options": {
|
||||||
|
"showHeader": true
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.1.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"csvContent": "Name, Value, SensorA, MyUnit, MyColor\nGoogle, 10, 50, km/h, blue\nGoogle, 100, 100,km/h, orange\n",
|
||||||
|
"hide": false,
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "csv_content"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Custom mappings and apply to self",
|
||||||
|
"transformations": [
|
||||||
|
{
|
||||||
|
"id": "configFromData",
|
||||||
|
"options": {
|
||||||
|
"applyTo": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "SensorA"
|
||||||
|
},
|
||||||
|
"applyToConfig": true,
|
||||||
|
"configRefId": "A",
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"configProperty": "unit",
|
||||||
|
"fieldName": "MyUnit",
|
||||||
|
"handlerKey": "unit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldName": "MyColor",
|
||||||
|
"handlerKey": "color"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": null,
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"align": "center",
|
||||||
|
"displayMode": "auto"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Value"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.displayMode",
|
||||||
|
"value": "color-background-solid"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 5,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 9
|
||||||
|
},
|
||||||
|
"id": 7,
|
||||||
|
"options": {
|
||||||
|
"showHeader": true
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.1.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"csvContent": "ID, DisplayText\n21412312312, Homer\n12421412413, Simpsons \n12321312313, Bart",
|
||||||
|
"hide": false,
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "csv_content"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Mapping data",
|
||||||
|
"transformations": [
|
||||||
|
{
|
||||||
|
"id": "configFromData",
|
||||||
|
"options": {
|
||||||
|
"applyToConfig": true,
|
||||||
|
"configRefId": "A",
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"fieldName": "Color",
|
||||||
|
"handlerKey": "mappings.color"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldName": "Value",
|
||||||
|
"handlerKey": "mappings.value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": null,
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"align": "center",
|
||||||
|
"displayMode": "auto"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Value"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.displayMode",
|
||||||
|
"value": "color-background-solid"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 10,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 9
|
||||||
|
},
|
||||||
|
"id": 6,
|
||||||
|
"options": {
|
||||||
|
"showHeader": true
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.1.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"csvContent": "Value, Color\nOK, blue\nPretty bad, red\nYay it's green, green\nSomething is off, orange\nNo idea, #88AA00\nAm I purple?, purple",
|
||||||
|
"hide": false,
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "csv_content"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Value mappings from query result applied to itself",
|
||||||
|
"transformations": [
|
||||||
|
{
|
||||||
|
"id": "configFromData",
|
||||||
|
"options": {
|
||||||
|
"applyTo": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Value"
|
||||||
|
},
|
||||||
|
"applyToConfig": true,
|
||||||
|
"configRefId": "A",
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"fieldName": "Color",
|
||||||
|
"handlerKey": "mappings.color"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldName": "Value",
|
||||||
|
"handlerKey": "mappings.value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": null,
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"align": "center",
|
||||||
|
"displayMode": "auto"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 5,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 14
|
||||||
|
},
|
||||||
|
"id": 8,
|
||||||
|
"options": {
|
||||||
|
"showHeader": true
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.1.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"csvContent": "ID, Value\n21412312312, 100\n12421412413, 20\n12321312313, 10",
|
||||||
|
"hide": false,
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "csv_content"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Display data",
|
||||||
|
"transformations": [
|
||||||
|
{
|
||||||
|
"id": "configFromData",
|
||||||
|
"options": {
|
||||||
|
"applyToConfig": true,
|
||||||
|
"configRefId": "A",
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"fieldName": "Color",
|
||||||
|
"handlerKey": "mappings.color"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldName": "Value",
|
||||||
|
"handlerKey": "mappings.value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": null,
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"axisSoftMin": 0,
|
||||||
|
"fillOpacity": 80,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"lineWidth": 1
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 10,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 19
|
||||||
|
},
|
||||||
|
"id": 9,
|
||||||
|
"options": {
|
||||||
|
"barWidth": 0.97,
|
||||||
|
"groupWidth": 0.7,
|
||||||
|
"legend": {
|
||||||
|
"calcs": [],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom"
|
||||||
|
},
|
||||||
|
"orientation": "horizontal",
|
||||||
|
"showValue": "auto",
|
||||||
|
"text": {},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.1.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"csvContent": "ID, Value\nA21412312312, 100\nA12421412413, 20\nA12321312313, 10\n",
|
||||||
|
"hide": false,
|
||||||
|
"refId": "data",
|
||||||
|
"scenarioId": "csv_content"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"csvContent": "ID, DisplayText\nA21412312312, Homer\nA12421412413, Marge \nA12321312313, Bart",
|
||||||
|
"hide": false,
|
||||||
|
"refId": "mappings",
|
||||||
|
"scenarioId": "csv_content"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Value mapping ID -> DisplayText from separate query",
|
||||||
|
"transformations": [
|
||||||
|
{
|
||||||
|
"id": "configFromData",
|
||||||
|
"options": {
|
||||||
|
"applyTo": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "ID"
|
||||||
|
},
|
||||||
|
"applyToConfig": false,
|
||||||
|
"configRefId": "mappings",
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"fieldName": "ID",
|
||||||
|
"handlerKey": "mappings.value"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldName": "DisplayText",
|
||||||
|
"handlerKey": "mappings.text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "barchart"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refresh": "",
|
||||||
|
"schemaVersion": 30,
|
||||||
|
"style": "dark",
|
||||||
|
"tags": [
|
||||||
|
"gdev",
|
||||||
|
"transform"
|
||||||
|
],
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-6h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {},
|
||||||
|
"timezone": "",
|
||||||
|
"title": "Transforms - Config from query",
|
||||||
|
"uid": "Juj4_7ink",
|
||||||
|
"version": 1
|
||||||
|
}
|
615
devenv/dev-dashboards/transforms/rows-to-fields.json
Normal file
615
devenv/dev-dashboards/transforms/rows-to-fields.json
Normal file
@ -0,0 +1,615 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": "-- Grafana --",
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"target": {
|
||||||
|
"limit": 100,
|
||||||
|
"matchAny": false,
|
||||||
|
"tags": [],
|
||||||
|
"type": "dashboard"
|
||||||
|
},
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"gnetId": null,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"datasource": "-- Dashboard --",
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"align": "left",
|
||||||
|
"displayMode": "auto"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 5,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 8,
|
||||||
|
"options": {
|
||||||
|
"showHeader": true
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.1.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"panelId": 2,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Raw data",
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": "-- Dashboard --",
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"align": "left",
|
||||||
|
"displayMode": "auto"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Value"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 82
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Unit"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 108
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 5,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 7,
|
||||||
|
"options": {
|
||||||
|
"showHeader": true,
|
||||||
|
"sortBy": []
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.1.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"panelId": 3,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Raw data",
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": null,
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 7,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 5
|
||||||
|
},
|
||||||
|
"id": 2,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"text": {},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.1.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"csvContent": "Name,Value,Unit,Color\nTemperature,10,degree,green\nPressure,100,bar,blue\nSpeed,30,km/h,red",
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "csv_content"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Unit and color from data",
|
||||||
|
"transformations": [
|
||||||
|
{
|
||||||
|
"id": "rowsToFields",
|
||||||
|
"options": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": null,
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"min": 0,
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 7,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 5
|
||||||
|
},
|
||||||
|
"id": 3,
|
||||||
|
"options": {
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"showThresholdLabels": true,
|
||||||
|
"showThresholdMarkers": true,
|
||||||
|
"text": {}
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.1.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"csvContent": "Name,Value,Unit,min,max, threshold1\nTemperature,10,degree,0,50,30\nPressure,100,Pa,0,300,200\nSpeed,30,km/h,0,150,110",
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "csv_content"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Min, Max & Thresholds from data",
|
||||||
|
"transformations": [
|
||||||
|
{
|
||||||
|
"id": "rowsToFields",
|
||||||
|
"options": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "gauge"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": "-- Dashboard --",
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"align": "left",
|
||||||
|
"displayMode": "auto"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 5,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 12
|
||||||
|
},
|
||||||
|
"id": 10,
|
||||||
|
"options": {
|
||||||
|
"showHeader": true
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.1.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"panelId": 9,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Raw data",
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": "-- Dashboard --",
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"align": "left",
|
||||||
|
"displayMode": "auto"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Value"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 82
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": {
|
||||||
|
"id": "byName",
|
||||||
|
"options": "Unit"
|
||||||
|
},
|
||||||
|
"properties": [
|
||||||
|
{
|
||||||
|
"id": "custom.width",
|
||||||
|
"value": 108
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 5,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 12
|
||||||
|
},
|
||||||
|
"id": 12,
|
||||||
|
"options": {
|
||||||
|
"showHeader": true,
|
||||||
|
"sortBy": []
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.1.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"panelId": 11,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Raw data (Custom mapping)",
|
||||||
|
"type": "table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": null,
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "continuous-GrYlRd"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 7,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 17
|
||||||
|
},
|
||||||
|
"id": 9,
|
||||||
|
"options": {
|
||||||
|
"displayMode": "gradient",
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"showUnfilled": true,
|
||||||
|
"text": {}
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.1.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"csvContent": "Name,Value,Unit,Min,Max\nTemperature,20,degree,0,50\nPressure,150,Pa,0,300\nSpeed,100,km/h,0,110",
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "csv_content"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Min max from data",
|
||||||
|
"transformations": [
|
||||||
|
{
|
||||||
|
"id": "rowsToFields",
|
||||||
|
"options": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "bargauge"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": null,
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"min": 0,
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 7,
|
||||||
|
"w": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 17
|
||||||
|
},
|
||||||
|
"id": 11,
|
||||||
|
"options": {
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"showThresholdLabels": true,
|
||||||
|
"showThresholdMarkers": true,
|
||||||
|
"text": {}
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.1.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"csvContent": "Name,Value,Type,Quota, Warning\nTemperature,25,degree,50,30\nPressure,100,Pa,300,200\nSpeed,30,km/h,150,130",
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "csv_content"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Custom mapping",
|
||||||
|
"transformations": [
|
||||||
|
{
|
||||||
|
"id": "rowsToFields",
|
||||||
|
"options": {
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"configProperty": "unit",
|
||||||
|
"fieldName": "Type",
|
||||||
|
"handlerKey": "unit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"configProperty": "max",
|
||||||
|
"fieldName": "Quota",
|
||||||
|
"handlerKey": "max"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"configProperty": "threshold1",
|
||||||
|
"fieldName": "Warning",
|
||||||
|
"handlerKey": "threshold1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "gauge"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": null,
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 7,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 24
|
||||||
|
},
|
||||||
|
"id": 13,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "horizontal",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"text": {},
|
||||||
|
"textMode": "auto"
|
||||||
|
},
|
||||||
|
"pluginVersion": "8.1.0-pre",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"csvContent": "Name, City, Country, Value\nSensorA, Stockholm, Sweden, 20\nSensorB, London, England, 50\nSensorC, New York, USA,100",
|
||||||
|
"refId": "A",
|
||||||
|
"scenarioId": "csv_content"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Extra string fields to labels",
|
||||||
|
"transformations": [
|
||||||
|
{
|
||||||
|
"id": "rowsToFields",
|
||||||
|
"options": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "stat"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refresh": "",
|
||||||
|
"schemaVersion": 30,
|
||||||
|
"style": "dark",
|
||||||
|
"tags": [
|
||||||
|
"gdev",
|
||||||
|
"transform"
|
||||||
|
],
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-6h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {},
|
||||||
|
"timezone": "",
|
||||||
|
"title": "Transforms - Rows to fields",
|
||||||
|
"uid": "PMtIInink",
|
||||||
|
"version": 1
|
||||||
|
}
|
@ -18,15 +18,13 @@ export enum ReducerID {
|
|||||||
diffperc = 'diffperc',
|
diffperc = 'diffperc',
|
||||||
delta = 'delta',
|
delta = 'delta',
|
||||||
step = 'step',
|
step = 'step',
|
||||||
|
|
||||||
firstNotNull = 'firstNotNull',
|
firstNotNull = 'firstNotNull',
|
||||||
lastNotNull = 'lastNotNull',
|
lastNotNull = 'lastNotNull',
|
||||||
|
|
||||||
changeCount = 'changeCount',
|
changeCount = 'changeCount',
|
||||||
distinctCount = 'distinctCount',
|
distinctCount = 'distinctCount',
|
||||||
|
|
||||||
allIsZero = 'allIsZero',
|
allIsZero = 'allIsZero',
|
||||||
allIsNull = 'allIsNull',
|
allIsNull = 'allIsNull',
|
||||||
|
allValues = 'allValues',
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal function
|
// Internal function
|
||||||
@ -232,6 +230,13 @@ export const fieldReducers = new Registry<FieldReducerInfo>(() => [
|
|||||||
description: 'Percentage difference between first and last values',
|
description: 'Percentage difference between first and last values',
|
||||||
standard: true,
|
standard: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: ReducerID.allValues,
|
||||||
|
name: 'All values',
|
||||||
|
description: 'Returns an array with all values',
|
||||||
|
standard: false,
|
||||||
|
reduce: (field: Field) => ({ allValues: field.values.toArray() }),
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function doStandardCalcs(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
|
export function doStandardCalcs(field: Field, ignoreNulls: boolean, nullAsZero: boolean): FieldCalcs {
|
||||||
|
@ -23,4 +23,6 @@ export enum DataTransformerID {
|
|||||||
groupBy = 'groupBy',
|
groupBy = 'groupBy',
|
||||||
sortBy = 'sortBy',
|
sortBy = 'sortBy',
|
||||||
histogram = 'histogram',
|
histogram = 'histogram',
|
||||||
|
configFromData = 'configFromData',
|
||||||
|
rowsToFields = 'rowsToFields',
|
||||||
}
|
}
|
||||||
|
@ -110,9 +110,11 @@ export class Gauge extends PureComponent<Props> {
|
|||||||
const valueWidthBase = Math.min(width, dimension * 1.3) * 0.9;
|
const valueWidthBase = Math.min(width, dimension * 1.3) * 0.9;
|
||||||
// remove gauge & marker width (on left and right side)
|
// remove gauge & marker width (on left and right side)
|
||||||
// and 10px is some padding that flot adds to the outer canvas
|
// and 10px is some padding that flot adds to the outer canvas
|
||||||
const valueWidth = valueWidthBase - ((gaugeWidth + (showThresholdMarkers ? thresholdMarkersWidth : 0)) * 2 + 10);
|
const valueWidth =
|
||||||
|
valueWidthBase -
|
||||||
|
((gaugeWidth + (showThresholdMarkers ? thresholdMarkersWidth : 0) + (showThresholdLabels ? 10 : 0)) * 2 + 10);
|
||||||
const fontSize = this.props.text?.valueSize ?? calculateFontSize(text, valueWidth, dimension, 1, gaugeWidth * 1.7);
|
const fontSize = this.props.text?.valueSize ?? calculateFontSize(text, valueWidth, dimension, 1, gaugeWidth * 1.7);
|
||||||
const thresholdLabelFontSize = fontSize / 2.5;
|
const thresholdLabelFontSize = Math.max(fontSize / 2.5, 12);
|
||||||
|
|
||||||
let min = field.min ?? 0;
|
let min = field.min ?? 0;
|
||||||
let max = field.max ?? 100;
|
let max = field.max ?? 100;
|
||||||
|
@ -28,7 +28,7 @@ func (p *testDataPlugin) handleCsvContentScenario(ctx context.Context, req *back
|
|||||||
}
|
}
|
||||||
|
|
||||||
csvContent := model.Get("csvContent").MustString()
|
csvContent := model.Get("csvContent").MustString()
|
||||||
alias := model.Get("alias").MustString(q.RefID)
|
alias := model.Get("alias").MustString("")
|
||||||
|
|
||||||
frame, err := p.loadCsvContent(strings.NewReader(csvContent), alias)
|
frame, err := p.loadCsvContent(strings.NewReader(csvContent), alias)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import {
|
import {
|
||||||
DataTransformerID,
|
DataTransformerID,
|
||||||
@ -8,7 +8,6 @@ import {
|
|||||||
TransformerRegistryItem,
|
TransformerRegistryItem,
|
||||||
TransformerUIProps,
|
TransformerUIProps,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { getAllFieldNamesFromDataFrames } from './OrganizeFieldsTransformerEditor';
|
|
||||||
import { Select, StatsPicker, stylesFactory } from '@grafana/ui';
|
import { Select, StatsPicker, stylesFactory } from '@grafana/ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -16,6 +15,7 @@ import {
|
|||||||
GroupByOperationID,
|
GroupByOperationID,
|
||||||
GroupByTransformerOptions,
|
GroupByTransformerOptions,
|
||||||
} from '@grafana/data/src/transformations/transformers/groupBy';
|
} from '@grafana/data/src/transformations/transformers/groupBy';
|
||||||
|
import { useAllFieldNamesFromDataFrames } from './utils';
|
||||||
|
|
||||||
interface FieldProps {
|
interface FieldProps {
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
@ -28,7 +28,7 @@ export const GroupByTransformerEditor: React.FC<TransformerUIProps<GroupByTransf
|
|||||||
options,
|
options,
|
||||||
onChange,
|
onChange,
|
||||||
}) => {
|
}) => {
|
||||||
const fieldNames = useMemo(() => getAllFieldNamesFromDataFrames(input), [input]);
|
const fieldNames = useAllFieldNamesFromDataFrames(input);
|
||||||
|
|
||||||
const onConfigChange = useCallback(
|
const onConfigChange = useCallback(
|
||||||
(fieldName: string) => (config: GroupByFieldOptions) => {
|
(fieldName: string) => (config: GroupByFieldOptions) => {
|
||||||
@ -47,7 +47,7 @@ export const GroupByTransformerEditor: React.FC<TransformerUIProps<GroupByTransf
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{fieldNames.map((key: string) => (
|
{fieldNames.map((key) => (
|
||||||
<GroupByFieldConfiguration
|
<GroupByFieldConfiguration
|
||||||
onConfigChange={onConfigChange(key)}
|
onConfigChange={onConfigChange(key)}
|
||||||
fieldName={key}
|
fieldName={key}
|
||||||
|
@ -2,18 +2,17 @@ import React, { useCallback, useMemo } from 'react';
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd';
|
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd';
|
||||||
import {
|
import {
|
||||||
DataFrame,
|
|
||||||
DataTransformerID,
|
DataTransformerID,
|
||||||
GrafanaTheme,
|
GrafanaTheme,
|
||||||
standardTransformers,
|
standardTransformers,
|
||||||
TransformerRegistryItem,
|
TransformerRegistryItem,
|
||||||
TransformerUIProps,
|
TransformerUIProps,
|
||||||
getFieldDisplayName,
|
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { stylesFactory, useTheme, Input, IconButton, Icon, FieldValidationMessage } from '@grafana/ui';
|
import { stylesFactory, useTheme, Input, IconButton, Icon, FieldValidationMessage } from '@grafana/ui';
|
||||||
|
|
||||||
import { OrganizeFieldsTransformerOptions } from '@grafana/data/src/transformations/transformers/organize';
|
import { OrganizeFieldsTransformerOptions } from '@grafana/data/src/transformations/transformers/organize';
|
||||||
import { createOrderFieldsComparer } from '@grafana/data/src/transformations/transformers/order';
|
import { createOrderFieldsComparer } from '@grafana/data/src/transformations/transformers/order';
|
||||||
|
import { useAllFieldNamesFromDataFrames } from './utils';
|
||||||
|
|
||||||
interface OrganizeFieldsTransformerEditorProps extends TransformerUIProps<OrganizeFieldsTransformerOptions> {}
|
interface OrganizeFieldsTransformerEditorProps extends TransformerUIProps<OrganizeFieldsTransformerOptions> {}
|
||||||
|
|
||||||
@ -21,7 +20,7 @@ const OrganizeFieldsTransformerEditor: React.FC<OrganizeFieldsTransformerEditorP
|
|||||||
const { options, input, onChange } = props;
|
const { options, input, onChange } = props;
|
||||||
const { indexByName, excludeByName, renameByName } = options;
|
const { indexByName, excludeByName, renameByName } = options;
|
||||||
|
|
||||||
const fieldNames = useMemo(() => getAllFieldNamesFromDataFrames(input), [input]);
|
const fieldNames = useAllFieldNamesFromDataFrames(input);
|
||||||
const orderedFieldNames = useMemo(() => orderFieldNamesByIndex(fieldNames, indexByName), [fieldNames, indexByName]);
|
const orderedFieldNames = useMemo(() => orderFieldNamesByIndex(fieldNames, indexByName), [fieldNames, indexByName]);
|
||||||
|
|
||||||
const onToggleVisibility = useCallback(
|
const onToggleVisibility = useCallback(
|
||||||
@ -205,26 +204,6 @@ const orderFieldNamesByIndex = (fieldNames: string[], indexByName: Record<string
|
|||||||
return fieldNames.sort(comparer);
|
return fieldNames.sort(comparer);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAllFieldNamesFromDataFrames = (input: DataFrame[]): string[] => {
|
|
||||||
if (!Array.isArray(input)) {
|
|
||||||
return [] as string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.keys(
|
|
||||||
input.reduce((names, frame) => {
|
|
||||||
if (!frame || !Array.isArray(frame.fields)) {
|
|
||||||
return names;
|
|
||||||
}
|
|
||||||
|
|
||||||
return frame.fields.reduce((names, field) => {
|
|
||||||
const t = getFieldDisplayName(field, frame, input);
|
|
||||||
names[t] = true;
|
|
||||||
return names;
|
|
||||||
}, names);
|
|
||||||
}, {} as Record<string, boolean>)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const organizeFieldsTransformRegistryItem: TransformerRegistryItem<OrganizeFieldsTransformerOptions> = {
|
export const organizeFieldsTransformRegistryItem: TransformerRegistryItem<OrganizeFieldsTransformerOptions> = {
|
||||||
id: DataTransformerID.organize,
|
id: DataTransformerID.organize,
|
||||||
editor: OrganizeFieldsTransformerEditor,
|
editor: OrganizeFieldsTransformerEditor,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
DataTransformerID,
|
DataTransformerID,
|
||||||
SelectableValue,
|
SelectableValue,
|
||||||
@ -6,18 +6,17 @@ import {
|
|||||||
TransformerRegistryItem,
|
TransformerRegistryItem,
|
||||||
TransformerUIProps,
|
TransformerUIProps,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { getAllFieldNamesFromDataFrames } from './OrganizeFieldsTransformerEditor';
|
|
||||||
import { Select } from '@grafana/ui';
|
import { Select } from '@grafana/ui';
|
||||||
|
|
||||||
import { SeriesToColumnsOptions } from '@grafana/data/src/transformations/transformers/seriesToColumns';
|
import { SeriesToColumnsOptions } from '@grafana/data/src/transformations/transformers/seriesToColumns';
|
||||||
|
import { useAllFieldNamesFromDataFrames } from './utils';
|
||||||
|
|
||||||
export const SeriesToFieldsTransformerEditor: React.FC<TransformerUIProps<SeriesToColumnsOptions>> = ({
|
export const SeriesToFieldsTransformerEditor: React.FC<TransformerUIProps<SeriesToColumnsOptions>> = ({
|
||||||
input,
|
input,
|
||||||
options,
|
options,
|
||||||
onChange,
|
onChange,
|
||||||
}) => {
|
}) => {
|
||||||
const fieldNames = useMemo(() => getAllFieldNamesFromDataFrames(input), [input]);
|
const fieldNames = useAllFieldNamesFromDataFrames(input).map((item: string) => ({ label: item, value: item }));
|
||||||
const fieldNameOptions = fieldNames.map((item: string) => ({ label: item, value: item }));
|
|
||||||
|
|
||||||
const onSelectField = useCallback(
|
const onSelectField = useCallback(
|
||||||
(value: SelectableValue<string>) => {
|
(value: SelectableValue<string>) => {
|
||||||
@ -33,7 +32,7 @@ export const SeriesToFieldsTransformerEditor: React.FC<TransformerUIProps<Series
|
|||||||
<div className="gf-form-inline">
|
<div className="gf-form-inline">
|
||||||
<div className="gf-form gf-form--grow">
|
<div className="gf-form gf-form--grow">
|
||||||
<div className="gf-form-label width-8">Field name</div>
|
<div className="gf-form-label width-8">Field name</div>
|
||||||
<Select options={fieldNameOptions} value={options.byField} onChange={onSelectField} isClearable />
|
<Select options={fieldNames} value={options.byField} onChange={onSelectField} isClearable />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,23 +1,15 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { DataTransformerID, standardTransformers, TransformerRegistryItem, TransformerUIProps } from '@grafana/data';
|
import { DataTransformerID, standardTransformers, TransformerRegistryItem, TransformerUIProps } from '@grafana/data';
|
||||||
import { getAllFieldNamesFromDataFrames } from './OrganizeFieldsTransformerEditor';
|
|
||||||
import { InlineField, InlineSwitch, InlineFieldRow, Select } from '@grafana/ui';
|
import { InlineField, InlineSwitch, InlineFieldRow, Select } from '@grafana/ui';
|
||||||
|
|
||||||
import { SortByField, SortByTransformerOptions } from '@grafana/data/src/transformations/transformers/sortBy';
|
import { SortByField, SortByTransformerOptions } from '@grafana/data/src/transformations/transformers/sortBy';
|
||||||
|
import { useAllFieldNamesFromDataFrames } from './utils';
|
||||||
|
|
||||||
export const SortByTransformerEditor: React.FC<TransformerUIProps<SortByTransformerOptions>> = ({
|
export const SortByTransformerEditor: React.FC<TransformerUIProps<SortByTransformerOptions>> = ({
|
||||||
input,
|
input,
|
||||||
options,
|
options,
|
||||||
onChange,
|
onChange,
|
||||||
}) => {
|
}) => {
|
||||||
const fieldNames = useMemo(
|
const fieldNames = useAllFieldNamesFromDataFrames(input).map((item: string) => ({ label: item, value: item }));
|
||||||
() =>
|
|
||||||
getAllFieldNamesFromDataFrames(input).map((n) => ({
|
|
||||||
value: n,
|
|
||||||
label: n,
|
|
||||||
})),
|
|
||||||
[input]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Only supports single sort for now
|
// Only supports single sort for now
|
||||||
const onSortChange = useCallback(
|
const onSortChange = useCallback(
|
||||||
@ -37,7 +29,7 @@ export const SortByTransformerEditor: React.FC<TransformerUIProps<SortByTransfor
|
|||||||
<InlineField label="Field" labelWidth={10} grow={true}>
|
<InlineField label="Field" labelWidth={10} grow={true}>
|
||||||
<Select
|
<Select
|
||||||
options={fieldNames}
|
options={fieldNames}
|
||||||
value={fieldNames.find((v) => v.value === s.field)}
|
value={s.field}
|
||||||
placeholder="Select field"
|
placeholder="Select field"
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
onSortChange(index, { ...s, field: v.value! });
|
onSortChange(index, { ...s, field: v.value! });
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { toDataFrame, FieldType } from '@grafana/data';
|
||||||
|
import { fireEvent, render, screen, getByText } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { Props, ConfigFromQueryTransformerEditor } from './ConfigFromQueryTransformerEditor';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'Name', type: FieldType.string, values: ['Temperature', 'Pressure'] },
|
||||||
|
{ name: 'Value', type: FieldType.number, values: [10, 200] },
|
||||||
|
{ name: 'Unit', type: FieldType.string, values: ['degree', 'pressurebar'] },
|
||||||
|
{ name: 'Miiin', type: FieldType.number, values: [3, 100] },
|
||||||
|
{ name: 'max', type: FieldType.string, values: [15, 200] },
|
||||||
|
],
|
||||||
|
refId: 'A',
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockOnChange = jest.fn();
|
||||||
|
|
||||||
|
const props: Props = {
|
||||||
|
input: [input],
|
||||||
|
onChange: mockOnChange,
|
||||||
|
options: {
|
||||||
|
mappings: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const setup = (testProps?: Partial<Props>) => {
|
||||||
|
const editorProps = { ...props, ...testProps };
|
||||||
|
return render(<ConfigFromQueryTransformerEditor {...editorProps} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ConfigFromQueryTransformerEditor', () => {
|
||||||
|
it('Should be able to select config frame by refId', async () => {
|
||||||
|
setup();
|
||||||
|
|
||||||
|
let select = (await screen.findByText('Config query')).nextSibling!;
|
||||||
|
await fireEvent.keyDown(select, { keyCode: 40 });
|
||||||
|
await userEvent.click(getByText(select as HTMLElement, 'A'));
|
||||||
|
|
||||||
|
expect(mockOnChange).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
configRefId: 'A',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,105 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
FieldMatcherID,
|
||||||
|
GrafanaTheme2,
|
||||||
|
PluginState,
|
||||||
|
SelectableValue,
|
||||||
|
TransformerRegistryItem,
|
||||||
|
TransformerUIProps,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { configFromDataTransformer, ConfigFromQueryTransformOptions } from './configFromQuery';
|
||||||
|
import { fieldMatchersUI, InlineField, InlineFieldRow, Select, useStyles2 } from '@grafana/ui';
|
||||||
|
import { FieldToConfigMappingEditor } from '../fieldToConfigMapping/FieldToConfigMappingEditor';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
|
export interface Props extends TransformerUIProps<ConfigFromQueryTransformOptions> {}
|
||||||
|
|
||||||
|
export function ConfigFromQueryTransformerEditor({ input, onChange, options }: Props) {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
const refIds = input
|
||||||
|
.map((x) => x.refId)
|
||||||
|
.filter((x) => x != null)
|
||||||
|
.map((x) => ({ label: x, value: x }));
|
||||||
|
|
||||||
|
const currentRefId = options.configRefId || 'config';
|
||||||
|
const currentMatcher = options.applyTo ?? { id: FieldMatcherID.byType, options: 'number' };
|
||||||
|
const matcherUI = fieldMatchersUI.get(currentMatcher.id);
|
||||||
|
const configFrame = input.find((x) => x.refId === currentRefId);
|
||||||
|
|
||||||
|
const onRefIdChange = (value: SelectableValue<string>) => {
|
||||||
|
onChange({
|
||||||
|
...options,
|
||||||
|
configRefId: value.value || 'config',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMatcherChange = (value: SelectableValue<string>) => {
|
||||||
|
onChange({ ...options, applyTo: { id: value.value! } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMatcherConfigChange = (matcherOption: any) => {
|
||||||
|
onChange({ ...options, applyTo: { id: currentMatcher.id, options: matcherOption } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const matchers = fieldMatchersUI
|
||||||
|
.list()
|
||||||
|
.filter((o) => !o.excludeFromPicker)
|
||||||
|
.map<SelectableValue<string>>((i) => ({ label: i.name, value: i.id, description: i.description }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<InlineFieldRow>
|
||||||
|
<InlineField label="Config query" labelWidth={20}>
|
||||||
|
<Select onChange={onRefIdChange} options={refIds} value={currentRefId} width={30} />
|
||||||
|
</InlineField>
|
||||||
|
</InlineFieldRow>
|
||||||
|
<InlineFieldRow>
|
||||||
|
<InlineField label="Apply to" labelWidth={20}>
|
||||||
|
<Select onChange={onMatcherChange} options={matchers} value={currentMatcher.id} width={30} />
|
||||||
|
</InlineField>
|
||||||
|
</InlineFieldRow>
|
||||||
|
<InlineFieldRow>
|
||||||
|
<InlineField label="Apply to options" labelWidth={20} className={styles.matcherOptions}>
|
||||||
|
<matcherUI.component
|
||||||
|
matcher={matcherUI.matcher}
|
||||||
|
data={input}
|
||||||
|
options={currentMatcher.options}
|
||||||
|
onChange={onMatcherConfigChange}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
</InlineFieldRow>
|
||||||
|
<InlineFieldRow>
|
||||||
|
{configFrame && (
|
||||||
|
<FieldToConfigMappingEditor
|
||||||
|
frame={configFrame}
|
||||||
|
mappings={options.mappings}
|
||||||
|
onChange={(mappings) => onChange({ ...options, mappings })}
|
||||||
|
withReducers
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</InlineFieldRow>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const configFromQueryTransformRegistryItem: TransformerRegistryItem<ConfigFromQueryTransformOptions> = {
|
||||||
|
id: configFromDataTransformer.id,
|
||||||
|
editor: ConfigFromQueryTransformerEditor,
|
||||||
|
transformation: configFromDataTransformer,
|
||||||
|
name: configFromDataTransformer.name,
|
||||||
|
description: configFromDataTransformer.description,
|
||||||
|
state: PluginState.beta,
|
||||||
|
help: `
|
||||||
|
### Use cases
|
||||||
|
|
||||||
|
Can take a query result and extract properties like min and max and apply it to the other query results.
|
||||||
|
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
matcherOptions: css`
|
||||||
|
min-width: 404px;
|
||||||
|
`,
|
||||||
|
});
|
@ -0,0 +1,157 @@
|
|||||||
|
import { toDataFrame, FieldType, ReducerID } from '@grafana/data';
|
||||||
|
import { FieldConfigHandlerKey } from '../fieldToConfigMapping/fieldToConfigMapping';
|
||||||
|
import { extractConfigFromQuery, ConfigFromQueryTransformOptions } from './configFromQuery';
|
||||||
|
|
||||||
|
describe('config from data', () => {
|
||||||
|
const config = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'Time', type: FieldType.time, values: [1, 2] },
|
||||||
|
{ name: 'Max', type: FieldType.number, values: [1, 10, 50] },
|
||||||
|
{ name: 'Min', type: FieldType.number, values: [1, 10, 5] },
|
||||||
|
{ name: 'Names', type: FieldType.string, values: ['first-name', 'middle', 'last-name'] },
|
||||||
|
],
|
||||||
|
refId: 'A',
|
||||||
|
});
|
||||||
|
|
||||||
|
const seriesA = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'Time', type: FieldType.time, values: [1, 2, 3] },
|
||||||
|
{
|
||||||
|
name: 'Value',
|
||||||
|
type: FieldType.number,
|
||||||
|
values: [2, 3, 4],
|
||||||
|
config: { displayName: 'SeriesA' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Select and apply with two frames and default mappings and reducer', () => {
|
||||||
|
const options: ConfigFromQueryTransformOptions = {
|
||||||
|
configRefId: 'A',
|
||||||
|
mappings: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = extractConfigFromQuery(options, [config, seriesA]);
|
||||||
|
expect(results.length).toBe(1);
|
||||||
|
expect(results[0].fields[1].config.max).toBe(50);
|
||||||
|
expect(results[0].fields[1].config.min).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can apply to config frame if there is only one frame', () => {
|
||||||
|
const options: ConfigFromQueryTransformOptions = {
|
||||||
|
configRefId: 'A',
|
||||||
|
mappings: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = extractConfigFromQuery(options, [config]);
|
||||||
|
expect(results.length).toBe(1);
|
||||||
|
expect(results[0].fields[1].name).toBe('Max');
|
||||||
|
expect(results[0].fields[1].config.max).toBe(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('With ignore mappings', () => {
|
||||||
|
const options: ConfigFromQueryTransformOptions = {
|
||||||
|
configRefId: 'A',
|
||||||
|
mappings: [{ fieldName: 'Min', handlerKey: FieldConfigHandlerKey.Ignore }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = extractConfigFromQuery(options, [config, seriesA]);
|
||||||
|
expect(results.length).toBe(1);
|
||||||
|
expect(results[0].fields[1].config.min).toEqual(undefined);
|
||||||
|
expect(results[0].fields[1].config.max).toEqual(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('With custom mappings', () => {
|
||||||
|
const options: ConfigFromQueryTransformOptions = {
|
||||||
|
configRefId: 'A',
|
||||||
|
mappings: [{ fieldName: 'Min', handlerKey: 'decimals' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = extractConfigFromQuery(options, [config, seriesA]);
|
||||||
|
expect(results.length).toBe(1);
|
||||||
|
expect(results[0].fields[1].config.decimals).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('With custom reducer', () => {
|
||||||
|
const options: ConfigFromQueryTransformOptions = {
|
||||||
|
configRefId: 'A',
|
||||||
|
mappings: [{ fieldName: 'Max', handlerKey: 'max', reducerId: ReducerID.min }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = extractConfigFromQuery(options, [config, seriesA]);
|
||||||
|
expect(results.length).toBe(1);
|
||||||
|
expect(results[0].fields[1].config.max).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('With custom matcher and displayName mapping', () => {
|
||||||
|
const options: ConfigFromQueryTransformOptions = {
|
||||||
|
configRefId: 'A',
|
||||||
|
mappings: [{ fieldName: 'Names', handlerKey: 'displayName', reducerId: ReducerID.first }],
|
||||||
|
applyTo: { id: 'byName', options: 'Value' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = extractConfigFromQuery(options, [config, seriesA]);
|
||||||
|
expect(results.length).toBe(1);
|
||||||
|
expect(results[0].fields[1].config.displayName).toBe('first-name');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('value mapping from data', () => {
|
||||||
|
const config = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'value', type: FieldType.number, values: [1, 2, 3] },
|
||||||
|
{ name: 'text', type: FieldType.string, values: ['one', 'two', 'three'] },
|
||||||
|
{ name: 'color', type: FieldType.string, values: ['red', 'blue', 'green'] },
|
||||||
|
],
|
||||||
|
refId: 'config',
|
||||||
|
});
|
||||||
|
|
||||||
|
const seriesA = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'Time', type: FieldType.time, values: [1, 2, 3] },
|
||||||
|
{
|
||||||
|
name: 'Value',
|
||||||
|
type: FieldType.number,
|
||||||
|
values: [1, 2, 3],
|
||||||
|
config: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should take all field values and map to value mappings', () => {
|
||||||
|
const options: ConfigFromQueryTransformOptions = {
|
||||||
|
configRefId: 'config',
|
||||||
|
mappings: [
|
||||||
|
{ fieldName: 'value', handlerKey: 'mappings.value' },
|
||||||
|
{ fieldName: 'color', handlerKey: 'mappings.color' },
|
||||||
|
{ fieldName: 'text', handlerKey: 'mappings.text' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = extractConfigFromQuery(options, [config, seriesA]);
|
||||||
|
expect(results[0].fields[1].config.mappings).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"options": Object {
|
||||||
|
"1": Object {
|
||||||
|
"color": "red",
|
||||||
|
"index": 0,
|
||||||
|
"text": "one",
|
||||||
|
},
|
||||||
|
"2": Object {
|
||||||
|
"color": "blue",
|
||||||
|
"index": 1,
|
||||||
|
"text": "two",
|
||||||
|
},
|
||||||
|
"3": Object {
|
||||||
|
"color": "green",
|
||||||
|
"index": 2,
|
||||||
|
"text": "three",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"type": "value",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,105 @@
|
|||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import {
|
||||||
|
ArrayVector,
|
||||||
|
DataFrame,
|
||||||
|
DataTransformerID,
|
||||||
|
DataTransformerInfo,
|
||||||
|
FieldMatcherID,
|
||||||
|
getFieldDisplayName,
|
||||||
|
getFieldMatcher,
|
||||||
|
MatcherConfig,
|
||||||
|
reduceField,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import {
|
||||||
|
getFieldConfigFromFrame,
|
||||||
|
FieldToConfigMapping,
|
||||||
|
evaluteFieldMappings,
|
||||||
|
} from '../fieldToConfigMapping/fieldToConfigMapping';
|
||||||
|
|
||||||
|
export interface ConfigFromQueryTransformOptions {
|
||||||
|
configRefId?: string;
|
||||||
|
mappings: FieldToConfigMapping[];
|
||||||
|
applyTo?: MatcherConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractConfigFromQuery(options: ConfigFromQueryTransformOptions, data: DataFrame[]) {
|
||||||
|
let configFrame: DataFrame | null = null;
|
||||||
|
|
||||||
|
for (const frame of data) {
|
||||||
|
if (frame.refId === options.configRefId) {
|
||||||
|
configFrame = frame;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!configFrame) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reducedConfigFrame: DataFrame = {
|
||||||
|
fields: [],
|
||||||
|
length: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mappingResult = evaluteFieldMappings(configFrame, options.mappings ?? [], false);
|
||||||
|
|
||||||
|
// reduce config frame
|
||||||
|
for (const field of configFrame.fields) {
|
||||||
|
const newField = { ...field };
|
||||||
|
const fieldName = getFieldDisplayName(field, configFrame);
|
||||||
|
const fieldMapping = mappingResult.index[fieldName];
|
||||||
|
const result = reduceField({ field, reducers: [fieldMapping.reducerId] });
|
||||||
|
newField.values = new ArrayVector([result[fieldMapping.reducerId]]);
|
||||||
|
reducedConfigFrame.fields.push(newField);
|
||||||
|
}
|
||||||
|
|
||||||
|
const output: DataFrame[] = [];
|
||||||
|
const matcher = getFieldMatcher(options.applyTo || { id: FieldMatcherID.numeric });
|
||||||
|
|
||||||
|
for (const frame of data) {
|
||||||
|
// Skip config frame in output
|
||||||
|
if (frame === configFrame && data.length > 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputFrame: DataFrame = {
|
||||||
|
fields: [],
|
||||||
|
length: frame.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const field of frame.fields) {
|
||||||
|
if (matcher(field, frame, data)) {
|
||||||
|
const dataConfig = getFieldConfigFromFrame(reducedConfigFrame, 0, mappingResult);
|
||||||
|
outputFrame.fields.push({
|
||||||
|
...field,
|
||||||
|
config: {
|
||||||
|
...field.config,
|
||||||
|
...dataConfig,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
outputFrame.fields.push(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output.push(outputFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const configFromDataTransformer: DataTransformerInfo<ConfigFromQueryTransformOptions> = {
|
||||||
|
id: DataTransformerID.configFromData,
|
||||||
|
name: 'Config from query results',
|
||||||
|
description: 'Set unit, min, max and more from data',
|
||||||
|
defaultOptions: {
|
||||||
|
configRefId: 'config',
|
||||||
|
mappings: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a modified copy of the series. If the transform is not or should not
|
||||||
|
* be applied, just return the input series
|
||||||
|
*/
|
||||||
|
operator: (options) => (source) => source.pipe(map((data) => extractConfigFromQuery(options, data))),
|
||||||
|
};
|
@ -0,0 +1,89 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { toDataFrame, FieldType } from '@grafana/data';
|
||||||
|
import { fireEvent, render, screen, getByText, getByLabelText } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { Props, FieldToConfigMappingEditor } from './FieldToConfigMappingEditor';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const frame = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'Unit', type: FieldType.string, values: ['degree', 'pressurebar'] },
|
||||||
|
{ name: 'Miiin', type: FieldType.number, values: [3, 100] },
|
||||||
|
{ name: 'max', type: FieldType.string, values: [15, 200] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockOnChange = jest.fn();
|
||||||
|
|
||||||
|
const props: Props = {
|
||||||
|
frame: frame,
|
||||||
|
onChange: mockOnChange,
|
||||||
|
mappings: [],
|
||||||
|
withReducers: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const setup = (testProps?: Partial<Props>) => {
|
||||||
|
const editorProps = { ...props, ...testProps };
|
||||||
|
return render(<FieldToConfigMappingEditor {...editorProps} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('FieldToConfigMappingEditor', () => {
|
||||||
|
it('Should render fields', async () => {
|
||||||
|
setup();
|
||||||
|
|
||||||
|
expect(await screen.findByText('Unit')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByText('Miiin')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByText('max')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByText('Max (auto)')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can change mapping', async () => {
|
||||||
|
setup();
|
||||||
|
|
||||||
|
const select = (await screen.findByTestId('Miiin-config-key')).childNodes[0];
|
||||||
|
await fireEvent.keyDown(select, { keyCode: 40 });
|
||||||
|
await userEvent.click(getByText(select as HTMLElement, 'Min'));
|
||||||
|
|
||||||
|
expect(mockOnChange).toHaveBeenCalledWith(expect.arrayContaining([{ fieldName: 'Miiin', handlerKey: 'min' }]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can remove added mapping', async () => {
|
||||||
|
setup({ mappings: [{ fieldName: 'max', handlerKey: 'min' }] });
|
||||||
|
|
||||||
|
const select = (await screen.findByTestId('max-config-key')).childNodes[0];
|
||||||
|
await userEvent.click(getByLabelText(select as HTMLElement, 'select-clear-value'));
|
||||||
|
|
||||||
|
expect(mockOnChange).toHaveBeenCalledWith(expect.arrayContaining([]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Automatic mapping is shown as placeholder', async () => {
|
||||||
|
setup({ mappings: [] });
|
||||||
|
|
||||||
|
const select = await screen.findByText('Max (auto)');
|
||||||
|
expect(select).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should show correct default reducer', async () => {
|
||||||
|
setup({ mappings: [{ fieldName: 'max', handlerKey: 'mappings.value' }] });
|
||||||
|
|
||||||
|
const reducer = await screen.findByTestId('max-reducer');
|
||||||
|
|
||||||
|
expect(getByText(reducer, 'All values')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can change reducer', async () => {
|
||||||
|
setup();
|
||||||
|
|
||||||
|
const reducer = await (await screen.findByTestId('max-reducer')).childNodes[0];
|
||||||
|
|
||||||
|
await fireEvent.keyDown(reducer, { keyCode: 40 });
|
||||||
|
await userEvent.click(getByText(reducer as HTMLElement, 'Last'));
|
||||||
|
|
||||||
|
expect(mockOnChange).toHaveBeenCalledWith(
|
||||||
|
expect.arrayContaining([{ fieldName: 'max', handlerKey: 'max', reducerId: 'last' }])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,197 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { DataFrame, getFieldDisplayName, GrafanaTheme2, ReducerID, SelectableValue } from '@grafana/data';
|
||||||
|
import { Select, StatsPicker, useStyles2 } from '@grafana/ui';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import {
|
||||||
|
configMapHandlers,
|
||||||
|
evaluteFieldMappings,
|
||||||
|
FieldToConfigMapHandler,
|
||||||
|
FieldToConfigMapping,
|
||||||
|
lookUpConfigHandler as findConfigHandlerFor,
|
||||||
|
} from '../fieldToConfigMapping/fieldToConfigMapping';
|
||||||
|
import { capitalize } from 'lodash';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
frame: DataFrame;
|
||||||
|
mappings: FieldToConfigMapping[];
|
||||||
|
onChange: (mappings: FieldToConfigMapping[]) => void;
|
||||||
|
withReducers?: boolean;
|
||||||
|
withNameAndValue?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FieldToConfigMappingEditor({ frame, mappings, onChange, withReducers, withNameAndValue }: Props) {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const rows = getViewModelRows(frame, mappings, withNameAndValue);
|
||||||
|
const configProps = configMapHandlers.map((def) => configHandlerToSelectOption(def, false)) as Array<
|
||||||
|
SelectableValue<string>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const onChangeConfigProperty = (row: FieldToConfigRowViewModel, value: SelectableValue<string | null>) => {
|
||||||
|
const existingIdx = mappings.findIndex((x) => x.fieldName === row.fieldName);
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
if (existingIdx !== -1) {
|
||||||
|
const update = [...mappings];
|
||||||
|
update.splice(existingIdx, 1, { ...mappings[existingIdx], handlerKey: value.value! });
|
||||||
|
onChange(update);
|
||||||
|
} else {
|
||||||
|
onChange([...mappings, { fieldName: row.fieldName, handlerKey: value.value! }]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (existingIdx !== -1) {
|
||||||
|
onChange(mappings.filter((x, index) => index !== existingIdx));
|
||||||
|
} else {
|
||||||
|
onChange([...mappings, { fieldName: row.fieldName, handlerKey: '__ignore' }]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeReducer = (row: FieldToConfigRowViewModel, reducerId: ReducerID) => {
|
||||||
|
const existingIdx = mappings.findIndex((x) => x.fieldName === row.fieldName);
|
||||||
|
|
||||||
|
if (existingIdx !== -1) {
|
||||||
|
const update = [...mappings];
|
||||||
|
update.splice(existingIdx, 1, { ...mappings[existingIdx], reducerId });
|
||||||
|
onChange(update);
|
||||||
|
} else {
|
||||||
|
onChange([...mappings, { fieldName: row.fieldName, handlerKey: row.handlerKey, reducerId }]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className={styles.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Field</th>
|
||||||
|
<th>Use as</th>
|
||||||
|
{withReducers && <th>Select</th>}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows.map((row) => (
|
||||||
|
<tr key={row.fieldName}>
|
||||||
|
<td className={styles.labelCell}>{row.fieldName}</td>
|
||||||
|
<td className={styles.selectCell} data-testid={`${row.fieldName}-config-key`}>
|
||||||
|
<Select
|
||||||
|
options={configProps}
|
||||||
|
value={row.configOption}
|
||||||
|
placeholder={row.placeholder}
|
||||||
|
isClearable={true}
|
||||||
|
onChange={(value) => onChangeConfigProperty(row, value)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{withReducers && (
|
||||||
|
<td data-testid={`${row.fieldName}-reducer`} className={styles.selectCell}>
|
||||||
|
<StatsPicker
|
||||||
|
stats={[row.reducerId]}
|
||||||
|
defaultStat={row.reducerId}
|
||||||
|
onChange={(stats: string[]) => onChangeReducer(row, stats[0] as ReducerID)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FieldToConfigRowViewModel {
|
||||||
|
handlerKey: string | null;
|
||||||
|
fieldName: string;
|
||||||
|
configOption: SelectableValue<string | null> | null;
|
||||||
|
placeholder?: string;
|
||||||
|
missingInFrame?: boolean;
|
||||||
|
reducerId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getViewModelRows(
|
||||||
|
frame: DataFrame,
|
||||||
|
mappings: FieldToConfigMapping[],
|
||||||
|
withNameAndValue?: boolean
|
||||||
|
): FieldToConfigRowViewModel[] {
|
||||||
|
const rows: FieldToConfigRowViewModel[] = [];
|
||||||
|
const mappingResult = evaluteFieldMappings(frame, mappings ?? [], withNameAndValue);
|
||||||
|
|
||||||
|
for (const field of frame.fields) {
|
||||||
|
const fieldName = getFieldDisplayName(field, frame);
|
||||||
|
const mapping = mappingResult.index[fieldName];
|
||||||
|
const option = configHandlerToSelectOption(mapping.handler, mapping.automatic);
|
||||||
|
|
||||||
|
rows.push({
|
||||||
|
fieldName,
|
||||||
|
configOption: mapping.automatic ? null : option,
|
||||||
|
placeholder: mapping.automatic ? option?.label : 'Choose',
|
||||||
|
handlerKey: mapping.handler?.key ?? null,
|
||||||
|
reducerId: mapping.reducerId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add rows for mappings that have no matching field
|
||||||
|
for (const mapping of mappings) {
|
||||||
|
if (!rows.find((x) => x.fieldName === mapping.fieldName)) {
|
||||||
|
const handler = findConfigHandlerFor(mapping.handlerKey);
|
||||||
|
|
||||||
|
rows.push({
|
||||||
|
fieldName: mapping.fieldName,
|
||||||
|
handlerKey: mapping.handlerKey,
|
||||||
|
configOption: configHandlerToSelectOption(handler, false),
|
||||||
|
missingInFrame: true,
|
||||||
|
reducerId: mapping.reducerId ?? ReducerID.lastNotNull,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
function configHandlerToSelectOption(
|
||||||
|
def: FieldToConfigMapHandler | null,
|
||||||
|
isAutomatic: boolean
|
||||||
|
): SelectableValue<string> | null {
|
||||||
|
if (!def) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = def.name ?? capitalize(def.key);
|
||||||
|
|
||||||
|
if (isAutomatic) {
|
||||||
|
name = `${name} (auto)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: name,
|
||||||
|
value: def.key,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
table: css`
|
||||||
|
margin-top: ${theme.spacing(1)};
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
border-right: 4px solid ${theme.colors.background.primary};
|
||||||
|
border-bottom: 4px solid ${theme.colors.background.primary};
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
font-size: ${theme.typography.bodySmall.fontSize};
|
||||||
|
line-height: ${theme.spacing(4)};
|
||||||
|
padding: ${theme.spacing(0, 1)};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
labelCell: css`
|
||||||
|
font-size: ${theme.typography.bodySmall.fontSize};
|
||||||
|
background: ${theme.colors.background.secondary};
|
||||||
|
padding: ${theme.spacing(0, 1)};
|
||||||
|
max-width: 400px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 140px;
|
||||||
|
`,
|
||||||
|
selectCell: css`
|
||||||
|
padding: 0;
|
||||||
|
min-width: 161px;
|
||||||
|
`,
|
||||||
|
});
|
@ -0,0 +1,344 @@
|
|||||||
|
import {
|
||||||
|
anyToNumber,
|
||||||
|
DataFrame,
|
||||||
|
FieldColorModeId,
|
||||||
|
FieldConfig,
|
||||||
|
getFieldDisplayName,
|
||||||
|
MappingType,
|
||||||
|
ReducerID,
|
||||||
|
ThresholdsMode,
|
||||||
|
ValueMapping,
|
||||||
|
ValueMap,
|
||||||
|
Field,
|
||||||
|
FieldType,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { isArray } from 'lodash';
|
||||||
|
|
||||||
|
export interface FieldToConfigMapping {
|
||||||
|
fieldName: string;
|
||||||
|
reducerId?: ReducerID;
|
||||||
|
handlerKey: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms a frame with fields to a map of field configs
|
||||||
|
*
|
||||||
|
* Input
|
||||||
|
* | Unit | Min | Max |
|
||||||
|
* --------------------------------
|
||||||
|
* | Temperature | 0 | 30 |
|
||||||
|
* | Pressure | 0 | 100 |
|
||||||
|
*
|
||||||
|
* Outputs
|
||||||
|
* {
|
||||||
|
{ min: 0, max: 100 },
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function getFieldConfigFromFrame(
|
||||||
|
frame: DataFrame,
|
||||||
|
rowIndex: number,
|
||||||
|
evaluatedMappings: EvaluatedMappingResult
|
||||||
|
): FieldConfig {
|
||||||
|
const config: FieldConfig = {};
|
||||||
|
const context: FieldToConfigContext = {};
|
||||||
|
|
||||||
|
for (const field of frame.fields) {
|
||||||
|
const fieldName = getFieldDisplayName(field, frame);
|
||||||
|
const mapping = evaluatedMappings.index[fieldName];
|
||||||
|
const handler = mapping.handler;
|
||||||
|
|
||||||
|
if (!handler) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configValue = field.values.get(rowIndex);
|
||||||
|
|
||||||
|
if (configValue === null || configValue === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newValue = handler.processor(configValue, config, context);
|
||||||
|
if (newValue != null) {
|
||||||
|
(config as any)[handler.targetProperty ?? handler.key] = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.mappingValues) {
|
||||||
|
config.mappings = combineValueMappings(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FieldToConfigContext {
|
||||||
|
mappingValues?: any[];
|
||||||
|
mappingColors?: string[];
|
||||||
|
mappingTexts?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldToConfigMapHandlerProcessor = (value: any, config: FieldConfig, context: FieldToConfigContext) => any;
|
||||||
|
|
||||||
|
export interface FieldToConfigMapHandler {
|
||||||
|
key: string;
|
||||||
|
targetProperty?: string;
|
||||||
|
name?: string;
|
||||||
|
processor: FieldToConfigMapHandlerProcessor;
|
||||||
|
defaultReducer?: ReducerID;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FieldConfigHandlerKey {
|
||||||
|
Name = 'field.name',
|
||||||
|
Value = 'field.value',
|
||||||
|
Label = 'field.label',
|
||||||
|
Ignore = '__ignore',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const configMapHandlers: FieldToConfigMapHandler[] = [
|
||||||
|
{
|
||||||
|
key: FieldConfigHandlerKey.Name,
|
||||||
|
name: 'Field name',
|
||||||
|
processor: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: FieldConfigHandlerKey.Value,
|
||||||
|
name: 'Field value',
|
||||||
|
processor: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: FieldConfigHandlerKey.Label,
|
||||||
|
name: 'Field label',
|
||||||
|
processor: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: FieldConfigHandlerKey.Ignore,
|
||||||
|
name: 'Ignore',
|
||||||
|
processor: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'max',
|
||||||
|
processor: toNumericOrUndefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'min',
|
||||||
|
processor: toNumericOrUndefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'unit',
|
||||||
|
processor: (value) => value.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'decimals',
|
||||||
|
processor: toNumericOrUndefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'displayName',
|
||||||
|
name: 'Display name',
|
||||||
|
processor: (value: any) => value.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'color',
|
||||||
|
processor: (value) => ({ fixedColor: value, mode: FieldColorModeId.Fixed }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'threshold1',
|
||||||
|
targetProperty: 'thresholds',
|
||||||
|
processor: (value, config) => {
|
||||||
|
const numeric = anyToNumber(value);
|
||||||
|
|
||||||
|
if (isNaN(numeric)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.thresholds) {
|
||||||
|
config.thresholds = {
|
||||||
|
mode: ThresholdsMode.Absolute,
|
||||||
|
steps: [{ value: -Infinity, color: 'green' }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
config.thresholds.steps.push({
|
||||||
|
value: numeric,
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
|
||||||
|
return config.thresholds;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'mappings.value',
|
||||||
|
name: 'Value mappings / Value',
|
||||||
|
targetProperty: 'mappings',
|
||||||
|
defaultReducer: ReducerID.allValues,
|
||||||
|
processor: (value, config, context) => {
|
||||||
|
if (!isArray(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.mappingValues = value;
|
||||||
|
return config.mappings;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'mappings.color',
|
||||||
|
name: 'Value mappings / Color',
|
||||||
|
targetProperty: 'mappings',
|
||||||
|
defaultReducer: ReducerID.allValues,
|
||||||
|
processor: (value, config, context) => {
|
||||||
|
if (!isArray(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.mappingColors = value;
|
||||||
|
return config.mappings;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'mappings.text',
|
||||||
|
name: 'Value mappings / Display text',
|
||||||
|
targetProperty: 'mappings',
|
||||||
|
defaultReducer: ReducerID.allValues,
|
||||||
|
processor: (value, config, context) => {
|
||||||
|
if (!isArray(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.mappingTexts = value;
|
||||||
|
return config.mappings;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function combineValueMappings(context: FieldToConfigContext): ValueMapping[] {
|
||||||
|
const valueMap: ValueMap = {
|
||||||
|
type: MappingType.ValueToText,
|
||||||
|
options: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!context.mappingValues) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < context.mappingValues.length; i++) {
|
||||||
|
const value = context.mappingValues[i];
|
||||||
|
if (value != null) {
|
||||||
|
valueMap.options[value.toString()] = {
|
||||||
|
color: context.mappingColors && context.mappingColors[i],
|
||||||
|
text: context.mappingTexts && context.mappingTexts[i],
|
||||||
|
index: i,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [valueMap];
|
||||||
|
}
|
||||||
|
|
||||||
|
let configMapHandlersIndex: Record<string, FieldToConfigMapHandler> | null = null;
|
||||||
|
|
||||||
|
export function getConfigMapHandlersIndex() {
|
||||||
|
if (configMapHandlersIndex === null) {
|
||||||
|
configMapHandlersIndex = {};
|
||||||
|
for (const def of configMapHandlers) {
|
||||||
|
configMapHandlersIndex[def.key] = def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return configMapHandlersIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNumericOrUndefined(value: any) {
|
||||||
|
const numeric = anyToNumber(value);
|
||||||
|
|
||||||
|
if (isNaN(numeric)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return numeric;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConfigHandlerKeyForField(fieldName: string, mappings: FieldToConfigMapping[]) {
|
||||||
|
for (const map of mappings) {
|
||||||
|
if (fieldName === map.fieldName) {
|
||||||
|
return map.handlerKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fieldName.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lookUpConfigHandler(key: string | null): FieldToConfigMapHandler | null {
|
||||||
|
if (!key) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getConfigMapHandlersIndex()[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EvaluatedMapping {
|
||||||
|
automatic: boolean;
|
||||||
|
handler: FieldToConfigMapHandler | null;
|
||||||
|
reducerId: ReducerID;
|
||||||
|
}
|
||||||
|
export interface EvaluatedMappingResult {
|
||||||
|
index: Record<string, EvaluatedMapping>;
|
||||||
|
nameField?: Field;
|
||||||
|
valueField?: Field;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function evaluteFieldMappings(
|
||||||
|
frame: DataFrame,
|
||||||
|
mappings: FieldToConfigMapping[],
|
||||||
|
withNameAndValue?: boolean
|
||||||
|
): EvaluatedMappingResult {
|
||||||
|
const result: EvaluatedMappingResult = {
|
||||||
|
index: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Look up name and value field in mappings
|
||||||
|
let nameFieldMappping = mappings.find((x) => x.handlerKey === FieldConfigHandlerKey.Name);
|
||||||
|
let valueFieldMapping = mappings.find((x) => x.handlerKey === FieldConfigHandlerKey.Value);
|
||||||
|
|
||||||
|
for (const field of frame.fields) {
|
||||||
|
const fieldName = getFieldDisplayName(field, frame);
|
||||||
|
const mapping = mappings.find((x) => x.fieldName === fieldName);
|
||||||
|
const key = mapping ? mapping.handlerKey : fieldName.toLowerCase();
|
||||||
|
let handler = lookUpConfigHandler(key);
|
||||||
|
|
||||||
|
// Name and value handlers are a special as their auto logic is based on first matching criteria
|
||||||
|
if (withNameAndValue) {
|
||||||
|
// If we have a handler it means manually specified field
|
||||||
|
if (handler) {
|
||||||
|
if (handler.key === FieldConfigHandlerKey.Name) {
|
||||||
|
result.nameField = field;
|
||||||
|
}
|
||||||
|
if (handler.key === FieldConfigHandlerKey.Value) {
|
||||||
|
result.valueField = field;
|
||||||
|
}
|
||||||
|
} else if (!mapping) {
|
||||||
|
// We have no name field and no mapping for it, pick first string
|
||||||
|
if (!result.nameField && !nameFieldMappping && field.type === FieldType.string) {
|
||||||
|
result.nameField = field;
|
||||||
|
handler = lookUpConfigHandler(FieldConfigHandlerKey.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.valueField && !valueFieldMapping && field.type === FieldType.number) {
|
||||||
|
result.valueField = field;
|
||||||
|
handler = lookUpConfigHandler(FieldConfigHandlerKey.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no handle and when in name and value mode (Rows to fields) default to labels
|
||||||
|
if (!handler && withNameAndValue) {
|
||||||
|
handler = lookUpConfigHandler(FieldConfigHandlerKey.Label);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.index[fieldName] = {
|
||||||
|
automatic: !mapping,
|
||||||
|
handler: handler,
|
||||||
|
reducerId: mapping?.reducerId ?? handler?.defaultReducer ?? ReducerID.lastNotNull,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { toDataFrame, FieldType } from '@grafana/data';
|
||||||
|
import { fireEvent, render, screen, getByText } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { Props, RowsToFieldsTransformerEditor } from './RowsToFieldsTransformerEditor';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'Name', type: FieldType.string, values: ['Temperature', 'Pressure'] },
|
||||||
|
{ name: 'Value', type: FieldType.number, values: [10, 200] },
|
||||||
|
{ name: 'Unit', type: FieldType.string, values: ['degree', 'pressurebar'] },
|
||||||
|
{ name: 'Miiin', type: FieldType.number, values: [3, 100] },
|
||||||
|
{ name: 'max', type: FieldType.string, values: [15, 200] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockOnChange = jest.fn();
|
||||||
|
|
||||||
|
const props: Props = {
|
||||||
|
input: [input],
|
||||||
|
onChange: mockOnChange,
|
||||||
|
options: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const setup = (testProps?: Partial<Props>) => {
|
||||||
|
const editorProps = { ...props, ...testProps };
|
||||||
|
return render(<RowsToFieldsTransformerEditor {...editorProps} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('RowsToFieldsTransformerEditor', () => {
|
||||||
|
it('Should be able to select name field', async () => {
|
||||||
|
setup();
|
||||||
|
|
||||||
|
const select = (await screen.findByTestId('Name-config-key')).childNodes[0];
|
||||||
|
await fireEvent.keyDown(select, { keyCode: 40 });
|
||||||
|
await userEvent.click(getByText(select as HTMLElement, 'Field name'));
|
||||||
|
|
||||||
|
expect(mockOnChange).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
mappings: [{ fieldName: 'Name', handlerKey: 'field.name' }],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should be able to select value field', async () => {
|
||||||
|
setup();
|
||||||
|
|
||||||
|
const select = (await screen.findByTestId('Value-config-key')).childNodes[0];
|
||||||
|
await fireEvent.keyDown(select, { keyCode: 40 });
|
||||||
|
await userEvent.click(getByText(select as HTMLElement, 'Field value'));
|
||||||
|
|
||||||
|
expect(mockOnChange).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
mappings: [{ fieldName: 'Value', handlerKey: 'field.value' }],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,68 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PluginState, TransformerRegistryItem, TransformerUIProps } from '@grafana/data';
|
||||||
|
import { rowsToFieldsTransformer, RowToFieldsTransformOptions } from './rowsToFields';
|
||||||
|
import { FieldToConfigMappingEditor } from '../fieldToConfigMapping/FieldToConfigMappingEditor';
|
||||||
|
|
||||||
|
export interface Props extends TransformerUIProps<RowToFieldsTransformOptions> {}
|
||||||
|
|
||||||
|
export function RowsToFieldsTransformerEditor({ input, options, onChange }: Props) {
|
||||||
|
if (input.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FieldToConfigMappingEditor
|
||||||
|
frame={input[0]}
|
||||||
|
mappings={options.mappings ?? []}
|
||||||
|
onChange={(mappings) => onChange({ ...options, mappings })}
|
||||||
|
withNameAndValue={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rowsToFieldsTransformRegistryItem: TransformerRegistryItem<RowToFieldsTransformOptions> = {
|
||||||
|
id: rowsToFieldsTransformer.id,
|
||||||
|
editor: RowsToFieldsTransformerEditor,
|
||||||
|
transformation: rowsToFieldsTransformer,
|
||||||
|
name: rowsToFieldsTransformer.name,
|
||||||
|
description: rowsToFieldsTransformer.description,
|
||||||
|
state: PluginState.beta,
|
||||||
|
help: `
|
||||||
|
### Use cases
|
||||||
|
|
||||||
|
This transformation transforms rows into separate fields. This can be useful as fields can be styled
|
||||||
|
and configured individually, something rows cannot. It can also use additional fields as sources for
|
||||||
|
data driven configuration or as sources for field labels. The additional labels can then be used to
|
||||||
|
define better display names for the resulting fields.
|
||||||
|
|
||||||
|
Useful when visualization data in:
|
||||||
|
* Gauge
|
||||||
|
* Stat
|
||||||
|
* Pie chart
|
||||||
|
|
||||||
|
### Configuration overview
|
||||||
|
|
||||||
|
* Select one field to use as the source of names for the new fields.
|
||||||
|
* Select one field to use as the values for the fields.
|
||||||
|
* Optionally map extra fields to config properties like min and max.
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
Input:
|
||||||
|
|
||||||
|
Name | Value | Max
|
||||||
|
--------|-------|------
|
||||||
|
ServerA | 10 | 100
|
||||||
|
ServerB | 20 | 200
|
||||||
|
ServerC | 30 | 300
|
||||||
|
|
||||||
|
Output:
|
||||||
|
|
||||||
|
ServerA (max=100) | ServerB (max=200) | ServerC (max=300)
|
||||||
|
------------------|------------------ | ------------------
|
||||||
|
10 | 20 | 30
|
||||||
|
|
||||||
|
`,
|
||||||
|
};
|
@ -0,0 +1,154 @@
|
|||||||
|
import { toDataFrame, FieldType } from '@grafana/data';
|
||||||
|
import { rowsToFields } from './rowsToFields';
|
||||||
|
|
||||||
|
describe('Rows to fields', () => {
|
||||||
|
it('Will extract min & max from field', () => {
|
||||||
|
const input = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'Name', type: FieldType.string, values: ['Temperature', 'Pressure'] },
|
||||||
|
{ name: 'Value', type: FieldType.number, values: [10, 200] },
|
||||||
|
{ name: 'Unit', type: FieldType.string, values: ['degree', 'pressurebar'] },
|
||||||
|
{ name: 'Miiin', type: FieldType.number, values: [3, 100] },
|
||||||
|
{ name: 'max', type: FieldType.string, values: [15, 200] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = rowsToFields(
|
||||||
|
{
|
||||||
|
mappings: [
|
||||||
|
{
|
||||||
|
fieldName: 'Miiin',
|
||||||
|
handlerKey: 'min',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
input
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toMatchInlineSnapshot(`
|
||||||
|
Object {
|
||||||
|
"fields": Array [
|
||||||
|
Object {
|
||||||
|
"config": Object {
|
||||||
|
"max": 15,
|
||||||
|
"min": 3,
|
||||||
|
"unit": "degree",
|
||||||
|
},
|
||||||
|
"labels": Object {},
|
||||||
|
"name": "Temperature",
|
||||||
|
"type": "number",
|
||||||
|
"values": Array [
|
||||||
|
10,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"config": Object {
|
||||||
|
"max": 200,
|
||||||
|
"min": 100,
|
||||||
|
"unit": "pressurebar",
|
||||||
|
},
|
||||||
|
"labels": Object {},
|
||||||
|
"name": "Pressure",
|
||||||
|
"type": "number",
|
||||||
|
"values": Array [
|
||||||
|
200,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"length": 1,
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can handle custom name and value field mapping', () => {
|
||||||
|
const input = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'Name', type: FieldType.string, values: ['Ignore'] },
|
||||||
|
{ name: 'SensorName', type: FieldType.string, values: ['Temperature'] },
|
||||||
|
{ name: 'Value', type: FieldType.number, values: [10] },
|
||||||
|
{ name: 'SensorReading', type: FieldType.number, values: [100] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = rowsToFields(
|
||||||
|
{
|
||||||
|
mappings: [
|
||||||
|
{ fieldName: 'SensorName', handlerKey: 'field.name' },
|
||||||
|
{ fieldName: 'SensorReading', handlerKey: 'field.value' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
input
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.fields[0].name).toBe('Temperature');
|
||||||
|
expect(result.fields[0].config).toEqual({});
|
||||||
|
expect(result.fields[0].values.get(0)).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can handle colors', () => {
|
||||||
|
const input = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'Name', type: FieldType.string, values: ['Temperature'] },
|
||||||
|
{ name: 'Value', type: FieldType.number, values: [10] },
|
||||||
|
{ name: 'Color', type: FieldType.string, values: ['blue'] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = rowsToFields({}, input);
|
||||||
|
|
||||||
|
expect(result.fields[0].config.color?.fixedColor).toBe('blue');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can handle thresholds', () => {
|
||||||
|
const input = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'Name', type: FieldType.string, values: ['Temperature'] },
|
||||||
|
{ name: 'Value', type: FieldType.number, values: [10] },
|
||||||
|
{ name: 'threshold1', type: FieldType.string, values: [30] },
|
||||||
|
{ name: 'threshold2', type: FieldType.string, values: [500] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = rowsToFields({}, input);
|
||||||
|
expect(result.fields[0].config.thresholds?.steps[1].value).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Will extract other string fields to labels', () => {
|
||||||
|
const input = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'Name', type: FieldType.string, values: ['Temperature', 'Pressure'] },
|
||||||
|
{ name: 'Value', type: FieldType.number, values: [10, 200] },
|
||||||
|
{ name: 'City', type: FieldType.string, values: ['Stockholm', 'New York'] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = rowsToFields({}, input);
|
||||||
|
|
||||||
|
expect(result.fields[0].labels).toEqual({ City: 'Stockholm' });
|
||||||
|
expect(result.fields[1].labels).toEqual({ City: 'New York' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can ignore field as auto picked for value or name', () => {
|
||||||
|
const input = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'Name', type: FieldType.string, values: ['Temperature'] },
|
||||||
|
{ name: 'Value', type: FieldType.number, values: [10] },
|
||||||
|
{ name: 'City', type: FieldType.string, values: ['Stockholm'] },
|
||||||
|
{ name: 'Value2', type: FieldType.number, values: [20] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = rowsToFields(
|
||||||
|
{
|
||||||
|
mappings: [
|
||||||
|
{ fieldName: 'Name', handlerKey: '__ignore' },
|
||||||
|
{ fieldName: 'Value', handlerKey: '__ignore' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
input
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.fields[0].name).toEqual('Stockholm');
|
||||||
|
expect(result.fields[0].values.get(0)).toEqual(20);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,95 @@
|
|||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import {
|
||||||
|
ArrayVector,
|
||||||
|
DataFrame,
|
||||||
|
DataTransformerID,
|
||||||
|
DataTransformerInfo,
|
||||||
|
Field,
|
||||||
|
getFieldDisplayName,
|
||||||
|
Labels,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import {
|
||||||
|
getFieldConfigFromFrame,
|
||||||
|
FieldToConfigMapping,
|
||||||
|
evaluteFieldMappings,
|
||||||
|
EvaluatedMappingResult,
|
||||||
|
FieldConfigHandlerKey,
|
||||||
|
} from '../fieldToConfigMapping/fieldToConfigMapping';
|
||||||
|
|
||||||
|
export interface RowToFieldsTransformOptions {
|
||||||
|
nameField?: string;
|
||||||
|
valueField?: string;
|
||||||
|
mappings?: FieldToConfigMapping[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rowsToFieldsTransformer: DataTransformerInfo<RowToFieldsTransformOptions> = {
|
||||||
|
id: DataTransformerID.rowsToFields,
|
||||||
|
name: 'Rows to fields',
|
||||||
|
description: 'Convert each row into a field with dynamic config',
|
||||||
|
defaultOptions: {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a modified copy of the series. If the transform is not or should not
|
||||||
|
* be applied, just return the input series
|
||||||
|
*/
|
||||||
|
operator: (options) => (source) =>
|
||||||
|
source.pipe(
|
||||||
|
map((data) => {
|
||||||
|
return data.map((frame) => rowsToFields(options, frame));
|
||||||
|
})
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export function rowsToFields(options: RowToFieldsTransformOptions, data: DataFrame): DataFrame {
|
||||||
|
const mappingResult = evaluteFieldMappings(data, options.mappings ?? [], true);
|
||||||
|
const { nameField, valueField } = mappingResult;
|
||||||
|
|
||||||
|
if (!nameField || !valueField) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const outFields: Field[] = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < nameField.values.length; index++) {
|
||||||
|
const name = nameField.values.get(index);
|
||||||
|
const value = valueField.values.get(index);
|
||||||
|
const config = getFieldConfigFromFrame(data, index, mappingResult);
|
||||||
|
const labels = getLabelsFromRow(data, index, mappingResult);
|
||||||
|
|
||||||
|
const field: Field = {
|
||||||
|
name: name,
|
||||||
|
type: valueField.type,
|
||||||
|
values: new ArrayVector([value]),
|
||||||
|
config: config,
|
||||||
|
labels,
|
||||||
|
};
|
||||||
|
|
||||||
|
outFields.push(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fields: outFields,
|
||||||
|
length: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLabelsFromRow(frame: DataFrame, index: number, mappingResult: EvaluatedMappingResult): Labels {
|
||||||
|
const labels = { ...mappingResult.nameField!.labels };
|
||||||
|
|
||||||
|
for (let i = 0; i < frame.fields.length; i++) {
|
||||||
|
const field = frame.fields[i];
|
||||||
|
const fieldName = getFieldDisplayName(field, frame);
|
||||||
|
const fieldMapping = mappingResult.index[fieldName];
|
||||||
|
|
||||||
|
if (fieldMapping.handler && fieldMapping.handler.key !== FieldConfigHandlerKey.Label) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = field.values.get(index);
|
||||||
|
if (value != null) {
|
||||||
|
labels[fieldName] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
}
|
24
public/app/core/components/TransformersUI/utils.ts
Normal file
24
public/app/core/components/TransformersUI/utils.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { DataFrame, getFieldDisplayName } from '@grafana/data';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
export function useAllFieldNamesFromDataFrames(input: DataFrame[]): string[] {
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!Array.isArray(input)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(
|
||||||
|
input.reduce((names, frame) => {
|
||||||
|
if (!frame || !Array.isArray(frame.fields)) {
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
return frame.fields.reduce((names, field) => {
|
||||||
|
const t = getFieldDisplayName(field, frame, input);
|
||||||
|
names[t] = true;
|
||||||
|
return names;
|
||||||
|
}, names);
|
||||||
|
}, {} as Record<string, boolean>)
|
||||||
|
);
|
||||||
|
}, [input]);
|
||||||
|
}
|
@ -14,6 +14,8 @@ import { seriesToRowsTransformerRegistryItem } from '../components/TransformersU
|
|||||||
import { concatenateTransformRegistryItem } from '../components/TransformersUI/ConcatenateTransformerEditor';
|
import { concatenateTransformRegistryItem } from '../components/TransformersUI/ConcatenateTransformerEditor';
|
||||||
import { renameByRegexTransformRegistryItem } from '../components/TransformersUI/RenameByRegexTransformer';
|
import { renameByRegexTransformRegistryItem } from '../components/TransformersUI/RenameByRegexTransformer';
|
||||||
import { histogramTransformRegistryItem } from '../components/TransformersUI/HistogramTransformerEditor';
|
import { histogramTransformRegistryItem } from '../components/TransformersUI/HistogramTransformerEditor';
|
||||||
|
import { rowsToFieldsTransformRegistryItem } from '../components/TransformersUI/rowsToFields/RowsToFieldsTransformerEditor';
|
||||||
|
import { configFromQueryTransformRegistryItem } from '../components/TransformersUI/configFromQuery/ConfigFromQueryTransformerEditor';
|
||||||
|
|
||||||
export const getStandardTransformers = (): Array<TransformerRegistryItem<any>> => {
|
export const getStandardTransformers = (): Array<TransformerRegistryItem<any>> => {
|
||||||
return [
|
return [
|
||||||
@ -32,5 +34,7 @@ export const getStandardTransformers = (): Array<TransformerRegistryItem<any>> =
|
|||||||
sortByTransformRegistryItem,
|
sortByTransformRegistryItem,
|
||||||
mergeTransformerRegistryItem,
|
mergeTransformerRegistryItem,
|
||||||
histogramTransformRegistryItem,
|
histogramTransformRegistryItem,
|
||||||
|
rowsToFieldsTransformRegistryItem,
|
||||||
|
configFromQueryTransformRegistryItem,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
@ -187,6 +187,7 @@ export function getFieldOverrideCategories(props: OptionPaneRenderProps): Option
|
|||||||
render: function renderAddPropertyButton() {
|
render: function renderAddPropertyButton() {
|
||||||
return (
|
return (
|
||||||
<ValuePicker
|
<ValuePicker
|
||||||
|
key="Add override property"
|
||||||
label="Add override property"
|
label="Add override property"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
isFullWidth={true}
|
isFullWidth={true}
|
||||||
|
@ -13,15 +13,7 @@ export const PluginStateInfo: FC<Props> = (props) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <Badge color={display.color} title={display.tooltip} text={display.text} icon={display.icon} />;
|
||||||
<Badge
|
|
||||||
color={display.color}
|
|
||||||
title={display.tooltip}
|
|
||||||
text={display.text}
|
|
||||||
icon={display.icon}
|
|
||||||
tooltip={display.tooltip}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function getFeatureStateInfo(state?: PluginState): BadgeProps | null {
|
function getFeatureStateInfo(state?: PluginState): BadgeProps | null {
|
||||||
|
@ -287,7 +287,7 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
|||||||
return _dir === 1 ? splits : splits.reverse();
|
return _dir === 1 ? splits : splits.reverse();
|
||||||
};
|
};
|
||||||
|
|
||||||
const xValues: Axis.Values = (u) => u.data[0];
|
const xValues: Axis.Values = (u) => u.data[0].map((x) => formatValue(0, x));
|
||||||
|
|
||||||
let hovered: Rect | undefined = undefined;
|
let hovered: Rect | undefined = undefined;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user