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,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"target": {
|
||||
"limit": 100,
|
||||
"matchAny": false,
|
||||
"tags": [],
|
||||
"type": "dashboard"
|
||||
},
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
@ -18,21 +24,26 @@
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"folderId": null,
|
||||
"datasource": null,
|
||||
"gridPos": {
|
||||
"h": 26,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"headings": true,
|
||||
"id": 7,
|
||||
"limit": 100,
|
||||
"links": [],
|
||||
"query": "",
|
||||
"recent": true,
|
||||
"search": false,
|
||||
"starred": true,
|
||||
"options": {
|
||||
"folderId": null,
|
||||
"maxItems": 100,
|
||||
"query": "",
|
||||
"showHeadings": true,
|
||||
"showRecentlyViewed": true,
|
||||
"showSearch": false,
|
||||
"showStarred": true,
|
||||
"tags": []
|
||||
},
|
||||
"pluginVersion": "8.1.0-pre",
|
||||
"tags": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
@ -40,95 +51,167 @@
|
||||
"type": "dashlist"
|
||||
},
|
||||
{
|
||||
"folderId": null,
|
||||
"datasource": null,
|
||||
"gridPos": {
|
||||
"h": 13,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 0
|
||||
},
|
||||
"headings": false,
|
||||
"id": 2,
|
||||
"limit": 1000,
|
||||
"links": [],
|
||||
"query": "",
|
||||
"recent": false,
|
||||
"search": true,
|
||||
"starred": false,
|
||||
"tags": ["panel-tests"],
|
||||
"options": {
|
||||
"maxItems": 1000,
|
||||
"query": "",
|
||||
"showHeadings": false,
|
||||
"showRecentlyViewed": false,
|
||||
"showSearch": true,
|
||||
"showStarred": false,
|
||||
"tags": [
|
||||
"panel-tests"
|
||||
]
|
||||
},
|
||||
"pluginVersion": "8.1.0-pre",
|
||||
"tags": [
|
||||
"panel-tests"
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "tag: panel-tests",
|
||||
"type": "dashlist"
|
||||
},
|
||||
{
|
||||
"folderId": null,
|
||||
"datasource": null,
|
||||
"gridPos": {
|
||||
"h": 26,
|
||||
"h": 13,
|
||||
"w": 6,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"headings": false,
|
||||
"id": 3,
|
||||
"limit": 1000,
|
||||
"links": [],
|
||||
"query": "",
|
||||
"recent": false,
|
||||
"search": true,
|
||||
"starred": false,
|
||||
"tags": ["gdev", "demo"],
|
||||
"options": {
|
||||
"folderId": null,
|
||||
"maxItems": 1000,
|
||||
"query": "",
|
||||
"showHeadings": false,
|
||||
"showRecentlyViewed": false,
|
||||
"showSearch": true,
|
||||
"showStarred": false,
|
||||
"tags": [
|
||||
"gdev",
|
||||
"demo"
|
||||
]
|
||||
},
|
||||
"pluginVersion": "8.1.0-pre",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"demo"
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "tag: dashboard-demo",
|
||||
"type": "dashlist"
|
||||
},
|
||||
{
|
||||
"folderId": null,
|
||||
"datasource": null,
|
||||
"gridPos": {
|
||||
"h": 26,
|
||||
"w": 6,
|
||||
"x": 18,
|
||||
"y": 0
|
||||
},
|
||||
"headings": false,
|
||||
"id": 5,
|
||||
"limit": 1000,
|
||||
"links": [],
|
||||
"query": "",
|
||||
"recent": false,
|
||||
"search": true,
|
||||
"starred": false,
|
||||
"tags": ["gdev", "datasource-test"],
|
||||
"options": {
|
||||
"folderId": null,
|
||||
"maxItems": 1000,
|
||||
"query": "",
|
||||
"showHeadings": false,
|
||||
"showRecentlyViewed": false,
|
||||
"showSearch": true,
|
||||
"showStarred": false,
|
||||
"tags": [
|
||||
"gdev",
|
||||
"datasource-test"
|
||||
]
|
||||
},
|
||||
"pluginVersion": "8.1.0-pre",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"datasource-test"
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Data source tests",
|
||||
"type": "dashlist"
|
||||
},
|
||||
{
|
||||
"folderId": null,
|
||||
"datasource": null,
|
||||
"gridPos": {
|
||||
"h": 13,
|
||||
"w": 6,
|
||||
"x": 6,
|
||||
"y": 13
|
||||
},
|
||||
"headings": false,
|
||||
"id": 4,
|
||||
"limit": 1000,
|
||||
"links": [],
|
||||
"query": "",
|
||||
"recent": false,
|
||||
"search": true,
|
||||
"starred": false,
|
||||
"tags": ["templating", "gdev"],
|
||||
"options": {
|
||||
"maxItems": 1000,
|
||||
"query": "",
|
||||
"showHeadings": false,
|
||||
"showRecentlyViewed": false,
|
||||
"showSearch": true,
|
||||
"showStarred": false,
|
||||
"tags": [
|
||||
"templating",
|
||||
"gdev"
|
||||
]
|
||||
},
|
||||
"pluginVersion": "8.1.0-pre",
|
||||
"tags": [
|
||||
"templating",
|
||||
"gdev"
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "tag: templating ",
|
||||
"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",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
@ -139,8 +222,29 @@
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
|
||||
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "Grafana Dev Overview & Home",
|
||||
|
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',
|
||||
delta = 'delta',
|
||||
step = 'step',
|
||||
|
||||
firstNotNull = 'firstNotNull',
|
||||
lastNotNull = 'lastNotNull',
|
||||
|
||||
changeCount = 'changeCount',
|
||||
distinctCount = 'distinctCount',
|
||||
|
||||
allIsZero = 'allIsZero',
|
||||
allIsNull = 'allIsNull',
|
||||
allValues = 'allValues',
|
||||
}
|
||||
|
||||
// Internal function
|
||||
@ -232,6 +230,13 @@ export const fieldReducers = new Registry<FieldReducerInfo>(() => [
|
||||
description: 'Percentage difference between first and last values',
|
||||
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 {
|
||||
|
@ -23,4 +23,6 @@ export enum DataTransformerID {
|
||||
groupBy = 'groupBy',
|
||||
sortBy = 'sortBy',
|
||||
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;
|
||||
// remove gauge & marker width (on left and right side)
|
||||
// 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 thresholdLabelFontSize = fontSize / 2.5;
|
||||
const thresholdLabelFontSize = Math.max(fontSize / 2.5, 12);
|
||||
|
||||
let min = field.min ?? 0;
|
||||
let max = field.max ?? 100;
|
||||
|
@ -28,7 +28,7 @@ func (p *testDataPlugin) handleCsvContentScenario(ctx context.Context, req *back
|
||||
}
|
||||
|
||||
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)
|
||||
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 {
|
||||
DataTransformerID,
|
||||
@ -8,7 +8,6 @@ import {
|
||||
TransformerRegistryItem,
|
||||
TransformerUIProps,
|
||||
} from '@grafana/data';
|
||||
import { getAllFieldNamesFromDataFrames } from './OrganizeFieldsTransformerEditor';
|
||||
import { Select, StatsPicker, stylesFactory } from '@grafana/ui';
|
||||
|
||||
import {
|
||||
@ -16,6 +15,7 @@ import {
|
||||
GroupByOperationID,
|
||||
GroupByTransformerOptions,
|
||||
} from '@grafana/data/src/transformations/transformers/groupBy';
|
||||
import { useAllFieldNamesFromDataFrames } from './utils';
|
||||
|
||||
interface FieldProps {
|
||||
fieldName: string;
|
||||
@ -28,7 +28,7 @@ export const GroupByTransformerEditor: React.FC<TransformerUIProps<GroupByTransf
|
||||
options,
|
||||
onChange,
|
||||
}) => {
|
||||
const fieldNames = useMemo(() => getAllFieldNamesFromDataFrames(input), [input]);
|
||||
const fieldNames = useAllFieldNamesFromDataFrames(input);
|
||||
|
||||
const onConfigChange = useCallback(
|
||||
(fieldName: string) => (config: GroupByFieldOptions) => {
|
||||
@ -47,7 +47,7 @@ export const GroupByTransformerEditor: React.FC<TransformerUIProps<GroupByTransf
|
||||
|
||||
return (
|
||||
<div>
|
||||
{fieldNames.map((key: string) => (
|
||||
{fieldNames.map((key) => (
|
||||
<GroupByFieldConfiguration
|
||||
onConfigChange={onConfigChange(key)}
|
||||
fieldName={key}
|
||||
|
@ -2,18 +2,17 @@ import React, { useCallback, useMemo } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd';
|
||||
import {
|
||||
DataFrame,
|
||||
DataTransformerID,
|
||||
GrafanaTheme,
|
||||
standardTransformers,
|
||||
TransformerRegistryItem,
|
||||
TransformerUIProps,
|
||||
getFieldDisplayName,
|
||||
} from '@grafana/data';
|
||||
import { stylesFactory, useTheme, Input, IconButton, Icon, FieldValidationMessage } from '@grafana/ui';
|
||||
|
||||
import { OrganizeFieldsTransformerOptions } from '@grafana/data/src/transformations/transformers/organize';
|
||||
import { createOrderFieldsComparer } from '@grafana/data/src/transformations/transformers/order';
|
||||
import { useAllFieldNamesFromDataFrames } from './utils';
|
||||
|
||||
interface OrganizeFieldsTransformerEditorProps extends TransformerUIProps<OrganizeFieldsTransformerOptions> {}
|
||||
|
||||
@ -21,7 +20,7 @@ const OrganizeFieldsTransformerEditor: React.FC<OrganizeFieldsTransformerEditorP
|
||||
const { options, input, onChange } = props;
|
||||
const { indexByName, excludeByName, renameByName } = options;
|
||||
|
||||
const fieldNames = useMemo(() => getAllFieldNamesFromDataFrames(input), [input]);
|
||||
const fieldNames = useAllFieldNamesFromDataFrames(input);
|
||||
const orderedFieldNames = useMemo(() => orderFieldNamesByIndex(fieldNames, indexByName), [fieldNames, indexByName]);
|
||||
|
||||
const onToggleVisibility = useCallback(
|
||||
@ -205,26 +204,6 @@ const orderFieldNamesByIndex = (fieldNames: string[], indexByName: Record<string
|
||||
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> = {
|
||||
id: DataTransformerID.organize,
|
||||
editor: OrganizeFieldsTransformerEditor,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import {
|
||||
DataTransformerID,
|
||||
SelectableValue,
|
||||
@ -6,18 +6,17 @@ import {
|
||||
TransformerRegistryItem,
|
||||
TransformerUIProps,
|
||||
} from '@grafana/data';
|
||||
import { getAllFieldNamesFromDataFrames } from './OrganizeFieldsTransformerEditor';
|
||||
import { Select } from '@grafana/ui';
|
||||
|
||||
import { SeriesToColumnsOptions } from '@grafana/data/src/transformations/transformers/seriesToColumns';
|
||||
import { useAllFieldNamesFromDataFrames } from './utils';
|
||||
|
||||
export const SeriesToFieldsTransformerEditor: React.FC<TransformerUIProps<SeriesToColumnsOptions>> = ({
|
||||
input,
|
||||
options,
|
||||
onChange,
|
||||
}) => {
|
||||
const fieldNames = useMemo(() => getAllFieldNamesFromDataFrames(input), [input]);
|
||||
const fieldNameOptions = fieldNames.map((item: string) => ({ label: item, value: item }));
|
||||
const fieldNames = useAllFieldNamesFromDataFrames(input).map((item: string) => ({ label: item, value: item }));
|
||||
|
||||
const onSelectField = useCallback(
|
||||
(value: SelectableValue<string>) => {
|
||||
@ -33,7 +32,7 @@ export const SeriesToFieldsTransformerEditor: React.FC<TransformerUIProps<Series
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form gf-form--grow">
|
||||
<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>
|
||||
);
|
||||
|
@ -1,23 +1,15 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { DataTransformerID, standardTransformers, TransformerRegistryItem, TransformerUIProps } from '@grafana/data';
|
||||
import { getAllFieldNamesFromDataFrames } from './OrganizeFieldsTransformerEditor';
|
||||
import { InlineField, InlineSwitch, InlineFieldRow, Select } from '@grafana/ui';
|
||||
|
||||
import { SortByField, SortByTransformerOptions } from '@grafana/data/src/transformations/transformers/sortBy';
|
||||
import { useAllFieldNamesFromDataFrames } from './utils';
|
||||
|
||||
export const SortByTransformerEditor: React.FC<TransformerUIProps<SortByTransformerOptions>> = ({
|
||||
input,
|
||||
options,
|
||||
onChange,
|
||||
}) => {
|
||||
const fieldNames = useMemo(
|
||||
() =>
|
||||
getAllFieldNamesFromDataFrames(input).map((n) => ({
|
||||
value: n,
|
||||
label: n,
|
||||
})),
|
||||
[input]
|
||||
);
|
||||
const fieldNames = useAllFieldNamesFromDataFrames(input).map((item: string) => ({ label: item, value: item }));
|
||||
|
||||
// Only supports single sort for now
|
||||
const onSortChange = useCallback(
|
||||
@ -37,7 +29,7 @@ export const SortByTransformerEditor: React.FC<TransformerUIProps<SortByTransfor
|
||||
<InlineField label="Field" labelWidth={10} grow={true}>
|
||||
<Select
|
||||
options={fieldNames}
|
||||
value={fieldNames.find((v) => v.value === s.field)}
|
||||
value={s.field}
|
||||
placeholder="Select field"
|
||||
onChange={(v) => {
|
||||
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 { renameByRegexTransformRegistryItem } from '../components/TransformersUI/RenameByRegexTransformer';
|
||||
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>> => {
|
||||
return [
|
||||
@ -32,5 +34,7 @@ export const getStandardTransformers = (): Array<TransformerRegistryItem<any>> =
|
||||
sortByTransformRegistryItem,
|
||||
mergeTransformerRegistryItem,
|
||||
histogramTransformRegistryItem,
|
||||
rowsToFieldsTransformRegistryItem,
|
||||
configFromQueryTransformRegistryItem,
|
||||
];
|
||||
};
|
||||
|
@ -187,6 +187,7 @@ export function getFieldOverrideCategories(props: OptionPaneRenderProps): Option
|
||||
render: function renderAddPropertyButton() {
|
||||
return (
|
||||
<ValuePicker
|
||||
key="Add override property"
|
||||
label="Add override property"
|
||||
variant="secondary"
|
||||
isFullWidth={true}
|
||||
|
@ -13,15 +13,7 @@ export const PluginStateInfo: FC<Props> = (props) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge
|
||||
color={display.color}
|
||||
title={display.tooltip}
|
||||
text={display.text}
|
||||
icon={display.icon}
|
||||
tooltip={display.tooltip}
|
||||
/>
|
||||
);
|
||||
return <Badge color={display.color} title={display.tooltip} text={display.text} icon={display.icon} />;
|
||||
};
|
||||
|
||||
function getFeatureStateInfo(state?: PluginState): BadgeProps | null {
|
||||
|
@ -287,7 +287,7 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) {
|
||||
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;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user