diff --git a/devenv/dev-dashboards/panel-graph/graph-ng-thresholds.json b/devenv/dev-dashboards/panel-graph/graph-ng-thresholds.json new file mode 100644 index 00000000000..4d775a698ab --- /dev/null +++ b/devenv/dev-dashboards/panel-graph/graph-ng-thresholds.json @@ -0,0 +1,1248 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 0 + }, + "hiddenSeries": false, + "id": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.0-pre", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "max": 20, + "min": 0, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "thresholds": [ + { + "$$hashKey": "object:14", + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 80, + "yaxis": "left" + }, + { + "$$hashKey": "object:44", + "colorMode": "warning", + "fill": true, + "line": true, + "op": "gt", + "value": 50, + "yaxis": "left" + } + ], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Angular Line + Area ", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:26", + "format": "short", + "label": null, + "logBase": 1, + "max": "100", + "min": "0", + "show": true + }, + { + "$$hashKey": "object:27", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "line+area" + } + }, + "mappings": [], + "max": 100, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(115, 191, 105, 0)", + "value": null + }, + { + "color": "orange", + "value": 60 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "max": 20, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Timeseries - Line + Area", + "type": "timeseries" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 7 + }, + "hiddenSeries": false, + "id": 6, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.0-pre", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "max": 20, + "min": 0, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "thresholds": [ + { + "$$hashKey": "object:109", + "colorMode": "critical", + "fill": true, + "line": false, + "op": "gt", + "value": 80, + "yaxis": "left" + }, + { + "$$hashKey": "object:115", + "colorMode": "warning", + "fill": true, + "line": false, + "op": "gt", + "value": 60, + "yaxis": "left" + } + ], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Angular thresholds no lines", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:26", + "format": "short", + "label": null, + "logBase": 1, + "max": "100", + "min": "0", + "show": true + }, + { + "$$hashKey": "object:27", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "area" + } + }, + "mappings": [], + "max": 100, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(115, 191, 105, 0)", + "value": null + }, + { + "color": "orange", + "value": 60 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 12, + "y": 7 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "max": 20, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Timeseries - Area", + "type": "timeseries" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 13 + }, + "hiddenSeries": false, + "id": 13, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.0-pre", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "max": 20, + "min": 0, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "thresholds": [ + { + "$$hashKey": "object:109", + "colorMode": "critical", + "fill": true, + "line": true, + "op": "lt", + "value": 20, + "yaxis": "left" + }, + { + "$$hashKey": "object:115", + "colorMode": "warning", + "fill": true, + "line": true, + "op": "lt", + "value": 60, + "yaxis": "left" + } + ], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Angular thresholds less then ", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:26", + "format": "short", + "label": null, + "logBase": 1, + "max": "100", + "min": "0", + "show": true + }, + { + "$$hashKey": "object:27", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "line+area" + } + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "orange", + "value": 20 + }, + { + "color": "transparent", + "value": 60 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 12, + "y": 13 + }, + "id": 14, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "max": 20, + "min": 0, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Angular thresholds less then ", + "type": "timeseries" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 19 + }, + "hiddenSeries": false, + "id": 11, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.0-pre", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "max": 20, + "min": 0, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "thresholds": [ + { + "$$hashKey": "object:14", + "colorMode": "critical", + "fill": false, + "line": true, + "op": "gt", + "value": 80, + "yaxis": "left" + }, + { + "$$hashKey": "object:44", + "colorMode": "warning", + "fill": false, + "line": true, + "op": "gt", + "value": 50, + "yaxis": "left" + } + ], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Angular thresholds only lines", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:26", + "format": "short", + "label": null, + "logBase": 1, + "max": "100", + "min": "0", + "show": true + }, + { + "$$hashKey": "object:27", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "line" + } + }, + "mappings": [], + "max": 100, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(115, 191, 105, 0)", + "value": null + }, + { + "color": "orange", + "value": 60 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 12, + "y": 19 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "timeFrom": null, + "timeShift": null, + "title": "Timeseries - Line Thresholds", + "type": "timeseries" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fill": 0, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 25 + }, + "hiddenSeries": false, + "id": 7, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "maxDataPoints": 30, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.0-pre", + "pointradius": 1, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "max": 80, + "min": 40, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "thresholds": [ + { + "$$hashKey": "object:14", + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 80, + "yaxis": "left" + }, + { + "$$hashKey": "object:44", + "colorMode": "warning", + "fill": true, + "line": true, + "op": "lt", + "value": 40, + "yaxis": "left" + }, + { + "$$hashKey": "object:40", + "colorMode": "critical", + "fill": true, + "line": true, + "op": "lt", + "value": 20, + "yaxis": "left" + } + ], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Angular bands with gap ", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:26", + "format": "short", + "label": null, + "logBase": 1, + "max": "100", + "min": "0", + "show": true + }, + { + "$$hashKey": "object:27", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "line+area" + } + }, + "mappings": [], + "max": 100, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "orange", + "value": 20 + }, + { + "color": "transparent", + "value": 40 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 25 + }, + "id": 12, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "max": 20, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Timeseries - with gaps", + "type": "timeseries" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fill": 0, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 32 + }, + "hiddenSeries": false, + "id": 8, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "maxDataPoints": 30, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.5.0-pre", + "pointradius": 1, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "max": 80, + "min": 40, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "thresholds": [ + { + "$$hashKey": "object:14", + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(50, 161, 230, 0.13)", + "line": true, + "lineColor": "#B877D9", + "op": "gt", + "value": 80, + "yaxis": "left" + }, + { + "$$hashKey": "object:44", + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(184, 119, 217, 0.23)", + "line": true, + "lineColor": "rgba(93, 196, 31, 0.6)", + "op": "lt", + "value": 40, + "yaxis": "left" + } + ], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Angular custom colors", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:26", + "format": "short", + "label": null, + "logBase": 1, + "max": "100", + "min": "0", + "show": true + }, + { + "$$hashKey": "object:27", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 4, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "always", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "line+area" + } + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(184, 119, 217, 0.23)", + "value": null + }, + { + "color": "transparent", + "value": 40 + }, + { + "color": "rgba(50, 161, 230, 0.13)", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 32 + }, + "id": 15, + "maxDataPoints": 30, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "max": 80, + "min": 40, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Time series custom colors", + "type": "timeseries" + } + ], + "schemaVersion": 29, + "style": "dark", + "tags": [ + "gdev", + "panel-tests" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Panel Tests - GraphNG Thresholds", + "uid": "6HMMh4rMz", + "version": 2 +} \ No newline at end of file diff --git a/packages/grafana-data/src/utils/namedColorsPalette.ts b/packages/grafana-data/src/utils/namedColorsPalette.ts index 5354a386f89..8e48c5fa6ed 100644 --- a/packages/grafana-data/src/utils/namedColorsPalette.ts +++ b/packages/grafana-data/src/utils/namedColorsPalette.ts @@ -35,7 +35,9 @@ export type Color = | 'semi-dark-purple' | 'light-purple' | 'super-light-purple' - | 'panel-bg'; + | 'panel-bg' + | 'transparent' + | 'text'; type ThemeVariants = { dark: string; @@ -84,6 +86,8 @@ export function buildColorsMapForTheme(theme: GrafanaTheme): Record { it('renders ColorPickerTrigger component by default', () => { expect( - renderer.create( {}} />).root.findByType(ColorPickerTrigger) + renderer.create( {}} />).root.findByType(ColorSwatch) ).toBeTruthy(); }); diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx index 244d0a68b69..a523281f33b 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx @@ -2,12 +2,12 @@ import React, { Component, createRef } from 'react'; import { PopoverController } from '../Tooltip/PopoverController'; import { Popover } from '../Tooltip/Popover'; import { ColorPickerPopover, ColorPickerProps, ColorPickerChangeHandler } from './ColorPickerPopover'; -import { getColorForTheme, GrafanaTheme } from '@grafana/data'; +import { getColorForTheme, GrafanaTheme2 } from '@grafana/data'; import { SeriesColorPickerPopover } from './SeriesColorPickerPopover'; import { css } from '@emotion/css'; -import { withTheme, stylesFactory } from '../../themes'; -import { ColorPickerTrigger } from './ColorPickerTrigger'; +import { withTheme2, stylesFactory } from '../../themes'; +import { ColorSwatch } from './ColorSwatch'; /** * If you need custom trigger for the color picker you can do that with a render prop pattern and supply a function @@ -71,11 +71,11 @@ export const colorPickerFactory = ( hideColorPicker: hidePopper, }) ) : ( - )} @@ -87,15 +87,15 @@ export const colorPickerFactory = ( }; }; -export const ColorPicker = withTheme(colorPickerFactory(ColorPickerPopover, 'ColorPicker')); -export const SeriesColorPicker = withTheme(colorPickerFactory(SeriesColorPickerPopover, 'SeriesColorPicker')); +export const ColorPicker = withTheme2(colorPickerFactory(ColorPickerPopover, 'ColorPicker')); +export const SeriesColorPicker = withTheme2(colorPickerFactory(SeriesColorPickerPopover, 'SeriesColorPicker')); -const getStyles = stylesFactory((theme: GrafanaTheme) => { +const getStyles = stylesFactory((theme: GrafanaTheme2) => { return { colorPicker: css` position: absolute; z-index: ${theme.zIndex.tooltip}; - color: ${theme.colors.text}; + color: ${theme.colors.text.primary}; max-width: 400px; font-size: ${theme.typography.size.sm}; // !important because these styles are also provided to popper via .popper classes from Tooltip component diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.test.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.test.tsx index 589b3ba645d..eaf6969920c 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.test.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; import { ColorPickerPopover } from './ColorPickerPopover'; -import { ColorSwatch } from './NamedColorsGroup'; import { flatten } from 'lodash'; import { getNamedColorPalette, getColorFromHexRgbOrName } from '@grafana/data'; +import { ColorSwatch } from './ColorSwatch'; const allColors = flatten(Array.from(getNamedColorPalette().values())); @@ -15,7 +15,7 @@ describe('ColorPickerPopover', () => { const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere((node) => node.prop('isSelected') === false); expect(selectedSwatch.length).toBe(1); - expect(notSelectedSwatches.length).toBe(allColors.length - 1); + expect(notSelectedSwatches.length).toBe(allColors.length + 1); expect(selectedSwatch.prop('isSelected')).toBe(true); }); @@ -26,7 +26,7 @@ describe('ColorPickerPopover', () => { const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere((node) => node.prop('isSelected') === false); expect(selectedSwatch.length).toBe(1); - expect(notSelectedSwatches.length).toBe(allColors.length - 1); + expect(notSelectedSwatches.length).toBe(allColors.length + 1); expect(selectedSwatch.prop('isSelected')).toBe(true); }); }); diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx index 9577bcfd2e0..8103c1ec2ad 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx @@ -2,15 +2,15 @@ import React from 'react'; import { NamedColorsPalette } from './NamedColorsPalette'; import { PopoverContentProps } from '../Tooltip/Tooltip'; import SpectrumPalette from './SpectrumPalette'; -import { Themeable } from '../../types/theme'; +import { Themeable2 } from '../../types/theme'; import { warnAboutColorPickerPropsDeprecation } from './warnAboutColorPickerPropsDeprecation'; -import { css, cx } from '@emotion/css'; -import { GrafanaTheme, GrafanaThemeType, getColorForTheme } from '@grafana/data'; -import { stylesFactory, withTheme } from '../../themes'; +import { css } from '@emotion/css'; +import { GrafanaTheme2, getColorForTheme } from '@grafana/data'; +import { stylesFactory, withTheme2 } from '../../themes'; export type ColorPickerChangeHandler = (color: string) => void; -export interface ColorPickerProps extends Themeable { +export interface ColorPickerProps extends Themeable2 { color: string; onChange: ColorPickerChangeHandler; @@ -59,7 +59,7 @@ class UnThemedColorPickerPopover extends Reac if (enableNamedColors) { return changeHandler(color); } - changeHandler(getColorForTheme(color, theme)); + changeHandler(getColorForTheme(color, theme.v1)); }; onTabChange = (tab: PickerType | keyof T) => { @@ -68,13 +68,13 @@ class UnThemedColorPickerPopover extends Reac renderPicker = () => { const { activePicker } = this.state; - const { color, theme } = this.props; + const { color } = this.props; switch (activePicker) { case 'spectrum': return ; case 'palette': - return ; + return ; default: return this.renderCustomPicker(activePicker); } @@ -117,12 +117,7 @@ class UnThemedColorPickerPopover extends Reac const { theme } = this.props; const styles = getStyles(theme); return ( -
+
Colors @@ -132,62 +127,50 @@ class UnThemedColorPickerPopover extends Reac
{this.renderCustomPickerTabs()}
-
{this.renderPicker()}
); } } -export const ColorPickerPopover = withTheme(UnThemedColorPickerPopover); +export const ColorPickerPopover = withTheme2(UnThemedColorPickerPopover); ColorPickerPopover.displayName = 'ColorPickerPopover'; -const getStyles = stylesFactory((theme: GrafanaTheme) => { +const getStyles = stylesFactory((theme: GrafanaTheme2) => { return { colorPickerPopover: css` - border-radius: ${theme.border.radius.md}; - `, - colorPickerPopoverLight: css` - color: ${theme.palette.black}; - background: linear-gradient(180deg, ${theme.palette.white} 0%, #f7f8fa 104.25%); - box-shadow: 0px 2px 4px #dde4ed, 0px 0px 2px #dde4ed; + border-radius: ${theme.shape.borderRadius()}; + box-shadow: ${theme.shadows.z3}; + background: ${theme.colors.background.primary}; .ColorPickerPopover__tab { width: 50%; text-align: center; - padding: ${theme.spacing.sm} 0; - background: #dde4ed; - } - .ColorPickerPopover__tab--active { - background: ${theme.palette.white}; - } - `, - colorPickerPopoverDark: css` - color: #d8d9da; - background: linear-gradient(180deg, #1e2028 0%, #161719 104.25%); - box-shadow: 0px 2px 4px ${theme.palette.black}, 0px 0px 2px ${theme.palette.black}; - - .ColorPickerPopover__tab { - width: 50%; - text-align: center; - padding: ${theme.spacing.sm} 0; - background: #303133; - color: ${theme.palette.white}; + padding: ${theme.spacing(1, 0)}; + background: ${theme.colors.background.secondary}; + color: ${theme.colors.text.secondary}; cursor: pointer; } + .ColorPickerPopover__tab--active { - background: none; + color: ${theme.colors.text.primary}; + font-weight: ${theme.typography.fontWeightMedium}; + background: ${theme.colors.background.primary}; } `, colorPickerPopoverContent: css` width: 336px; + font-size: ${theme.typography.bodySmall.fontSize}; min-height: 184px; - padding: ${theme.spacing.lg}; + padding: ${theme.spacing(2)}; + display: flex; + align-items: center; + justify-content: center; `, colorPickerPopoverTabs: css` display: flex; width: 100%; - border-radius: ${theme.border.radius.md} ${theme.border.radius.md} 0 0; + border-radius: ${theme.shape.borderRadius()} ${theme.shape.borderRadius()} 0 0; overflow: hidden; `, }; diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerTrigger.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerTrigger.tsx deleted file mode 100644 index 684655276d6..00000000000 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPickerTrigger.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { forwardRef } from 'react'; - -interface ColorPickerTriggerProps { - onClick: () => void; - onMouseLeave: () => void; - color: string; -} - -export const ColorPickerTrigger = forwardRef(function ColorPickerTrigger( - props: ColorPickerTriggerProps, - ref: React.Ref -) { - return ( -
-
-
-
-
- ); -}); diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorSwatch.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorSwatch.tsx new file mode 100644 index 00000000000..c9b691f86a6 --- /dev/null +++ b/packages/grafana-ui/src/components/ColorPicker/ColorSwatch.tsx @@ -0,0 +1,60 @@ +import React, { CSSProperties } from 'react'; +import tinycolor from 'tinycolor2'; +import { useTheme2 } from '../../themes/ThemeContext'; + +/** @internal */ +export enum ColorSwatchVariant { + Small = 'small', + Large = 'large', +} + +/** @internal */ +export interface Props extends React.DOMAttributes { + color: string; + label?: string; + variant?: ColorSwatchVariant; + isSelected?: boolean; +} + +/** @internal */ +export const ColorSwatch = React.forwardRef( + ({ color, label, variant = ColorSwatchVariant.Small, isSelected, ...otherProps }, ref) => { + const theme = useTheme2(); + const tc = tinycolor(color); + const isSmall = variant === ColorSwatchVariant.Small; + const hasLabel = !!label; + const swatchSize = isSmall ? '16px' : '32px'; + + const swatchStyles: CSSProperties = { + width: swatchSize, + height: swatchSize, + borderRadius: '50%', + background: `${color}`, + marginRight: hasLabel ? '8px' : '0px', + boxShadow: isSelected + ? `inset 0 0 0 2px ${color}, inset 0 0 0 4px ${theme.colors.getContrastText(color)}` + : 'none', + }; + + if (tc.getAlpha() < 0.1) { + swatchStyles.border = `2px solid ${theme.colors.border.medium}`; + } + + return ( +
+
+ {hasLabel && {label}} +
+ ); + } +); + +ColorSwatch.displayName = 'ColorSwatch'; diff --git a/packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx b/packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx index cd78c9122c5..aeeffd8023d 100644 --- a/packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx @@ -1,59 +1,13 @@ import React, { FunctionComponent } from 'react'; -import { Themeable } from '../../types'; import { ColorDefinition } from '@grafana/data'; import { Color } from 'csstype'; import { upperFirst, find } from 'lodash'; +import { ColorSwatch, ColorSwatchVariant } from './ColorSwatch'; +import { useTheme2 } from '../../themes/ThemeContext'; type ColorChangeHandler = (color: ColorDefinition) => void; -export enum ColorSwatchVariant { - Small = 'small', - Large = 'large', -} - -interface ColorSwatchProps extends Themeable, React.DOMAttributes { - color: string; - label?: string; - variant?: ColorSwatchVariant; - isSelected?: boolean; -} - -export const ColorSwatch: FunctionComponent = ({ - color, - label, - variant = ColorSwatchVariant.Small, - isSelected, - theme, - ...otherProps -}) => { - const isSmall = variant === ColorSwatchVariant.Small; - const swatchSize = isSmall ? '16px' : '32px'; - - const swatchStyles = { - width: swatchSize, - height: swatchSize, - borderRadius: '50%', - background: `${color}`, - marginRight: isSmall ? '0px' : '8px', - boxShadow: isSelected ? `inset 0 0 0 2px ${color}, inset 0 0 0 4px ${theme.colors.bg1}` : 'none', - }; - - return ( -
-
- {variant === ColorSwatchVariant.Large && {label}} -
- ); -}; - -interface NamedColorsGroupProps extends Themeable { +interface NamedColorsGroupProps { colors: ColorDefinition[]; selectedColor?: Color; onColorSelect: ColorChangeHandler; @@ -64,9 +18,9 @@ const NamedColorsGroup: FunctionComponent = ({ colors, selectedColor, onColorSelect, - theme, ...otherProps }) => { + const theme = useTheme2(); const primaryColor = find(colors, (color) => !!color.isPrimary); return ( @@ -76,10 +30,9 @@ const NamedColorsGroup: FunctionComponent = ({ key={primaryColor.name} isSelected={primaryColor.name === selectedColor} variant={ColorSwatchVariant.Large} - color={primaryColor.variants[theme.type]} + color={primaryColor.variants[theme.colors.mode]} label={upperFirst(primaryColor.hue)} onClick={() => onColorSelect(primaryColor)} - theme={theme} /> )}
= ({ onColorSelect(color)} - theme={theme} />
) diff --git a/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx index d457c8afa59..75347a5d15f 100644 --- a/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx @@ -1,9 +1,7 @@ -import React from 'react'; +import React, { useState } from 'react'; import { NamedColorsPalette, NamedColorsPaletteProps } from './NamedColorsPalette'; import { Meta, Story } from '@storybook/react'; import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; -import { renderComponentWithTheme } from '../../utils/storybook/withTheme'; -import { UseState } from '../../utils/storybook/UseState'; import mdx from './ColorPicker.mdx'; export default { @@ -31,17 +29,10 @@ interface StoryProps extends Partial { } export const NamedColors: Story = ({ selectedColor }) => { - return ( - - {(selectedColor, updateSelectedColor) => { - return renderComponentWithTheme(NamedColorsPalette, { - color: selectedColor, - onChange: updateSelectedColor, - }); - }} - - ); + const [color, setColor] = useState('green'); + return ; }; + NamedColors.args = { - selectedColor: 'red', + color: 'green', }; diff --git a/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.test.tsx b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.test.tsx index 03524a77d71..00ee361469d 100644 --- a/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.test.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; import { NamedColorsPalette } from './NamedColorsPalette'; -import { ColorSwatch } from './NamedColorsGroup'; -import { getColorDefinitionByName, GrafanaThemeType } from '@grafana/data'; -import { getTheme } from '../../themes'; +import { createTheme, getColorDefinitionByName } from '@grafana/data'; +import { ColorSwatch } from './ColorSwatch'; +import { ThemeContext } from '../../themes'; describe('NamedColorsPalette', () => { const BasicGreen = getColorDefinitionByName('green'); @@ -16,22 +16,21 @@ describe('NamedColorsPalette', () => { }); it('should render provided color variant specific for theme', () => { - wrapper = mount( {}} />); + wrapper = mount( {}} />); selectedSwatch = wrapper.find(ColorSwatch).findWhere((node) => node.key() === BasicGreen.name); expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark); wrapper.unmount(); - wrapper = mount( - {}} /> + + const withLightTheme = ( + + {}} /> + ); + + wrapper = mount(withLightTheme); selectedSwatch = wrapper.find(ColorSwatch).findWhere((node) => node.key() === BasicGreen.name); expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.light); }); - - it('should render dar variant of provided color when theme not provided', () => { - wrapper = mount( {}} theme={getTheme()} />); - selectedSwatch = wrapper.find(ColorSwatch).findWhere((node) => node.key() === BasicGreen.name); - expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark); - }); }); }); diff --git a/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.tsx b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.tsx index 193456080f1..a57cc3b0bfe 100644 --- a/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.tsx @@ -1,20 +1,23 @@ import React from 'react'; import { getNamedColorPalette } from '@grafana/data'; -import { Themeable } from '../../types/index'; import NamedColorsGroup from './NamedColorsGroup'; +import { VerticalGroup } from '../Layout/Layout'; +import { ColorSwatch } from './ColorSwatch'; +import { useTheme2 } from '../../themes/ThemeContext'; -export interface NamedColorsPaletteProps extends Themeable { +export interface NamedColorsPaletteProps { color?: string; onChange: (colorName: string) => void; } -export const NamedColorsPalette = ({ color, onChange, theme }: NamedColorsPaletteProps) => { +export const NamedColorsPalette = ({ color, onChange }: NamedColorsPaletteProps) => { + const theme = useTheme2(); + const swatches: JSX.Element[] = []; getNamedColorPalette().forEach((colors, hue) => { swatches.push( { @@ -25,15 +28,30 @@ export const NamedColorsPalette = ({ color, onChange, theme }: NamedColorsPalett }); return ( -
- {swatches} -
+ +
+ {swatches} + onChange('transparent')} + /> + onChange('text')} + /> +
+
); }; diff --git a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx index 227ce6382f5..37c806c6550 100644 --- a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx @@ -4,7 +4,7 @@ import { ColorPickerPopover, ColorPickerProps } from './ColorPickerPopover'; import { PopoverContentProps } from '../Tooltip/Tooltip'; import { Switch } from '../Forms/Legacy/Switch/Switch'; import { css } from '@emotion/css'; -import { withTheme, useStyles } from '../../themes'; +import { withTheme2, useStyles } from '../../themes'; import { Button } from '../Button'; export interface SeriesColorPickerPopoverProps extends ColorPickerProps, PopoverContentProps { @@ -89,7 +89,7 @@ export class AxisSelector extends React.PureComponent { return { diff --git a/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx b/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx index 09bb33e218b..1c305c38065 100644 --- a/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx @@ -5,7 +5,7 @@ import tinycolor from 'tinycolor2'; import ColorInput from './ColorInput'; import { GrafanaTheme, getColorForTheme } from '@grafana/data'; import { css, cx } from '@emotion/css'; -import { useStyles, useTheme } from '../../themes'; +import { useStyles, useTheme2 } from '../../themes'; import { useThrottleFn } from 'react-use'; export interface SpectrumPaletteProps { @@ -17,25 +17,27 @@ const SpectrumPalette: React.FunctionComponent = ({ color, const [currentColor, setColor] = useState(color); useThrottleFn(onChange, 500, [currentColor]); - const theme = useTheme(); + const theme = useTheme2(); const styles = useStyles(getStyles); const rgbaString = useMemo(() => { return currentColor.startsWith('rgba') ? currentColor - : tinycolor(getColorForTheme(currentColor, theme)).toRgbString(); + : tinycolor(getColorForTheme(currentColor, theme.v1)).toRgbString(); }, [currentColor, theme]); return ( - <> +
- - +
); }; const getStyles = (theme: GrafanaTheme) => ({ + wrapper: css` + flex-grow: 1; + `, root: css` &.react-colorful { width: auto; diff --git a/packages/grafana-ui/src/components/GraphNG/utils.ts b/packages/grafana-ui/src/components/GraphNG/utils.ts index e8e365c5fb4..3f189f19de7 100644 --- a/packages/grafana-ui/src/components/GraphNG/utils.ts +++ b/packages/grafana-ui/src/components/GraphNG/utils.ts @@ -22,6 +22,7 @@ import { AxisPlacement, DrawStyle, GraphFieldConfig, + GraphTresholdsStyleMode, PointVisibility, ScaleDirection, ScaleOrientation, @@ -224,6 +225,19 @@ export function preparePlotConfigBuilder( hideInLegend: customConfig.hideFrom?.legend, }); + // Render thresholds in graph + if (customConfig.thresholdsStyle && config.thresholds) { + const thresholdDisplay = customConfig.thresholdsStyle.mode ?? GraphTresholdsStyleMode.Off; + if (thresholdDisplay !== GraphTresholdsStyleMode.Off) { + builder.addThresholds({ + config: customConfig.thresholdsStyle, + thresholds: config.thresholds, + scaleKey, + theme, + }); + } + } + collectStackingGroups(field, stackingGroups, seriesIndex); } diff --git a/packages/grafana-ui/src/components/Input/Input.tsx b/packages/grafana-ui/src/components/Input/Input.tsx index a0b9bbfc5bd..0a906fb0202 100644 --- a/packages/grafana-ui/src/components/Input/Input.tsx +++ b/packages/grafana-ui/src/components/Input/Input.tsx @@ -193,7 +193,7 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false, width }: css` label: input-suffix; padding-left: ${theme.spacing(1)}; - padding-right: ${theme.spacing(0.5)}; + padding-right: ${theme.spacing(1)}; margin-bottom: -2px; border-left: none; border-top-left-radius: 0; diff --git a/packages/grafana-ui/src/components/OptionsUI/color.tsx b/packages/grafana-ui/src/components/OptionsUI/color.tsx index 4c77b791b10..db255c3d1b8 100644 --- a/packages/grafana-ui/src/components/OptionsUI/color.tsx +++ b/packages/grafana-ui/src/components/OptionsUI/color.tsx @@ -3,7 +3,7 @@ import { getColorForTheme, GrafanaTheme } from '@grafana/data'; import { ColorPicker } from '../ColorPicker/ColorPicker'; import { stylesFactory, useTheme } from '../../themes'; import { css } from '@emotion/css'; -import { ColorPickerTrigger } from '../ColorPicker/ColorPickerTrigger'; +import { ColorSwatch } from '../ColorPicker/ColorSwatch'; /** * @alpha @@ -26,7 +26,7 @@ export const ColorValueEditor: React.FC = ({ value, onCha return (
- {
- - - +
); diff --git a/packages/grafana-ui/src/components/uPlot/config.ts b/packages/grafana-ui/src/components/uPlot/config.ts index bc3429d8140..0bd73176a2a 100644 --- a/packages/grafana-ui/src/components/uPlot/config.ts +++ b/packages/grafana-ui/src/components/uPlot/config.ts @@ -192,6 +192,24 @@ export interface StackingConfig { group?: string; } +/** + * @alpha + */ +export enum GraphTresholdsStyleMode { + Off = 'off', + Line = 'line', + Area = 'area', + LineAndArea = 'line+area', + Series = 'series', +} + +/** + * @alpha + */ +export interface GraphThresholdsStyleConfig { + mode: GraphTresholdsStyleMode; +} + /** * @alpha */ @@ -205,6 +223,7 @@ export interface GraphFieldConfig drawStyle?: DrawStyle; gradientMode?: GraphGradientMode; stacking?: StackingConfig; + thresholdsStyle?: GraphThresholdsStyleConfig; } /** @@ -254,4 +273,11 @@ export const graphFieldOptions = { { label: 'Off', value: StackingMode.None }, { label: 'Normal', value: StackingMode.Normal }, ] as Array>, + + thresholdsDisplayModes: [ + { label: 'Off', value: GraphTresholdsStyleMode.Off }, + { label: 'As lines', value: GraphTresholdsStyleMode.Line }, + { label: 'As filled regions', value: GraphTresholdsStyleMode.Area }, + { label: 'As filled regions and lines', value: GraphTresholdsStyleMode.LineAndArea }, + ] as Array>, }; diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts index 2c7251c0036..72f7d89540e 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts @@ -9,8 +9,9 @@ import { ScaleDistribution, ScaleOrientation, ScaleDirection, + GraphTresholdsStyleMode, } from '../config'; -import { createTheme } from '@grafana/data'; +import { createTheme, ThresholdsMode } from '@grafana/data'; describe('UPlotConfigBuilder', () => { const darkTheme = createTheme(); @@ -658,4 +659,37 @@ describe('UPlotConfigBuilder', () => { `); }); }); + + describe('Thresholds', () => { + it('Only adds one threshold per scale', () => { + const builder = new UPlotConfigBuilder(); + const addHookFn = jest.fn(); + builder.addHook = addHookFn; + + builder.addThresholds({ + scaleKey: 'A', + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [], + }, + config: { + mode: GraphTresholdsStyleMode.Area, + }, + theme: darkTheme, + }); + builder.addThresholds({ + scaleKey: 'A', + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [], + }, + config: { + mode: GraphTresholdsStyleMode.Area, + }, + theme: darkTheme, + }); + + expect(addHookFn).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts index d6ce04384f4..0cc8e77370a 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts @@ -7,6 +7,7 @@ import uPlot, { Cursor, Band, Hooks, BBox } from 'uplot'; import { defaultsDeep } from 'lodash'; import { DefaultTimeZone, getTimeZoneInfo } from '@grafana/data'; import { pluginLog } from '../utils'; +import { getThresholdsDrawHook, UPlotThresholdOptions } from './UPlotThresholds'; export class UPlotConfigBuilder { private series: UPlotSeriesBuilder[] = []; @@ -21,6 +22,8 @@ export class UPlotConfigBuilder { private hasBottomAxis = false; private hooks: Hooks.Arrays = {}; private tz: string | undefined = undefined; + // to prevent more than one threshold per scale + private thresholds: Record = {}; constructor(getTimeZone = () => DefaultTimeZone) { this.tz = getTimeZoneInfo(getTimeZone(), Date.now())?.ianaName; @@ -36,6 +39,13 @@ export class UPlotConfigBuilder { this.hooks[type]!.push(hook as any); } + addThresholds(options: UPlotThresholdOptions) { + if (!this.thresholds[options.scaleKey]) { + this.thresholds[options.scaleKey] = options; + this.addHook('drawClear', getThresholdsDrawHook(options)); + } + } + addAxis(props: AxisProps) { props.placement = props.placement ?? AxisPlacement.Auto; diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotThresholds.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotThresholds.ts new file mode 100644 index 00000000000..7c059765efb --- /dev/null +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotThresholds.ts @@ -0,0 +1,126 @@ +import { getColorForTheme, GrafanaTheme2, ThresholdsConfig } from '@grafana/data'; +import tinycolor from 'tinycolor2'; +import { GraphThresholdsStyleConfig, GraphTresholdsStyleMode } from '../config'; + +export interface UPlotThresholdOptions { + scaleKey: string; + thresholds: ThresholdsConfig; + config: GraphThresholdsStyleConfig; + theme: GrafanaTheme2; +} + +export function getThresholdsDrawHook(options: UPlotThresholdOptions) { + return (u: uPlot) => { + const ctx = u.ctx; + const { scaleKey, thresholds, theme, config } = options; + const { steps } = thresholds; + const { min: xMin, max: xMax } = u.scales.x; + const { min: yMin, max: yMax } = u.scales[scaleKey]; + + if (xMin === undefined || xMax === undefined || yMin === undefined || yMax === undefined) { + return; + } + + function addLines() { + // Thresholds below a transparent threshold is treated like "less than", and line drawn previous threshold + let transparentIndex = 0; + + for (let idx = 0; idx < steps.length; idx++) { + const step = steps[idx]; + if (step.color === 'transparent') { + transparentIndex = idx; + break; + } + } + + // Ignore the base -Infinity threshold by always starting on index 1 + for (let idx = 1; idx < steps.length; idx++) { + const step = steps[idx]; + let color: tinycolor.Instance; + + // if we are below a transparent index treat this a less then threshold, use previous thresholds color + if (transparentIndex >= idx && idx > 0) { + color = tinycolor(getColorForTheme(steps[idx - 1].color, theme.v1)); + } else { + color = tinycolor(getColorForTheme(step.color, theme.v1)); + } + + // Unless alpha specififed set to default value + if (color.getAlpha() === 1) { + color.setAlpha(0.7); + } + + let x0 = u.valToPos(xMin!, 'x', true); + let y0 = u.valToPos(step.value, scaleKey, true); + let x1 = u.valToPos(xMax!, 'x', true); + let y1 = u.valToPos(step.value, scaleKey, true); + + ctx.beginPath(); + ctx.lineWidth = 2; + ctx.strokeStyle = color.toString(); + ctx.moveTo(x0, y0); + ctx.lineTo(x1, y1); + ctx.stroke(); + ctx.closePath(); + } + } + + function addAreas() { + for (let idx = 0; idx < steps.length; idx++) { + const step = steps[idx]; + + // skip thresholds that cannot be seen + if (step.value > yMax!) { + continue; + } + + // if this is the last step make the next step the same color but +Infinity + const nextStep = + idx + 1 < steps.length + ? steps[idx + 1] + : { + ...step, + value: Infinity, + }; + + let color = tinycolor(getColorForTheme(step.color, theme.v1)); + + // Ignore fully transparent colors + const alpha = color.getAlpha(); + if (alpha === 0) { + continue; + } + + /// if no alpha set automatic alpha + if (alpha === 1) { + color = color.setAlpha(0.15); + } + + let value = step.value === -Infinity ? yMin : step.value; + let nextValue = nextStep.value === Infinity || nextStep.value > yMax! ? yMax : nextStep.value; + + let x0 = u.valToPos(xMin ?? 0, 'x', true); + let y0 = u.valToPos(value ?? 0, scaleKey, true); + let x1 = u.valToPos(xMax ?? 1, 'x', true); + let y1 = u.valToPos(nextValue ?? 1, scaleKey, true); + + ctx.save(); + ctx.fillStyle = color.toString(); + ctx.fillRect(x0, y0, x1 - x0, y1 - y0); + ctx.restore(); + } + } + + switch (config.mode) { + case GraphTresholdsStyleMode.Line: + addLines(); + break; + case GraphTresholdsStyleMode.Area: + addAreas(); + break; + case GraphTresholdsStyleMode.LineAndArea: + addLines(); + addAreas(); + } + }; +} diff --git a/public/app/plugins/panel/timeseries/config.ts b/public/app/plugins/panel/timeseries/config.ts index f18a1e5d113..423b1ede389 100644 --- a/public/app/plugins/panel/timeseries/config.ts +++ b/public/app/plugins/panel/timeseries/config.ts @@ -190,6 +190,16 @@ export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOption addStackingConfig(builder, cfg.stacking); addAxisConfig(builder, cfg); addHideFrom(builder); + + builder.addSelect({ + path: 'thresholdsStyle.mode', + name: 'Show thresholds', + category: ['Thresholds'], + defaultValue: graphFieldOptions.thresholdsDisplayModes[0].value, + settings: { + options: graphFieldOptions.thresholdsDisplayModes, + }, + }); }, }; } diff --git a/public/app/plugins/panel/timeseries/migrations.test.ts b/public/app/plugins/panel/timeseries/migrations.test.ts index de496597a97..0da3ecd6e53 100644 --- a/public/app/plugins/panel/timeseries/migrations.test.ts +++ b/public/app/plugins/panel/timeseries/migrations.test.ts @@ -67,6 +67,130 @@ describe('Graph Migrations', () => { expect(panel).toMatchSnapshot(); }); }); + + describe('thresholds', () => { + test('Only gt thresholds', () => { + const old: any = { + angular: { + thresholds: [ + { + colorMode: 'critical', + fill: true, + line: false, + op: 'gt', + value: 80, + yaxis: 'left', + }, + { + colorMode: 'warning', + fill: true, + line: false, + op: 'gt', + value: 50, + yaxis: 'left', + }, + ], + }, + }; + const panel = {} as PanelModel; + panel.options = graphPanelChangedHandler(panel, 'graph', old); + expect(panel.fieldConfig.defaults.custom.thresholdsStyle.mode).toBe('area'); + expect(panel.fieldConfig.defaults.thresholds?.steps).toMatchInlineSnapshot(` + Array [ + Object { + "color": "transparent", + "value": -Infinity, + }, + Object { + "color": "orange", + "value": 50, + }, + Object { + "color": "red", + "value": 80, + }, + ] + `); + }); + + test('gt & lt thresholds', () => { + const old: any = { + angular: { + thresholds: [ + { + colorMode: 'critical', + fill: true, + line: true, + op: 'gt', + value: 80, + yaxis: 'left', + }, + { + colorMode: 'warning', + fill: true, + line: true, + op: 'lt', + value: 40, + yaxis: 'left', + }, + ], + }, + }; + + const panel = {} as PanelModel; + panel.options = graphPanelChangedHandler(panel, 'graph', old); + expect(panel.fieldConfig.defaults.custom.thresholdsStyle.mode).toBe('line+area'); + expect(panel.fieldConfig.defaults.thresholds?.steps).toMatchInlineSnapshot(` + Array [ + Object { + "color": "orange", + "value": -Infinity, + }, + Object { + "color": "transparent", + "value": 40, + }, + Object { + "color": "red", + "value": 80, + }, + ] + `); + }); + + test('Only lt thresholds', () => { + const old: any = { + angular: { + thresholds: [ + { + colorMode: 'warning', + fill: true, + line: true, + op: 'lt', + value: 40, + yaxis: 'left', + }, + ], + }, + }; + + const panel = {} as PanelModel; + panel.options = graphPanelChangedHandler(panel, 'graph', old); + expect(panel.fieldConfig.defaults.custom.thresholdsStyle.mode).toBe('line+area'); + expect(panel.fieldConfig.defaults.thresholds?.steps).toMatchInlineSnapshot(` + Array [ + Object { + "color": "orange", + "value": -Infinity, + }, + Object { + "color": "transparent", + "value": 40, + }, + ] + `); + }); + }); }); const stairscase = { diff --git a/public/app/plugins/panel/timeseries/migrations.ts b/public/app/plugins/panel/timeseries/migrations.ts index 4d449b9d82c..052b8e2220a 100644 --- a/public/app/plugins/panel/timeseries/migrations.ts +++ b/public/app/plugins/panel/timeseries/migrations.ts @@ -9,12 +9,15 @@ import { fieldReducers, NullValueMode, PanelModel, + Threshold, + ThresholdsMode, } from '@grafana/data'; import { AxisPlacement, DrawStyle, GraphFieldConfig, GraphGradientMode, + GraphTresholdsStyleMode, LegendDisplayMode, LineInterpolation, LineStyle, @@ -312,6 +315,83 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour } } + if (angular.thresholds && angular.thresholds.length > 0) { + let steps: Threshold[] = []; + let area = false; + let line = false; + + const sorted = (angular.thresholds as AngularThreshold[]).sort((a, b) => (a.value > b.value ? 1 : -1)); + + for (let idx = 0; idx < sorted.length; idx++) { + const threshold = sorted[idx]; + const next = sorted.length > idx + 1 ? sorted[idx + 1] : null; + + if (threshold.fill) { + area = true; + } + + if (threshold.line) { + line = true; + } + + if (threshold.op === 'gt') { + steps.push({ + value: threshold.value, + color: getThresholdColor(threshold), + }); + } + + if (threshold.op === 'lt') { + if (steps.length === 0) { + steps.push({ + value: -Infinity, + color: getThresholdColor(threshold), + }); + } + + // next op is gt and there is a gap set color to transparent + if (next && next.op === 'gt' && next.value > threshold.value) { + steps.push({ + value: threshold.value, + color: 'transparent', + }); + // if next is a lt we need to use it's color + } else if (next && next.op === 'lt') { + steps.push({ + value: threshold.value, + color: getThresholdColor(next), + }); + } else { + steps.push({ + value: threshold.value, + color: 'transparent', + }); + } + } + } + + // if now less then threshold add an -Infinity base that is transparent + if (steps.length > 0 && steps[0].value !== -Infinity) { + steps.unshift({ + color: 'transparent', + value: -Infinity, + }); + } + + let displayMode = area ? GraphTresholdsStyleMode.Area : GraphTresholdsStyleMode.Line; + if (line && area) { + displayMode = GraphTresholdsStyleMode.LineAndArea; + } + + // TODO move into standard ThresholdConfig ? + y1.custom.thresholdsStyle = { mode: displayMode }; + + y1.thresholds = { + mode: ThresholdsMode.Absolute, + steps, + }; + } + return { fieldConfig: { defaults: omitBy(y1, isNil), @@ -321,6 +401,33 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour }; } +function getThresholdColor(threshold: AngularThreshold): string { + if (threshold.colorMode === 'critical') { + return 'red'; + } + + if (threshold.colorMode === 'warning') { + return 'orange'; + } + + if (threshold.colorMode === 'custom') { + return threshold.fillColor || threshold.lineColor; + } + + return 'red'; +} + +interface AngularThreshold { + op: string; + fill: boolean; + line: boolean; + value: number; + colorMode: 'critical' | 'warning' | 'custom'; + yaxis?: 'left' | 'right'; + fillColor: string; + lineColor: string; +} + // { // "label": "Y111", // "show": true,