diff --git a/devenv/dev-dashboards/home.json b/devenv/dev-dashboards/home.json index f4cdfed9d53..a65bd3eeecd 100644 --- a/devenv/dev-dashboards/home.json +++ b/devenv/dev-dashboards/home.json @@ -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,11 +222,32 @@ "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", "uid": "j6T00KRZz", "version": 2 -} +} \ No newline at end of file diff --git a/devenv/dev-dashboards/transforms/config-from-query.json b/devenv/dev-dashboards/transforms/config-from-query.json new file mode 100644 index 00000000000..ce834663b66 --- /dev/null +++ b/devenv/dev-dashboards/transforms/config-from-query.json @@ -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 + } \ No newline at end of file diff --git a/devenv/dev-dashboards/transforms/rows-to-fields.json b/devenv/dev-dashboards/transforms/rows-to-fields.json new file mode 100644 index 00000000000..4e27d9b94ec --- /dev/null +++ b/devenv/dev-dashboards/transforms/rows-to-fields.json @@ -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 + } \ No newline at end of file diff --git a/packages/grafana-data/src/transformations/fieldReducer.ts b/packages/grafana-data/src/transformations/fieldReducer.ts index a07c0bd1769..8b2fea28f97 100644 --- a/packages/grafana-data/src/transformations/fieldReducer.ts +++ b/packages/grafana-data/src/transformations/fieldReducer.ts @@ -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(() => [ 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 { diff --git a/packages/grafana-data/src/transformations/transformers/ids.ts b/packages/grafana-data/src/transformations/transformers/ids.ts index f62da205690..9018302f605 100644 --- a/packages/grafana-data/src/transformations/transformers/ids.ts +++ b/packages/grafana-data/src/transformations/transformers/ids.ts @@ -23,4 +23,6 @@ export enum DataTransformerID { groupBy = 'groupBy', sortBy = 'sortBy', histogram = 'histogram', + configFromData = 'configFromData', + rowsToFields = 'rowsToFields', } diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index 1087c91e6e0..82b5133d6fd 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -110,9 +110,11 @@ export class Gauge extends PureComponent { 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; diff --git a/pkg/tsdb/testdatasource/csv_data.go b/pkg/tsdb/testdatasource/csv_data.go index 9b4841162a0..86756594d0f 100644 --- a/pkg/tsdb/testdatasource/csv_data.go +++ b/pkg/tsdb/testdatasource/csv_data.go @@ -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 { diff --git a/public/app/core/components/TransformersUI/GroupByTransformerEditor.tsx b/public/app/core/components/TransformersUI/GroupByTransformerEditor.tsx index 5706844c195..5a1270cef2f 100644 --- a/public/app/core/components/TransformersUI/GroupByTransformerEditor.tsx +++ b/public/app/core/components/TransformersUI/GroupByTransformerEditor.tsx @@ -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 { - 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 - {fieldNames.map((key: string) => ( + {fieldNames.map((key) => ( {} @@ -21,7 +20,7 @@ const OrganizeFieldsTransformerEditor: React.FC 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 { - 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) - ); -}; - export const organizeFieldsTransformRegistryItem: TransformerRegistryItem = { id: DataTransformerID.organize, editor: OrganizeFieldsTransformerEditor, diff --git a/public/app/core/components/TransformersUI/SeriesToFieldsTransformerEditor.tsx b/public/app/core/components/TransformersUI/SeriesToFieldsTransformerEditor.tsx index f4e58da17fa..ad3cf65ff2a 100644 --- a/public/app/core/components/TransformersUI/SeriesToFieldsTransformerEditor.tsx +++ b/public/app/core/components/TransformersUI/SeriesToFieldsTransformerEditor.tsx @@ -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> = ({ 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) => { @@ -33,7 +32,7 @@ export const SeriesToFieldsTransformerEditor: React.FC
Field name
-
); diff --git a/public/app/core/components/TransformersUI/SortByTransformerEditor.tsx b/public/app/core/components/TransformersUI/SortByTransformerEditor.tsx index fbcac6d7980..4ac53bcd525 100644 --- a/public/app/core/components/TransformersUI/SortByTransformerEditor.tsx +++ b/public/app/core/components/TransformersUI/SortByTransformerEditor.tsx @@ -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> = ({ 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 + + + + + onChangeConfigProperty(row, value)} + /> + + {withReducers && ( + + onChangeReducer(row, stats[0] as ReducerID)} + /> + + )} + + ))} + + + ); +} + +interface FieldToConfigRowViewModel { + handlerKey: string | null; + fieldName: string; + configOption: SelectableValue | 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 | 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; + `, +}); diff --git a/public/app/core/components/TransformersUI/fieldToConfigMapping/fieldToConfigMapping.ts b/public/app/core/components/TransformersUI/fieldToConfigMapping/fieldToConfigMapping.ts new file mode 100644 index 00000000000..8795d73c5e2 --- /dev/null +++ b/public/app/core/components/TransformersUI/fieldToConfigMapping/fieldToConfigMapping.ts @@ -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 | 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; + 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; +} diff --git a/public/app/core/components/TransformersUI/rowsToFields/RowsToFieldsTransformerEditor.test.tsx b/public/app/core/components/TransformersUI/rowsToFields/RowsToFieldsTransformerEditor.test.tsx new file mode 100644 index 00000000000..22f5038eb5b --- /dev/null +++ b/public/app/core/components/TransformersUI/rowsToFields/RowsToFieldsTransformerEditor.test.tsx @@ -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) => { + const editorProps = { ...props, ...testProps }; + return render(); +}; + +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' }], + }) + ); + }); +}); diff --git a/public/app/core/components/TransformersUI/rowsToFields/RowsToFieldsTransformerEditor.tsx b/public/app/core/components/TransformersUI/rowsToFields/RowsToFieldsTransformerEditor.tsx new file mode 100644 index 00000000000..4f66fb2ef95 --- /dev/null +++ b/public/app/core/components/TransformersUI/rowsToFields/RowsToFieldsTransformerEditor.tsx @@ -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 {} + +export function RowsToFieldsTransformerEditor({ input, options, onChange }: Props) { + if (input.length === 0) { + return null; + } + + return ( +
+ onChange({ ...options, mappings })} + withNameAndValue={true} + /> +
+ ); +} + +export const rowsToFieldsTransformRegistryItem: TransformerRegistryItem = { + 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 + +`, +}; diff --git a/public/app/core/components/TransformersUI/rowsToFields/rowsToFields.test.ts b/public/app/core/components/TransformersUI/rowsToFields/rowsToFields.test.ts new file mode 100644 index 00000000000..0873a4b3586 --- /dev/null +++ b/public/app/core/components/TransformersUI/rowsToFields/rowsToFields.test.ts @@ -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); + }); +}); diff --git a/public/app/core/components/TransformersUI/rowsToFields/rowsToFields.ts b/public/app/core/components/TransformersUI/rowsToFields/rowsToFields.ts new file mode 100644 index 00000000000..fc5bbd21478 --- /dev/null +++ b/public/app/core/components/TransformersUI/rowsToFields/rowsToFields.ts @@ -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 = { + 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; +} diff --git a/public/app/core/components/TransformersUI/utils.ts b/public/app/core/components/TransformersUI/utils.ts new file mode 100644 index 00000000000..4bb949169a0 --- /dev/null +++ b/public/app/core/components/TransformersUI/utils.ts @@ -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) + ); + }, [input]); +} diff --git a/public/app/core/utils/standardTransformers.ts b/public/app/core/utils/standardTransformers.ts index 584ed687472..5d436996f8b 100644 --- a/public/app/core/utils/standardTransformers.ts +++ b/public/app/core/utils/standardTransformers.ts @@ -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> => { return [ @@ -32,5 +34,7 @@ export const getStandardTransformers = (): Array> = sortByTransformRegistryItem, mergeTransformerRegistryItem, histogramTransformRegistryItem, + rowsToFieldsTransformRegistryItem, + configFromQueryTransformRegistryItem, ]; }; diff --git a/public/app/features/dashboard/components/PanelEditor/getFieldOverrideElements.tsx b/public/app/features/dashboard/components/PanelEditor/getFieldOverrideElements.tsx index 2c6e6a969ac..3b40139eb16 100644 --- a/public/app/features/dashboard/components/PanelEditor/getFieldOverrideElements.tsx +++ b/public/app/features/dashboard/components/PanelEditor/getFieldOverrideElements.tsx @@ -187,6 +187,7 @@ export function getFieldOverrideCategories(props: OptionPaneRenderProps): Option render: function renderAddPropertyButton() { return ( = (props) => { return null; } - return ( - - ); + return ; }; function getFeatureStateInfo(state?: PluginState): BadgeProps | null { diff --git a/public/app/plugins/panel/barchart/bars.ts b/public/app/plugins/panel/barchart/bars.ts index 6faa4a4e748..d74a6d50d91 100644 --- a/public/app/plugins/panel/barchart/bars.ts +++ b/public/app/plugins/panel/barchart/bars.ts @@ -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;