diff --git a/.drone.star b/.drone.star index bac6e561223..64e35284af1 100644 --- a/.drone.star +++ b/.drone.star @@ -2,9 +2,10 @@ load('scripts/pr.star', 'pr_pipelines') load('scripts/main.star', 'main_pipelines') load('scripts/release.star', 'release_pipelines', 'test_release_pipelines') load('scripts/version.star', 'version_branch_pipelines') +load('scripts/job.star', 'cronjobs') load('scripts/vault.star', 'secrets') def main(ctx): edition = 'oss' return pr_pipelines(edition=edition) + main_pipelines(edition=edition) + release_pipelines() + \ - test_release_pipelines() + version_branch_pipelines() + secrets() + test_release_pipelines() + version_branch_pipelines() + cronjobs(edition=edition) + secrets() diff --git a/.drone.yml b/.drone.yml index 1f874631a89..8da19a6c170 100644 --- a/.drone.yml +++ b/.drone.yml @@ -3432,6 +3432,38 @@ depends_on: - enterprise-build-release-branch - enterprise-windows-release-branch +--- +kind: pipeline +type: docker +name: scan-docker-images + +platform: + os: linux + arch: amd64 + +steps: +- name: scan-docker-images-unkown-low-medium-vulnerabilities + image: aquasec/trivy:0.18.3 + commands: + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/grafana:latest + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/grafana:main + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/grafana:latest-ubuntu + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/grafana:main-ubuntu + +- name: scan-docker-images-high-critical-vulnerabilities + image: aquasec/trivy:0.18.3 + commands: + - trivy --exit-code 1 --severity HIGH,CRITICAL grafana/grafana:latest + - trivy --exit-code 1 --severity HIGH,CRITICAL grafana/grafana:main + - trivy --exit-code 1 --severity HIGH,CRITICAL grafana/grafana:latest-ubuntu + - trivy --exit-code 1 --severity HIGH,CRITICAL grafana/grafana:main-ubuntu + +trigger: + cron: + - nightly + event: + - cron + --- kind: secret name: dockerconfigjson diff --git a/UPGRADING_DEPENDENCIES.md b/UPGRADING_DEPENDENCIES.md index 3f3c87db4bd..914309f287a 100644 --- a/UPGRADING_DEPENDENCIES.md +++ b/UPGRADING_DEPENDENCIES.md @@ -4,14 +4,14 @@ Upgrading Go or Node.js requires making changes in many different files. See bel ## Go -- CircleCi +- Drone - `grafana/build-container` - Appveyor - Dockerfile ## Node.js -- CircleCI +- Drone - `grafana/build-container` - Appveyor - Dockerfile @@ -42,6 +42,10 @@ GO111MODULE=on go mod tidy You have to commit the changes to `go.mod` and `go.sum` before submitting the pull request. +### Upgrading dependencies + +If you need to upgrade a direct or indirect dependency, you can do it like so, $MODULE being the dependency in question: `go get -u $MODULE`. The corresponding entry in go.mod should then have the version you specified; if it's an indirect dependency, the entry should have the `// indirect` comment. Follow this by executing `go mod tidy`, to ensure that go.mod and go.sum are up to date. If the indirect dependency turns out to not be used (transitively) by any of our packages, `go mod tidy` will actually strip it from go.mod. In that case, you can just ignore it since it isn't used in the end. + ## Node.js dependencies Updated using `yarn`. @@ -50,9 +54,9 @@ Updated using `yarn`. ## Where to make changes -### CircleCI +### Drone -Our builds run on CircleCI through our build script. +Our CI builds run on Drone. #### Files @@ -66,7 +70,7 @@ Our builds run on CircleCI through our build script. ### grafana/build-container -The main build step (in CircleCI) is built using a custom build container that comes pre-baked with some of the necessary dependencies. +The main build steps (in Drone) happen using a custom Docker image that comes pre-baked with some of the necessary dependencies. Link: [grafana/build-container](https://github.com/grafana/grafana/tree/main/scripts/build/ci-build) diff --git a/devenv/dev-dashboards/datasource-testdata/new_features_in_v8.json b/devenv/dev-dashboards/datasource-testdata/new_features_in_v8.json new file mode 100644 index 00000000000..266ed794efa --- /dev/null +++ b/devenv/dev-dashboards/datasource-testdata/new_features_in_v8.json @@ -0,0 +1,2841 @@ +{ + "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, + "id": 625, + "links": [], + "panels": [ + { + "datasource": null, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 13, + "options": { + "content": "\n# Welcome to Grafana 8.0\n\nGrafana v8.0 adds many visualizations and improvements to panel edit, tracing and security. This dashboard highlights \nsome of the new visualization capabilities. There are so many new visualizations and display options so be sure to scroll down\nto see everything. \n\n[Read the what's new article](https://grafana.com/docs/grafana/next/whatsnew/whats-new-in-v8-0/)   |  \n[Download](https://grafana.com/grafana/download/beta)   |  \n[Grafana Cloud now free with 50gb logs, 10K metric series](https://grafana.com/signup/cloud/connect-account?pg=play&plcmt=whatnew)", + "mode": "markdown" + }, + "pluginVersion": "8.1.0-pre", + "targets": [ + { + "refId": "A", + "target": "" + } + ], + "timeFrom": null, + "timeShift": null, + "transparent": true, + "type": "text" + }, + { + "cacheTimeout": null, + "datasource": "gdev-testdata", + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 4 + }, + "id": 61, + "links": [], + "options": { + "content": "## State timeline\n\nThe new state timeline panel is one of the new core visualizations. \nIt adds unique state over time visualization for string or boolean states. \n\n### Map strings to colors\n\nWith the new revamped value mapping feature you \ncan now easily map strings, booleans, nulls & NaN values\nto a color or new display text. \n\nThis mapping and coloring support will work in all new core panels.", + "mode": "markdown" + }, + "pluginVersion": "8.1.0-pre", + "targets": [ + { + "alias": "__server_names", + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 6 + } + ], + "type": "text" + }, + { + "datasource": "gdev-testdata", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "fillOpacity": 80, + "lineWidth": 0 + }, + "mappings": [ + { + "options": { + "CRITICAL": { + "color": "red", + "index": 3 + }, + "HIGH": { + "color": "orange", + "index": 2 + }, + "LOW": { + "color": "blue", + "index": 0 + }, + "NORMAL": { + "color": "green", + "index": 1 + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 18, + "x": 6, + "y": 4 + }, + "id": 63, + "options": { + "alignValue": "center", + "colWidth": 0.9, + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "right" + }, + "mergeValues": true, + "mode": "changes", + "rowHeight": 0.97, + "showValue": "always" + }, + "pluginVersion": "7.5.0-pre", + "targets": [ + { + "alias": "Level A", + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "LOW,HIGH,NORMAL,NORMAL,NORMAL,LOW,LOW,NORMAL,HIGH,CRITICAL" + }, + { + "alias": "Level B", + "hide": false, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "NORMAL,LOW,LOW,CRITICAL,CRITICAL,LOW,LOW,NORMAL,HIGH,CRITICAL" + }, + { + "alias": "Level C", + "hide": false, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "NORMAL,NORMAL,NORMAL,NORMAL,CRITICAL,LOW,NORMAL,NORMAL,NORMAL,LOW" + } + ], + "title": "State timeline strings", + "type": "state-timeline" + }, + { + "cacheTimeout": null, + "datasource": "gdev-testdata", + "gridPos": { + "h": 15, + "w": 6, + "x": 0, + "y": 12 + }, + "id": 70, + "links": [], + "options": { + "content": "### Discrete time series\n\nIt can also transform any numerical time series into discrete states trough \nthresholds. This allow you to quickly gauge health over time and visualize\nthe duration spent in each threshold bracket. \n", + "mode": "markdown" + }, + "pluginVersion": "8.1.0-pre", + "targets": [ + { + "alias": "__server_names", + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 6 + } + ], + "type": "text" + }, + { + "datasource": "gdev-graphite-1.0", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 100, + "lineWidth": 0 + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + }, + { + "color": "green", + "value": 50 + }, + { + "color": "red", + "value": 300 + } + ] + }, + "unit": "degree" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 18, + "x": 6, + "y": 12 + }, + "id": 65, + "maxDataPoints": 200, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "mergeValues": true, + "rowHeight": 0.96, + "showValue": "never" + }, + "targets": [ + { + "refId": "A", + "target": "aliasByNode(apps.backend.*.counters.requests.count, 2)" + } + ], + "title": "State timeline with time series + thresholds", + "type": "state-timeline" + }, + { + "datasource": "gdev-graphite-1.0", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "axisWidth": 85, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "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": "area" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + }, + { + "color": "green", + "value": 50 + }, + { + "color": "transparent", + "value": 50 + }, + { + "color": "red", + "value": 300 + } + ] + }, + "unit": "degree" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 18, + "x": 6, + "y": 20 + }, + "id": 66, + "maxDataPoints": 200, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "8.0.0-beta2", + "targets": [ + { + "refId": "A", + "target": "aliasByNode(apps.backend.*.counters.requests.count, 2)" + } + ], + "title": "Same query/data as above", + "type": "timeseries" + }, + { + "cacheTimeout": null, + "datasource": "gdev-testdata", + "gridPos": { + "h": 13, + "w": 6, + "x": 0, + "y": 27 + }, + "id": 67, + "links": [], + "options": { + "content": "## Status grid\n\nA sister panel to the state timeline is the new Status grid panel. It can visualize periodic state in a \ngrid. Works with both numerical, string or boolean state. ", + "mode": "markdown" + }, + "pluginVersion": "8.1.0-pre", + "targets": [ + { + "alias": "__server_names", + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 6 + } + ], + "type": "text" + }, + { + "datasource": "gdev-testdata", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 1 + }, + "mappings": [], + "max": 40, + "min": -10, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + }, + { + "color": "#EAB839", + "value": 0 + } + ] + }, + "unit": "degree" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 18, + "x": 6, + "y": 27 + }, + "id": 68, + "maxDataPoints": 10, + "options": { + "colWidth": 0.95, + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "rowHeight": 0.9, + "showValue": "auto" + }, + "targets": [ + { + "alias": "__house_locations", + "max": 40, + "min": -10, + "noise": 5, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 4, + "spread": 20, + "startValue": 10 + } + ], + "title": "State timeline with time series + thresholds", + "type": "status-grid" + }, + { + "datasource": "gdev-testdata", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "fillOpacity": 70, + "lineWidth": 1 + }, + "mappings": [ + { + "options": { + "match": "true", + "result": { + "color": "green", + "index": 0, + "text": "True" + } + }, + "type": "special" + }, + { + "options": { + "match": "false", + "result": { + "color": "red", + "index": 1, + "text": "False" + } + }, + "type": "special" + } + ], + "max": 40, + "min": -10, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + }, + { + "color": "#EAB839", + "value": 0 + } + ] + }, + "unit": "degree" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 18, + "x": 6, + "y": 35 + }, + "id": 69, + "maxDataPoints": 10, + "options": { + "colWidth": 0.95, + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "rowHeight": 0.9, + "showValue": "auto" + }, + "targets": [ + { + "alias": "SensorA", + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "true,false,true,false,true,true,true,true,false" + }, + { + "alias": "SensorB", + "hide": false, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "true,true,true,false,true,true,false,true,false" + } + ], + "title": "Status grid - boolean values", + "type": "status-grid" + }, + { + "cacheTimeout": null, + "datasource": "gdev-testdata", + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 40 + }, + "id": 59, + "links": [], + "options": { + "content": "## Bar chart \n\nA new bar chart panel adds new graphing capabilities to Grafana, especially for non\ntime series based data. It supports a categorical x or y field, grouped bars, horizontal \nand vertical layout. ", + "mode": "markdown" + }, + "pluginVersion": "8.1.0-pre", + "targets": [ + { + "alias": "__server_names", + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 6 + } + ], + "type": "text" + }, + { + "datasource": "gdev-testdata", + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "blue", + "mode": "fixed" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "fillOpacity": 74, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineWidth": 1 + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "pressure" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 43 + }, + "id": 73, + "options": { + "barWidth": 0.81, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "right" + }, + "orientation": "horizontal", + "showValue": "auto", + "text": {}, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "csvFileName": "browser_marketshare.csv", + "refId": "A", + "scenarioId": "csv_file" + } + ], + "title": "Browser market share", + "transformations": [ + { + "id": "filterByValue", + "options": { + "filters": [ + { + "config": { + "id": "greater", + "options": { + "value": 0.37 + } + }, + "fieldName": "Market share" + } + ], + "match": "any", + "type": "include" + } + }, + { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "desc": true, + "field": "Market share" + } + ] + } + } + ], + "type": "barchart" + }, + { + "datasource": "gdev-testdata", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineWidth": 1 + }, + "mappings": [ + { + "options": { + "match": "true", + "result": { + "color": "green", + "index": 0, + "text": "True" + } + }, + "type": "special" + }, + { + "options": { + "match": "false", + "result": { + "color": "red", + "index": 1, + "text": "False" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 43 + }, + "id": 74, + "maxDataPoints": 10, + "options": { + "barWidth": 0.97, + "groupWidth": 0.7, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right" + }, + "orientation": "auto", + "showValue": "auto", + "text": {}, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "alias": "SensorA", + "csvFileName": "js_libraries.csv", + "refId": "A", + "scenarioId": "csv_file" + } + ], + "title": "Popular JS Frameworks", + "type": "barchart" + }, + { + "cacheTimeout": null, + "datasource": "gdev-testdata", + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 53 + }, + "id": 71, + "links": [], + "options": { + "content": "## Next-gen Graph\n\nThe new next-gen graphing visualization named `Time series` is now the default\nvisualization in v8. It supports many new options as well as improved performance.\nBelow we highlight some new features. ", + "mode": "markdown" + }, + "pluginVersion": "8.1.0-pre", + "targets": [ + { + "alias": "__server_names", + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 6 + } + ], + "type": "text" + }, + { + "cacheTimeout": null, + "datasource": "gdev-testdata", + "gridPos": { + "h": 6, + "w": 6, + "x": 0, + "y": 56 + }, + "id": 25, + "links": [], + "options": { + "content": "### Interpolation modes\n\nThe new time series panel brings a new interpolation mode\nwhere you can choose between.\n* Linear\n* Smooth\n* Step before\n* Step after (same as old staircase option)", + "mode": "markdown" + }, + "pluginVersion": "8.1.0-pre", + "targets": [ + { + "alias": "__server_names", + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 6 + } + ], + "type": "text" + }, + { + "datasource": "gdev-testdata", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "hue", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Requests/s" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 6, + "y": 56 + }, + "id": 23, + "maxDataPoints": 80, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "7.4.0-beta1", + "targets": [ + { + "alias": "Requests/s", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 1, + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Interpolation mode: smooth", + "type": "timeseries" + }, + { + "datasource": "gdev-testdata", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "hue", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "stepBefore", + "lineWidth": 2, + "pointSize": 8, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "always", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Requests/s" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 12, + "y": 56 + }, + "id": 26, + "maxDataPoints": 80, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "7.4.0-beta1", + "targets": [ + { + "alias": "Requests/s", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "seriesCount": 1, + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "1,20,90,30,5,0" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Interpolation mode: Step before", + "type": "timeseries" + }, + { + "datasource": "gdev-testdata", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "hue", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "stepAfter", + "lineWidth": 2, + "pointSize": 8, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "always", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Requests/s" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 56 + }, + "id": 27, + "maxDataPoints": 80, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "7.4.0-beta1", + "targets": [ + { + "alias": "Requests/s", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "seriesCount": 1, + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "1,20,90,30,5,0" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Interpolation mode: Step after", + "type": "timeseries" + }, + { + "cacheTimeout": null, + "datasource": "gdev-testdata", + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 62 + }, + "id": 43, + "links": [], + "options": { + "content": "### Soft min and max\n\nAuto **Min** and **Max** can make small variations look much bigger than they are. This can be fixed by setting **Min** & **Max**. \nThis creates a new problem when spikes in the data occur. Because the \n**Min** and **Max** are hard the full spike cannot be seen. The **Soft min** and **Soft max** options give control over the default range of the axis but also allow going beyond this range when the data does. \n\nYou can combine both the existing standard option **Min** and **Max** and the new **Soft min**\n and **Soft max** to define both soft and hard limits. ", + "mode": "markdown" + }, + "pluginVersion": "8.1.0-pre", + "targets": [ + { + "alias": "__server_names", + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 6 + } + ], + "type": "text" + }, + { + "datasource": "gdev-testdata", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": { + "label": "None", + "value": "none" + }, + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "degree" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 6, + "y": 62 + }, + "id": 41, + "options": { + "graph": {}, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "7.4.0-beta1", + "targets": [ + { + "alias": "", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "10,11,12,11,10,11,12,12,11,10,9,10,11,12,10,10,11,12,13,11,10,9,10,11,12,13,14,10,10" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Auto min max", + "type": "timeseries" + }, + { + "datasource": "gdev-testdata", + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": { + "label": "None", + "value": "none" + }, + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "max": 30, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "degree" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 12, + "y": 62 + }, + "id": 32, + "options": { + "graph": {}, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "7.4.0-beta1", + "targets": [ + { + "alias": "", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "10,11,12,11,10,11,12,12,11,10,9,10,11,12,200,10,11,12,13,11,10,9,10,11,12,13,14,10,10" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Hard min 0, max 30", + "type": "timeseries" + }, + { + "datasource": "gdev-testdata", + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMax": 30, + "axisSoftMin": 0, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": { + "label": "None", + "value": "none" + }, + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "degree" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 18, + "y": 62 + }, + "id": 44, + "options": { + "graph": {}, + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "7.4.0-beta1", + "targets": [ + { + "alias": "", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "10,11,12,11,10,11,12,12,11,10,9,10,11,12,200,10,11,12,13,11,10,9,10,11,12,13,14,10,10" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Soft min 0, soft max 30", + "type": "timeseries" + }, + { + "cacheTimeout": null, + "datasource": "gdev-testdata", + "gridPos": { + "h": 5, + "w": 6, + "x": 0, + "y": 70 + }, + "id": 55, + "links": [], + "options": { + "content": "### Multiple Y-Axes\n\nWith the new panel you can have more than 2 y-axes! ", + "mode": "markdown" + }, + "pluginVersion": "8.1.0-pre", + "targets": [ + { + "alias": "__server_names", + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 6 + } + ], + "type": "text" + }, + { + "datasource": "gdev-testdata", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMax": 60, + "axisSoftMin": 0, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "opacity", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Temperature" + }, + "properties": [ + { + "id": "unit", + "value": "celsius" + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Pressure" + }, + "properties": [ + { + "id": "unit", + "value": "pressurekpa" + }, + { + "id": "custom.axisPlacement", + "value": "right" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Energy" + }, + "properties": [ + { + "id": "unit", + "value": "watt" + }, + { + "id": "custom.axisPlacement", + "value": "left" + } + ] + } + ] + }, + "gridPos": { + "h": 5, + "w": 18, + "x": 6, + "y": 70 + }, + "id": 54, + "maxDataPoints": 50, + "options": { + "legend": { + "calcs": [ + "last" + ], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "pluginVersion": "7.4.0-beta1", + "targets": [ + { + "alias": "Temperature", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "20,25,30,18,35,20,30,25" + }, + { + "alias": "Pressure", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "hide": false, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "B", + "scenarioId": "csv_metric_values", + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "320,230,120,180,400,340,50,270" + }, + { + "alias": "Energy", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "hide": false, + "lines": 10, + "max": 300, + "min": 100, + "noise": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "C", + "scenarioId": "random_walk", + "spread": 100, + "startValue": 0, + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "" + } + ], + "title": "Multiple Y-Axes (more than 2!)", + "type": "timeseries" + }, + { + "cacheTimeout": null, + "datasource": "gdev-testdata", + "gridPos": { + "h": 5, + "w": 6, + "x": 0, + "y": 75 + }, + "id": 51, + "links": [], + "options": { + "content": "### Richer display options\n\nThe new time series panel supports more control over lines, fill gradients that \nwork for both line, area graphs & bars. ", + "mode": "markdown" + }, + "pluginVersion": "8.1.0-pre", + "targets": [ + { + "alias": "__server_names", + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 6 + } + ], + "type": "text" + }, + { + "datasource": "gdev-testdata", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": { + "label": "None", + "value": "none" + }, + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "B-series" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 20 + ], + "fill": "dash" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "C-series" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 30 + ], + "fill": "dash" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "D-series" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 30, + 3, + 3 + ], + "fill": "dash" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "E-series" + }, + "properties": [ + { + "id": "custom.lineStyle" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "F-series" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 0, + 10 + ], + "fill": "dot" + } + } + ] + } + ] + }, + "gridPos": { + "h": 5, + "w": 6, + "x": 6, + "y": 75 + }, + "id": 48, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "7.4.0-beta1", + "targets": [ + { + "alias": "", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "10,10" + }, + { + "alias": "", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "B", + "scenarioId": "csv_metric_values", + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "9,9" + }, + { + "alias": "", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "C", + "scenarioId": "csv_metric_values", + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "8,8" + }, + { + "alias": "", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "D", + "scenarioId": "csv_metric_values", + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "7,7" + }, + { + "alias": "", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "E", + "scenarioId": "csv_metric_values", + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "6,6" + }, + { + "alias": "", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "F", + "scenarioId": "csv_metric_values", + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "5,5" + } + ], + "title": "Advanced dashed line settings", + "type": "timeseries" + }, + { + "datasource": "gdev-testdata", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": { + "label": "None", + "value": "none" + }, + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "dash": [ + 10, + 10 + ], + "fill": "dash" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "B-series" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 20 + ], + "fill": "dash" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "C-series" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 10, + 30 + ], + "fill": "dash" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "D-series" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 30, + 3, + 3 + ], + "fill": "dash" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "E-series" + }, + "properties": [ + { + "id": "custom.lineStyle" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "F-series" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "dash": [ + 0, + 10 + ], + "fill": "dot" + } + } + ] + } + ] + }, + "gridPos": { + "h": 5, + "w": 6, + "x": 12, + "y": 75 + }, + "id": 76, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "7.4.0-beta1", + "targets": [ + { + "alias": "", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "10,10" + }, + { + "alias": "", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "B", + "scenarioId": "csv_metric_values", + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "9,9" + }, + { + "alias": "", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "C", + "scenarioId": "csv_metric_values", + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "8,8" + }, + { + "alias": "", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "D", + "scenarioId": "csv_metric_values", + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "7,7" + }, + { + "alias": "", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "E", + "scenarioId": "csv_metric_values", + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "6,6" + }, + { + "alias": "", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "F", + "scenarioId": "csv_metric_values", + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "5,5" + } + ], + "title": "Advanced dashed line settings", + "type": "timeseries" + }, + { + "datasource": "gdev-testdata", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 82, + "gradientMode": "hue", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "dash": [ + 10, + 10 + ], + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A-series" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 5, + "w": 6, + "x": 18, + "y": 75 + }, + "id": 50, + "maxDataPoints": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + }, + "tooltip": { + "mode": "single" + } + }, + "pluginVersion": "7.4.0-beta1", + "targets": [ + { + "alias": "", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "hide": false, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "A", + "scenarioId": "random_walk", + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "1,20,90,30,5,0" + } + ], + "title": "Bars with hue gradient", + "type": "timeseries" + }, + { + "cacheTimeout": null, + "datasource": "gdev-testdata", + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 80 + }, + "id": 75, + "links": [], + "options": { + "content": "## Histogram\n\nThis hidden feature of the old Graph panel is now a standalone visualization. It combines a histogram \ntransformation and bar chart visualization into single integrated easy to use panel. There is also a new standalone histogram \ntransformation that can be paired with any visualization. ", + "mode": "markdown" + }, + "pluginVersion": "8.1.0-pre", + "targets": [ + { + "alias": "__server_names", + "refId": "A", + "scenarioId": "random_walk", + "seriesCount": 6 + } + ], + "type": "text" + }, + { + "datasource": "gdev-testdata", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "fillOpacity": 80, + "gradientMode": "hue", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineWidth": 1 + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A-series" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 11, + "w": 12, + "x": 0, + "y": 83 + }, + "id": 49, + "maxDataPoints": null, + "options": { + "bucketOffset": 0, + "bucketSize": 10, + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom" + } + }, + "pluginVersion": "7.4.0-beta1", + "targets": [ + { + "alias": "", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "hide": false, + "lines": 10, + "max": 100, + "min": 1, + "noise": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "A", + "scenarioId": "random_walk", + "spread": 10, + "startValue": 50, + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "1,20,90,30,5,0" + } + ], + "title": "Histogram visualization", + "type": "histogram" + }, + { + "datasource": "gdev-testdata", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A-series" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 11, + "w": 12, + "x": 12, + "y": 83 + }, + "id": 77, + "maxDataPoints": null, + "options": { + "showHeader": true + }, + "pluginVersion": "8.1.0-pre", + "targets": [ + { + "alias": "", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "hide": false, + "lines": 10, + "max": 100, + "min": 1, + "noise": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "A", + "scenarioId": "random_walk", + "spread": 10, + "startValue": 50, + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "1,20,90,30,5,0" + } + ], + "title": "Histogram transform ", + "transformations": [ + { + "id": "histogram", + "options": { + "bucketSize": 10, + "combine": true, + "fields": {} + } + } + ], + "type": "table" + } + ], + "refresh": false, + "schemaVersion": 30, + "style": "dark", + "tags": [ + "gdev", + "demo" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "New Features in v8.0", + "uid": "8mux8PqGz", + "version": 17 + } \ No newline at end of file diff --git a/devenv/dev-dashboards/panel-barchart/barchart-autosizing.json b/devenv/dev-dashboards/panel-barchart/barchart-autosizing.json index a7b26f42270..27aa029161f 100644 --- a/devenv/dev-dashboards/panel-barchart/barchart-autosizing.json +++ b/devenv/dev-dashboards/panel-barchart/barchart-autosizing.json @@ -15,7 +15,6 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "id": 441, "links": [], "panels": [ { @@ -56,19 +55,21 @@ "overrides": [] }, "gridPos": { - "h": 13, - "w": 24, + "h": 10, + "w": 12, "x": 0, "y": 0 }, "id": 9, "options": { "barWidth": 1, - "groupWidth": 1, + "groupWidth": 0.82, "legend": { - "calcs": [], + "calcs": [ + "max" + ], "displayMode": "list", - "placement": "bottom" + "placement": "right" }, "orientation": "auto", "showValue": "auto", @@ -79,11 +80,85 @@ }, "targets": [ { + "csvContent": "Name,Stat1,Stat2\nStockholm, 10, 15\nNew York, 19, 5\nLondon, 10, 1\nNegative, 15, -5\nLong value, 15,10", "refId": "A", - "scenarioId": "categorical_data" + "scenarioId": "csv_content" } ], - "title": "Plenty od data, automatic value sizing", + "title": "Auto sizing & auto show values", + "type": "barchart" + }, + { + "datasource": null, + "description": "Should be smaller given the longer value", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineWidth": 0 + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 15, + "options": { + "barWidth": 1, + "groupWidth": 0.82, + "legend": { + "calcs": [ + "max" + ], + "displayMode": "list", + "placement": "right" + }, + "orientation": "auto", + "showValue": "auto", + "text": {}, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "csvContent": "Name,Stat1,Stat2\nStockholm, 10, 15\nNew York, 19, 5\nLondon, 10, 1\nNegative, 15, -5\nLong value, 15,10", + "refId": "A", + "scenarioId": "csv_content" + } + ], + "title": "Auto sizing & auto show values", "type": "barchart" }, { @@ -124,10 +199,152 @@ "overrides": [] }, "gridPos": { - "h": 12, - "w": 24, + "h": 11, + "w": 8, "x": 0, - "y": 13 + "y": 10 + }, + "id": 16, + "options": { + "barWidth": 1, + "groupWidth": 0.89, + "legend": { + "calcs": [ + "max" + ], + "displayMode": "list", + "placement": "right" + }, + "orientation": "auto", + "showValue": "auto", + "text": {}, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "csvContent": "Name,Stat1,Stat2,Stat3,Stat4,Stat5,Stat6,Stat7,Stat8,Stat9,Stat10\nA, 10, 15,8,3,4,12,14,1,5,10\nB, 19, 5,8,3,4,12,14,6,7,2\nC, 15, 5,8,3,4,10,4,6,7,2\n", + "refId": "A", + "scenarioId": "csv_content" + } + ], + "title": "auto show values & No room for value", + "type": "barchart" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineWidth": 0 + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 8, + "y": 10 + }, + "id": 17, + "options": { + "barWidth": 1, + "groupWidth": 0.89, + "legend": { + "calcs": [ + "max" + ], + "displayMode": "list", + "placement": "right" + }, + "orientation": "auto", + "showValue": "always", + "text": {}, + "tooltip": { + "mode": "single" + } + }, + "targets": [ + { + "csvContent": "Name,Stat1,Stat2,Stat3,Stat4,Stat5,Stat6,Stat7,Stat8,Stat9,Stat10\nA, 10, 15,8,3,4,12,14,1,5,10\nB, 19, 5,8,3,4,12,14,6,7,2\nC, 15, 5,8,3,4,10,4,6,7,2\n", + "refId": "A", + "scenarioId": "csv_content" + } + ], + "title": "auto show values & Always show value", + "type": "barchart" + }, + { + "datasource": "-- Dashboard --", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineWidth": 0 + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 8, + "x": 16, + "y": 10 }, "id": 10, "options": { @@ -142,7 +359,7 @@ "showValue": "auto", "text": { "size": 10, - "valueSize": 10 + "valueSize": 25 }, "tooltip": { "mode": "single" @@ -150,11 +367,11 @@ }, "targets": [ { - "refId": "A", - "scenarioId": "categorical_data" + "panelId": 9, + "refId": "A" } ], - "title": "Plenty od data, fixed value sizing", + "title": "Fixed value sizing", "type": "barchart" }, { @@ -195,267 +412,42 @@ "overrides": [] }, "gridPos": { - "h": 12, + "h": 11, "w": 12, "x": 0, - "y": 25 + "y": 21 }, - "id": 11, + "id": 18, "options": { "barWidth": 1, - "groupWidth": 1, + "groupWidth": 0.82, "legend": { - "calcs": [], + "calcs": [ + "max" + ], "displayMode": "list", - "placement": "bottom" - }, - "orientation": "auto", - "showValue": "auto", - "text": { - "size": 10 - }, - "tooltip": { - "mode": "single" - } - }, - "targets": [ - { - "refId": "A", - "scenarioId": "categorical_data" - } - ], - "title": "Auto font size, value auto visible", - "transformations": [ - { - "id": "filterByValue", - "options": { - "filters": [ - { - "config": { - "id": "equal", - "options": { - "value": "Bedroom" - } - }, - "fieldName": "location" - }, - { - "config": { - "id": "equal", - "options": { - "value": "Cellar" - } - }, - "fieldName": "location" - } - ], - "match": "any", - "type": "include" - } - } - ], - "type": "barchart" - }, - { - "datasource": null, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "axisSoftMin": 0, - "fillOpacity": 80, - "gradientMode": "none", - "hideFrom": { - "graph": false, - "legend": false, - "tooltip": false - }, - "lineWidth": 0 - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 12, - "w": 12, - "x": 12, - "y": 25 - }, - "id": 12, - "options": { - "barWidth": 1, - "groupWidth": 1, - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom" - }, - "orientation": "auto", - "showValue": "always", - "text": { - "size": 10 - }, - "tooltip": { - "mode": "single" - } - }, - "targets": [ - { - "refId": "A", - "scenarioId": "categorical_data" - } - ], - "title": "Auto font size, value always visible", - "transformations": [ - { - "id": "filterByValue", - "options": { - "filters": [ - { - "config": { - "id": "equal", - "options": { - "value": "Bedroom" - } - }, - "fieldName": "location" - }, - { - "config": { - "id": "equal", - "options": { - "value": "Cellar" - } - }, - "fieldName": "location" - } - ], - "match": "any", - "type": "include" - } - } - ], - "type": "barchart" - }, - { - "datasource": null, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "axisSoftMin": 0, - "fillOpacity": 80, - "gradientMode": "none", - "hideFrom": { - "graph": false, - "legend": false, - "tooltip": false - }, - "lineWidth": 0 - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 12, - "w": 12, - "x": 0, - "y": 37 - }, - "id": 13, - "options": { - "barWidth": 1, - "groupWidth": 1, - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom" + "placement": "right" }, "orientation": "horizontal", "showValue": "auto", - "text": { - "size": 10 - }, + "text": {}, "tooltip": { "mode": "single" } }, "targets": [ { + "csvContent": "Name,Stat1,Stat2\nStockholm, 10, 15\nNew York, 19, 5\nLondon, 10, 1\nLong value, 15,10", "refId": "A", - "scenarioId": "categorical_data" - } - ], - "title": "Auto font size, value auto visible", - "transformations": [ - { - "id": "filterByValue", - "options": { - "filters": [ - { - "config": { - "id": "equal", - "options": { - "value": "Bedroom" - } - }, - "fieldName": "location" - }, - { - "config": { - "id": "equal", - "options": { - "value": "Cellar" - } - }, - "fieldName": "location" - } - ], - "match": "any", - "type": "include" - } + "scenarioId": "csv_content" } ], + "title": "Auto sizing & auto show values", "type": "barchart" }, { "datasource": null, + "description": "", "fieldConfig": { "defaults": { "color": { @@ -492,65 +484,37 @@ "overrides": [] }, "gridPos": { - "h": 12, + "h": 11, "w": 12, "x": 12, - "y": 37 + "y": 21 }, - "id": 14, + "id": 19, "options": { "barWidth": 1, - "groupWidth": 1, + "groupWidth": 0.89, "legend": { - "calcs": [], + "calcs": [ + "max" + ], "displayMode": "list", - "placement": "bottom" + "placement": "right" }, "orientation": "horizontal", - "showValue": "always", - "text": { - "size": 10 - }, + "showValue": "auto", + "text": {}, "tooltip": { "mode": "single" } }, "targets": [ { + "csvContent": "Name,Stat1,Stat2,Stat3,Stat4,Stat5,Stat6,Stat7,Stat8,Stat9,Stat10\nA, 10, 15,8,3,4,12,14,1,5,10\nB, 19, 5,8,3,4,12,14,6,7,2\nC, 15, 5,8,3,4,10,4,6,7,2\n", "refId": "A", - "scenarioId": "categorical_data" - } - ], - "title": "Auto font size, value always visible", - "transformations": [ - { - "id": "filterByValue", - "options": { - "filters": [ - { - "config": { - "id": "equal", - "options": { - "value": "Bedroom" - } - }, - "fieldName": "location" - }, - { - "config": { - "id": "equal", - "options": { - "value": "Cellar" - } - }, - "fieldName": "location" - } - ], - "match": "any", - "type": "include" - } + "scenarioId": "csv_content" } ], + "title": "auto show values & little room", "type": "barchart" } ], @@ -571,7 +535,7 @@ }, "timepicker": {}, "timezone": "", - "title": "BarChart - value text sizing", + "title": "BarChart - Panel Tests - Value sizing", "uid": "WFlOM-jM1", - "version": 9 -} + "version": 3 +} \ No newline at end of file diff --git a/devenv/docker/blocks/slow_proxy/.env b/devenv/docker/blocks/slow_proxy/.env index bd169d2d680..74f4e9c1841 100644 --- a/devenv/docker/blocks/slow_proxy/.env +++ b/devenv/docker/blocks/slow_proxy/.env @@ -1,2 +1,2 @@ -ORIGIN_SERVER=http://localhost:9090/ +ORIGIN_SERVER=http://host.docker.internal:9090/ SLEEP_DURATION=60s \ No newline at end of file diff --git a/devenv/docker/blocks/slow_proxy/Dockerfile b/devenv/docker/blocks/slow_proxy/Dockerfile index d48c945f5ab..735d433414c 100644 --- a/devenv/docker/blocks/slow_proxy/Dockerfile +++ b/devenv/docker/blocks/slow_proxy/Dockerfile @@ -1,7 +1,10 @@ - -FROM golang:latest +FROM golang:latest as builder ADD main.go / WORKDIR / -RUN GO111MODULE=off go build -o main . +RUN GO111MODULE=off CGO_ENABLED=0 go build -o main . + +FROM scratch +WORKDIR / EXPOSE 3011 +COPY --from=builder /main /main ENTRYPOINT ["/main"] diff --git a/devenv/docker/blocks/slow_proxy/docker-compose.yaml b/devenv/docker/blocks/slow_proxy/docker-compose.yaml index a74c6539644..c64fb5d33a7 100644 --- a/devenv/docker/blocks/slow_proxy/docker-compose.yaml +++ b/devenv/docker/blocks/slow_proxy/docker-compose.yaml @@ -1,8 +1,9 @@ slow_proxy: build: docker/blocks/slow_proxy - network_mode: host ports: - "3011:3011" + extra_hosts: + - "host.docker.internal:host-gateway" environment: ORIGIN_SERVER: ${ORIGIN_SERVER} SLEEP_DURATION: ${SLEEP_DURATION} \ No newline at end of file diff --git a/devenv/docker/blocks/slow_proxy/main.go b/devenv/docker/blocks/slow_proxy/main.go index d571da0ee98..4c98eb9d17c 100644 --- a/devenv/docker/blocks/slow_proxy/main.go +++ b/devenv/docker/blocks/slow_proxy/main.go @@ -12,12 +12,14 @@ import ( func main() { origin := os.Getenv("ORIGIN_SERVER") if origin == "" { - origin = "http://localhost:9090/" + // it is never not-set, the default is in the `.env` file + log.Fatalf("missing env-variable ORIGIN_SERVER") } sleepDurationStr := os.Getenv("SLEEP_DURATION") if sleepDurationStr == "" { - sleepDurationStr = "60s" + // it is never not-set, the default is in the `.env` file + log.Fatalf("missing env-variable SLEEP_DURATION") } sleep, err := time.ParseDuration(sleepDurationStr) diff --git a/devenv/docker/blocks/slow_proxy_mac/.env b/devenv/docker/blocks/slow_proxy_mac/.env deleted file mode 100644 index 74f4e9c1841..00000000000 --- a/devenv/docker/blocks/slow_proxy_mac/.env +++ /dev/null @@ -1,2 +0,0 @@ -ORIGIN_SERVER=http://host.docker.internal:9090/ -SLEEP_DURATION=60s \ No newline at end of file diff --git a/devenv/docker/blocks/slow_proxy_mac/Dockerfile b/devenv/docker/blocks/slow_proxy_mac/Dockerfile deleted file mode 100644 index 735d433414c..00000000000 --- a/devenv/docker/blocks/slow_proxy_mac/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM golang:latest as builder -ADD main.go / -WORKDIR / -RUN GO111MODULE=off CGO_ENABLED=0 go build -o main . - -FROM scratch -WORKDIR / -EXPOSE 3011 -COPY --from=builder /main /main -ENTRYPOINT ["/main"] diff --git a/devenv/docker/blocks/slow_proxy_mac/docker-compose.yaml b/devenv/docker/blocks/slow_proxy_mac/docker-compose.yaml deleted file mode 100644 index 135c230c9bc..00000000000 --- a/devenv/docker/blocks/slow_proxy_mac/docker-compose.yaml +++ /dev/null @@ -1,7 +0,0 @@ - slow_proxy_mac: - build: docker/blocks/slow_proxy_mac - ports: - - '3011:3011' - environment: - ORIGIN_SERVER: ${ORIGIN_SERVER} - SLEEP_DURATION: ${SLEEP_DURATION} \ No newline at end of file diff --git a/devenv/docker/blocks/slow_proxy_mac/main.go b/devenv/docker/blocks/slow_proxy_mac/main.go deleted file mode 100644 index 4c98eb9d17c..00000000000 --- a/devenv/docker/blocks/slow_proxy_mac/main.go +++ /dev/null @@ -1,40 +0,0 @@ -package main - -import ( - "log" - "net/http" - "net/http/httputil" - "net/url" - "os" - "time" -) - -func main() { - origin := os.Getenv("ORIGIN_SERVER") - if origin == "" { - // it is never not-set, the default is in the `.env` file - log.Fatalf("missing env-variable ORIGIN_SERVER") - } - - sleepDurationStr := os.Getenv("SLEEP_DURATION") - if sleepDurationStr == "" { - // it is never not-set, the default is in the `.env` file - log.Fatalf("missing env-variable SLEEP_DURATION") - } - - sleep, err := time.ParseDuration(sleepDurationStr) - if err != nil { - log.Fatalf("failed to parse SLEEP_DURATION: %v", err) - } - - originURL, _ := url.Parse(origin) - proxy := httputil.NewSingleHostReverseProxy(originURL) - - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - log.Printf("sleeping for %s then proxying request: url '%s', headers: '%v'", sleep.String(), r.RequestURI, r.Header) - <-time.After(sleep) - proxy.ServeHTTP(w, r) - }) - - log.Fatal(http.ListenAndServe(":3011", nil)) -} diff --git a/docs/sources/administration/image_rendering.md b/docs/sources/administration/image_rendering.md index f43f87df99d..cee132a6c7b 100755 --- a/docs/sources/administration/image_rendering.md +++ b/docs/sources/administration/image_rendering.md @@ -7,7 +7,7 @@ weight = 300 # Image rendering -Grafana supports automatic rendering of panels as PNG images. This allows Grafana to automatically generate images of your panels to include in [alert notifications]({{< relref "../alerting/notifications.md" >}}). +Grafana supports automatic rendering of panels as PNG images. This allows Grafana to automatically generate images of your panels to include in [alert notifications]({{< relref "../alerting/old-alerting/notifications.md" >}}). >**Note:** Image rendering of dashboards is not supported at this time. diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index ff8d8db8c74..279c92f7cab 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -152,7 +152,7 @@ Since not all datasources have the same configuration settings we only have the | timeInterval | string | Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL and MSSQL | Lowest interval/step value that should be used for this data source. | | httpMode | string | Influxdb | HTTP Method. 'GET', 'POST', defaults to GET | | maxSeries | number | Influxdb | Max number of series/tables that Grafana processes | -| httpMethod | string | Prometheus | HTTP Method. 'GET', 'POST', defaults to GET | +| httpMethod | string | Prometheus | HTTP Method. 'GET', 'POST', defaults to POST | | customQueryParameters | string | Prometheus | Query parameters to add, as a URL-encoded string. | | esVersion | string | Elasticsearch | Elasticsearch version (E.g. `7.0.0`, `7.6.1`) | | timeField | string | Elasticsearch | Which field that should be used as timestamp | diff --git a/docs/sources/administration/set-up-for-high-availability.md b/docs/sources/administration/set-up-for-high-availability.md index 7a4a2636aa6..2d40ef721d3 100644 --- a/docs/sources/administration/set-up-for-high-availability.md +++ b/docs/sources/administration/set-up-for-high-availability.md @@ -22,7 +22,7 @@ Grafana will now persist all long term data in the database. How to configure th ## Alerting -Currently alerting supports a limited form of high availability. [Alert notifications]({{< relref "../alerting/notifications.md" >}}) are deduplicated when running multiple servers. This means all alerts are executed on every server but alert notifications are only sent once per alert. Grafana does not support load distribution between servers. +Currently alerting supports a limited form of high availability. [Alert notifications]({{< relref "../alerting/old-alerting/notifications.md" >}}) are deduplicated when running multiple servers. This means all alerts are executed on every server but alert notifications are only sent once per alert. Grafana does not support load distribution between servers. ## User sessions diff --git a/docs/sources/alerting/_index.md b/docs/sources/alerting/_index.md index bd4e93282cf..db2417aec8b 100644 --- a/docs/sources/alerting/_index.md +++ b/docs/sources/alerting/_index.md @@ -8,21 +8,24 @@ weight = 110 Alerts allow you to know about problems in your systems moments after they occur. Robust and actionable alerts help you identify and resolve issues quickly, minimizing disruption to your services. +> **Note:** In Grafana 8.0, you can optionally choose the beta version of our new [unified altering]({{< relref "./unified-alerting/_index.md" >}}) system. Out of the box, Grafana still supports the [old alters]({{< relref "./old-alerting/_index.md" >}}) . + + Alerts have four main components: - Alert rule - One or more conditions, the frequency of evaluation, and the (optional) duration that a condition must be met before notifying. -- Contact point - A channel for sending notifications when the conditions of an alert rule are met. +- Contact point - A channel for sending notifications when the conditions of an alert rule are met. (Available in unified alerting only.) - Notification policy - A set of matching and grouping criteria used to determine where, and how frequently, to send notifications. -- Silences - Date and matching criteria used to silence notifications. +- Silences - Date and matching criteria used to silence notifications. (Available in unified alerting only.) ## Alert tasks You can perform the following tasks for alerts: -- [Create an alert rule]({{< relref "create-alerts.md" >}}) -- [View existing alert rules and their current state]({{< relref "view-alerts.md" >}}) -- [Test alert rules and troubleshoot]({{< relref "troubleshoot-alerts.md" >}}) -- [Add or edit an alert contact point]({{< relref "notifications.md" >}}) +- [Create an alert rule]({{< relref "./old-alerting/create-alerts.md" >}}) +- [View existing alert rules and their current state]({{< relref "./old-alerting/view-alerts.md" >}}) +- [Test alert rules and troubleshoot]({{< relref "./old-alerting/troubleshoot-alerts.md" >}}) +- [Add or edit an alert contact point]({{< relref "./old-alerting/notifications.md" >}}) ## Clustering diff --git a/docs/sources/alerting/difference-old-new.md b/docs/sources/alerting/difference-old-new.md index c3d15851e0f..3d34ca24a3d 100644 --- a/docs/sources/alerting/difference-old-new.md +++ b/docs/sources/alerting/difference-old-new.md @@ -1,11 +1,11 @@ +++ -title = "Difference between new and old alerts" +title = " Difference between unified and old Grafana alerts" description = "Pause an existing alert rule" keywords = ["grafana", "alerting", "guide", "rules", "view"] -weight = 400 +weight = 112 +++ -# Difference between new and old alerts +# Difference between unified and old Grafana alerts The `ngalert` feature toggle enables the beta version of our new alerting system. diff --git a/docs/sources/alerting/old-alerting/_index.md b/docs/sources/alerting/old-alerting/_index.md new file mode 100644 index 00000000000..d7f127896ec --- /dev/null +++ b/docs/sources/alerting/old-alerting/_index.md @@ -0,0 +1,52 @@ ++++ +title = "Old Grafana Alerts" +aliases = ["/docs/grafana/latest/alerting/rules/", "/docs/grafana/latest/alerting/metrics/"] +weight = 114 ++++ + +# Old Grafana alerts + +Alerts allow you to know about problems in your systems moments after they occur. Robust and actionable alerts help you identify and resolve issues quickly, minimizing disruption to your services. + +Alerts have four main components: + +- Alert rule - One or more conditions, the frequency of evaluation, and the (optional) duration that a condition must be met before notifying. +- Contact point - A channel for sending notifications when the conditions of an alert rule are met. +- Notification policy - A set of matching and grouping criteria used to determine where, and how frequently, to send notifications. +- Silences - Date and matching criteria used to silence notifications. + +## Alert tasks + +You can perform the following tasks for alerts: + +- [Create an alert rule]({{< relref "create-alerts.md" >}}) +- [View existing alert rules and their current state]({{< relref "view-alerts.md" >}}) +- [Test alert rules and troubleshoot]({{< relref "troubleshoot-alerts.md" >}}) +- [Add or edit an alert contact point]({{< relref "notifications.md" >}}) + +## Clustering + +Currently alerting supports a limited form of high availability. Since v4.2.0 of Grafana, alert notifications are deduped when running multiple servers. This means all alerts are executed on every server but no duplicate alert notifications are sent due to the deduping logic. Proper load balancing of alerts will be introduced in the future. + +## Alert evaluation + +Grafana managed alerts are evaluated by the Grafana backend. Rule evaluations are scheduled, according to the alert rule configuration, and queries are evaluated by an engine that is part of core Grafana. + +Alert rules can only query backend data sources with alerting enabled: +- builtin or developed and maintained by grafana: `Graphite`, `Prometheus`, `Loki`, `InfluxDB`, `Elasticsearch`, +`Google Cloud Monitoring`, `Cloudwatch`, `Azure Monitor`, `MySQL`, `PostgreSQL`, `MSSQL`, `OpenTSDB`, `Oracle`, and `Azure Data Explorer` +- any community backend data sources with alerting enabled (`backend` and `alerting` properties are set in the [plugin.json]({{< relref "../../developers/plugins/metadata.md" >}})) + +## Metrics from the alert engine + +The alert engine publishes some internal metrics about itself. You can read more about how Grafana publishes [internal metrics]({{< relref "../../administration/view-server/internal-metrics.md" >}}). + +Metric Name | Type | Description +---------- | ----------- | ---------- +`alerting.alerts` | gauge | How many alerts by state +`alerting.request_duration_seconds` | histogram | Histogram of requests to the Alerting API +`alerting.active_configurations` | gauge | The number of active, non default alertmanager configurations for grafana managed alerts +`alerting.rule_evaluations_total` | counter | The total number of rule evaluations +`alerting.rule_evaluation_failures_total` | counter | The total number of rule evaluation failures +`alerting.rule_evaluation_duration_seconds` | summary | The duration for a rule to execute +`alerting.rule_group_rules` | gauge | The number of rules diff --git a/docs/sources/alerting/add-notification-template.md b/docs/sources/alerting/old-alerting/add-notification-template.md similarity index 100% rename from docs/sources/alerting/add-notification-template.md rename to docs/sources/alerting/old-alerting/add-notification-template.md diff --git a/docs/sources/alerting/create-alerts.md b/docs/sources/alerting/old-alerting/create-alerts.md similarity index 98% rename from docs/sources/alerting/create-alerts.md rename to docs/sources/alerting/old-alerting/create-alerts.md index d986b1390d3..0e68935c589 100644 --- a/docs/sources/alerting/create-alerts.md +++ b/docs/sources/alerting/old-alerting/create-alerts.md @@ -37,7 +37,7 @@ This section describes the fields you fill out to create an alert. - **Evaluate every -** Specify how often the scheduler should evaluate the alert rule. This is referred to as the _evaluation interval_. - **For -** Specify how long the query needs to violate the configured thresholds before the alert notification triggers. -You can set a minimum evaluation interval in the `alerting.min_interval_seconds` configuration field, to set a minimum time between evaluations. Refer to [Configuration]({{< relref "../administration/configuration.md" >}}#min-interval-seconds) for more information. +You can set a minimum evaluation interval in the `alerting.min_interval_seconds` configuration field, to set a minimum time between evaluations. Refer to [Configuration]({{< relref "../../administration/configuration.md" >}}#min-interval-seconds) for more information. > **Caution:** Do not use `For` with the `If no data or all values are null` setting set to `No Data`. The triggering of `No Data` will trigger instantly and not take `For` into consideration. This may also result in that an OK notification not being sent if alert transitions from `No Data -> Pending -> OK`. diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/old-alerting/notifications.md similarity index 94% rename from docs/sources/alerting/notifications.md rename to docs/sources/alerting/old-alerting/notifications.md index 5550bbce827..e89bf56b8a8 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/old-alerting/notifications.md @@ -74,7 +74,7 @@ VictorOps | `victorops` | yes, external only | yes ### Email -To enable email notifications you have to set up [SMTP settings]({{< relref "../administration/configuration/#smtp" >}}) +To enable email notifications you have to set up [SMTP settings]({{< relref "../../administration/configuration/#smtp" >}}) in the Grafana config. Email notifications will upload an image of the alert graph to an external image destination if available or fallback to attaching the image to the email. Be aware that if you use the `local` image storage email servers and clients might not be @@ -252,19 +252,19 @@ Alertmanager handles alerts sent by client applications such as Prometheus serve ## Enable images in notifications {#external-image-store} Grafana can render the panel associated with the alert rule as a PNG image and include that in the notification. Read more about the requirements and how to configure -[image rendering]({{< relref "../administration/image_rendering/" >}}). +[image rendering]({{< relref "../../administration/image_rendering/" >}}). -You must configure an [external image storage provider]({{< relref "../administration/configuration/#external-image-storage" >}}) in order to receive images in alert notifications. If your notification channel requires that the image be publicly accessible (e.g. Slack, PagerDuty), configure a provider which uploads the image to a remote image store like Amazon S3, Webdav, Google Cloud Storage, or Azure Blob Storage. Otherwise, the local provider can be used to serve the image directly from Grafana. +You must configure an [external image storage provider]({{< relref "../../administration/configuration/#external-image-storage" >}}) in order to receive images in alert notifications. If your notification channel requires that the image be publicly accessible (e.g. Slack, PagerDuty), configure a provider which uploads the image to a remote image store like Amazon S3, Webdav, Google Cloud Storage, or Azure Blob Storage. Otherwise, the local provider can be used to serve the image directly from Grafana. Notification services which need public image access are marked as 'external only'. ## Configure the link back to Grafana from alert notifications All alert notifications contain a link back to the triggered alert in the Grafana instance. -This URL is based on the [domain]({{< relref "../administration/configuration/#domain" >}}) setting in Grafana. +This URL is based on the [domain]({{< relref "../../administration/configuration/#domain" >}}) setting in Grafana. ## Notification templating > **Note:** Alert notification templating is only available in Grafana v7.4 and above. -The alert notification template feature allows you to take the [label]({{< relref "../basics/timeseries-dimensions.md#labels" >}}) value from an alert query and [inject that into alert notifications]({{< relref "./add-notification-template.md" >}}). +The alert notification template feature allows you to take the [label]({{< relref "../../basics/timeseries-dimensions.md#labels" >}}) value from an alert query and [inject that into alert notifications]({{< relref "./add-notification-template.md" >}}). diff --git a/docs/sources/alerting/pause-an-alert-rule.md b/docs/sources/alerting/old-alerting/pause-an-alert-rule.md similarity index 100% rename from docs/sources/alerting/pause-an-alert-rule.md rename to docs/sources/alerting/old-alerting/pause-an-alert-rule.md diff --git a/docs/sources/alerting/troubleshoot-alerts.md b/docs/sources/alerting/old-alerting/troubleshoot-alerts.md similarity index 100% rename from docs/sources/alerting/troubleshoot-alerts.md rename to docs/sources/alerting/old-alerting/troubleshoot-alerts.md diff --git a/docs/sources/alerting/view-alerts.md b/docs/sources/alerting/old-alerting/view-alerts.md similarity index 100% rename from docs/sources/alerting/view-alerts.md rename to docs/sources/alerting/old-alerting/view-alerts.md diff --git a/docs/sources/alerting/8.0/_index.md b/docs/sources/alerting/unified-alerting/_index.md similarity index 63% rename from docs/sources/alerting/8.0/_index.md rename to docs/sources/alerting/unified-alerting/_index.md index d517f424354..8283dbcd507 100644 --- a/docs/sources/alerting/8.0/_index.md +++ b/docs/sources/alerting/unified-alerting/_index.md @@ -1,8 +1,11 @@ +++ -title = "Alerts" +title = "Unified Alerts" aliases = ["/docs/grafana/latest/alerting/rules/", "/docs/grafana/latest/alerting/metrics/"] -weight = 110 +weight = 113 +++ -# Alerts v2.0 overview +# Unified alerts + + + diff --git a/docs/sources/basics/timeseries-dimensions.md b/docs/sources/basics/timeseries-dimensions.md index e8394990a02..5e345e5954f 100644 --- a/docs/sources/basics/timeseries-dimensions.md +++ b/docs/sources/basics/timeseries-dimensions.md @@ -76,7 +76,7 @@ In this case the labels that represent the dimensions will have two keys based o > **Note:** More than one dimension is currently only supported in the Logs queries within the Azure Monitor service as of version 7.1. -> **Note:** Multiple dimensions are not supported in a way that maps to multiple alerts in Grafana, but rather they are treated as multiple conditions to a single alert. See the documentation on [creating alerts with multiple series]({{< relref "../alerting/create-alerts.md#multiple-series" >}}). +> **Note:** Multiple dimensions are not supported in a way that maps to multiple alerts in Grafana, but rather they are treated as multiple conditions to a single alert. See the documentation on [creating alerts with multiple series]({{< relref "../alerting/old-alerting/create-alerts.md#multiple-series" >}}). ### Multiple values diff --git a/docs/sources/developers/plugins/backend/_index.md b/docs/sources/developers/plugins/backend/_index.md index 087859fee5c..392f0047371 100644 --- a/docs/sources/developers/plugins/backend/_index.md +++ b/docs/sources/developers/plugins/backend/_index.md @@ -12,7 +12,7 @@ However, one limitation with these plugins are that they execute on the client-s We use the term _backend plugin_ to denote that a plugin has a backend component. Still, normally a backend plugin requires frontend components as well. This is for example true for backend data source plugins which normally need configuration and query editor components implemented for the frontend. -Data source plugins can be extended with a backend component. In the future we plan to support additional types and possibly new kinds of plugins, such as [notifiers for Grafana Alerting]({{< relref "../../../alerting/notifications.md" >}}) and custom authentication to name a few. +Data source plugins can be extended with a backend component. In the future we plan to support additional types and possibly new kinds of plugins, such as [notifiers for Grafana Alerting]({{< relref "../../../alerting/old-alerting/notifications.md" >}}) and custom authentication to name a few. ## Use cases for implementing a backend plugin diff --git a/docs/sources/enterprise/access-control/provisioning.md b/docs/sources/enterprise/access-control/provisioning.md index eeaef9f2b63..27a1cd5e298 100644 --- a/docs/sources/enterprise/access-control/provisioning.md +++ b/docs/sources/enterprise/access-control/provisioning.md @@ -41,15 +41,15 @@ Note that it is only possibly to provision [organization local]({{< relref "./ro ## Manage default built-in role assignments -During the startup, Grafana creates [default built-in role assignments]({{< relref "./roles#default-built-in-role-assignments" >}}) with [predefined roles]({{< relref "./roles#predefined-roles" >}}). You can remove and add back later those assignments by using provisioning. +During the startup, Grafana creates [default built-in role assignments]({{< relref "./roles#default-built-in-role-assignments" >}}) with [fixed roles]({{< relref "./roles#fixed-roles" >}}). You can remove and add back later those assignments by using provisioning. ### Remove default assignment -To remove default built-in role assignment, you can use `removeDefaultAssignments` element in the configuration file. You would need to provide built-in role name and predefined role name. +To remove default built-in role assignment, you can use `removeDefaultAssignments` element in the configuration file. You would need to provide built-in role name and fixed role name. ### Add back default assignment -To add back default built-in role assignment, you can use `addDefaultAssignments` element in the configuration file. You would need to provide built-in role name and predefined role name. +To add back default built-in role assignment, you can use `addDefaultAssignments` element in the configuration file. You would need to provide built-in role name and fixed role name. ## Example of a role configuration file @@ -61,15 +61,15 @@ apiVersion: 1 removeDefaultAssignments: # , must be one of the Organization roles (`Viewer`, `Editor`, `Admin`) or `Grafana Admin` - builtInRole: "Grafana Admin" - # , must be one of the existing predefined roles - predefinedRole: "grafana:roles:permissions:admin" + # , must be one of the existing fixed roles + fixedRole: "fixed:permissions:admin" # list of default built-in role assignments that should be added back addDefaultAssignments: # , must be one of the Organization roles (`Viewer`, `Editor`, `Admin`) or `Grafana Admin` - builtInRole: "Admin" - # , must be one of the existing predefined roles - predefinedRole: "grafana:roles:reporting:admin:read" + # , must be one of the existing fixed roles + fixedRole: "fixed:reporting:admin:read" # list of roles that should be deleted deleteRoles: @@ -127,7 +127,7 @@ A basic set of validation rules are applied to the input `yaml` files. ### Roles - `name` must not be empty -- `name` must not have `grafana:roles:` prefix. +- `name` must not have `fixed:` prefix. ### Built-in role assignments diff --git a/docs/sources/enterprise/access-control/roles.md b/docs/sources/enterprise/access-control/roles.md index 31595e54ef9..3bf274a8dfe 100644 --- a/docs/sources/enterprise/access-control/roles.md +++ b/docs/sources/enterprise/access-control/roles.md @@ -1,7 +1,7 @@ +++ title = "Roles" description = "Understand roles in fine-grained access control" -keywords = ["grafana", "fine-grained-access-control", "roles", "predefined-roles", "built-in-role-assignments", "permissions", "enterprise"] +keywords = ["grafana", "fine-grained-access-control", "roles", "fixed-roles", "built-in-role-assignments", "permissions", "enterprise"] weight = 105 +++ @@ -10,7 +10,7 @@ weight = 105 A role represents set of permissions that allow you to perform specific actions on Grafana resources. Refer to [Permissions]({{< relref "./permissions.md" >}}) to understand how permissions work. There are two types of roles: -- [Predefined roles]({{< relref "./roles.md#predefined-roles" >}}), which provide granular access for specific resources within Grafana and are managed by the Grafana itself. +- [Fixed roles]({{< relref "./roles.md#fixed-roles" >}}), which provide granular access for specific resources within Grafana and are managed by the Grafana itself. - [Custom roles]({{< relref "./roles.md#custom-roles.md" >}}), which provide granular access based on the user specified set of permissions. You can use [Fine-grained access control API]({{< relref "../../http_api/access_control.md" >}}) to list available roles and permissions. @@ -19,27 +19,27 @@ You can use [Fine-grained access control API]({{< relref "../../http_api/access_ A role can be either _global_ or _organization local_. _Global_ roles are not mapped to any specific organization and can be reused across multiple organizations, whereas _organization local_ roles are only available for that specific organization. -## Predefined roles +## Fixed roles -Predefined roles provide convenience and guarantee of consistent behaviour by combining relevant [permissions]({{< relref "./permissions.md" >}}) together. Predefined roles are created and updated by the Grafana, during the startup. -There are few basic rules for predefined roles: +Fixed roles provide convenience and guarantee of consistent behaviour by combining relevant [permissions]({{< relref "./permissions.md" >}}) together. Fixed roles are created and updated by Grafana during startup. +There are few basic rules for fixed roles: -- All predefined roles are _global_ by default -- All predefined roles have a `grafana:roles:` prefix. -- You can’t change or delete a predefined role. +- All fixed roles are _global_. +- All fixed roles have a `fixed:` prefix. +- You can’t change or delete a fixed role. Role name | Permissions | Description --- | --- | --- -grafana:roles:permissions:admin:read | roles:read
roles:list
roles.builtin:list | Allows to list and get available roles and built-in role assignments. -grafana:roles:permissions:admin:edit | All permissions from `grafana:roles:permissions:admin:read` and
roles:write
roles:delete
roles.builtin:add
roles.builtin:remove | Allows every read action and in addition allows to create, change and delete custom roles and create or remove built-in role assignments. -grafana:roles:reporting:admin:read | reports:read
reports:send
reports.settings:read | Allows to read reports and report settings. -grafana:roles:reporting:admin:edit | All permissions from `grafana:roles:reporting:admin:read` and
reports.admin:write
reports:delete
reports.settings:write | Allows every read action for reports and in addition allows to administer reports. -grafana:roles:users:admin:read | users.authtoken:list
users.quotas:list
users:read
users.teams:read | Allows to list and get users and related information. -grafana:roles:users:admin:edit | All permissions from `grafana:roles:users:admin:read` and
users.password:update
users:write
users:create
users:delete
users:enable
users:disable
users.permissions:update
users:logout
users.authtoken:update
users.quotas:update | Allows every read action for users and in addition allows to administer users. -grafana:roles:users:org:read | org.users:read | Allows to get user organizations. -grafana:roles:users:org:edit | All permissions from `grafana:roles:users:org:read` and
org.users:add
org.users:remove
org.users.role:update | Allows every read action for user organizations and in addition allows to administer user organizations. -grafana:roles:ldap:admin:read | ldap.user:read
ldap.status:read | Allows to read LDAP information and status. -grafana:roles:ldap:admin:edit | All permissions from `grafana:roles:ldap:admin:read` and
ldap.user:sync | Allows every read action for LDAP and in addition allows to administer LDAP. +fixed:permissions:admin:read | roles:read
roles:list
roles.builtin:list | Allows to list and get available roles and built-in role assignments. +fixed:permissions:admin:edit | All permissions from `fixed:permissions:admin:read` and
roles:write
roles:delete
roles.builtin:add
roles.builtin:remove | Allows every read action and in addition allows to create, change and delete custom roles and create or remove built-in role assignments. +fixed:reporting:admin:read | reports:read
reports:send
reports.settings:read | Allows to read reports and report settings. +fixed:reporting:admin:edit | All permissions from `fixed:reporting:admin:read` and
reports.admin:write
reports:delete
reports.settings:write | Allows every read action for reports and in addition allows to administer reports. +fixed:users:admin:read | users.authtoken:list
users.quotas:list
users:read
users.teams:read | Allows to list and get users and related information. +fixed:users:admin:edit | All permissions from `fixed:users:admin:read` and
users.password:update
users:write
users:create
users:delete
users:enable
users:disable
users.permissions:update
users:logout
users.authtoken:update
users.quotas:update | Allows every read action for users and in addition allows to administer users. +fixed:users:org:read | org.users:read | Allows to get user organizations. +fixed:users:org:edit | All permissions from `fixed:users:org:read` and
org.users:add
org.users:remove
org.users.role:update | Allows every read action for user organizations and in addition allows to administer user organizations. +fixed:ldap:admin:read | ldap.user:read
ldap.status:read | Allows to read LDAP information and status. +fixed:ldap:admin:edit | All permissions from `fixed:ldap:admin:read` and
ldap.user:sync | Allows every read action for LDAP and in addition allows to administer LDAP. ## Custom roles @@ -52,7 +52,7 @@ To create, update or delete a custom role, you can use the [Fine-grained access A role's name is intended as a human friendly identifier for the role, helping administrators understand the purpose of a role. The name cannot be longer than 190 characters, and we recommend using ASCII characters. Role names must be unique within an organization. -Roles with names prefixed by `grafana:roles:` are predefined roles created by Grafana and cannot be created or modified by users. +Roles with names prefixed by `fixed:` are fixed roles created by Grafana and cannot be created or modified by users. ##### Role version @@ -79,7 +79,7 @@ Note that you won't be able to create, update or delete a custom role with permi ## Built-in role assignments -To control what your users can access or not, you can assign or unassign [Custom roles]({{< ref "#custom-roles" >}}) or [Predefined roles]({{< ref "#predefined-roles" >}}) to the existing [Organization roles]({{< relref "../../permissions/organization_roles.md" >}}) or to [Grafana Server Admin]({{< relref "../../permissions/_index.md#grafana-server-admin-role" >}}) role. +To control what your users can access or not, you can assign or unassign [Custom roles]({{< ref "#custom-roles" >}}) or [Fixed roles]({{< ref "#fixed-roles" >}}) to the existing [Organization roles]({{< relref "../../permissions/organization_roles.md" >}}) or to [Grafana Server Admin]({{< relref "../../permissions/_index.md#grafana-server-admin-role" >}}) role. These assignments are called built-in role assignments. During startup, Grafana will create default assignments for you. When you make any changes to the built-on role assignments, Grafana will take them into account and won’t overwrite during next start. @@ -97,5 +97,5 @@ You can only create _organization local_ assignments for _organization local_ ro Built-in role | Associated role | Description --- | --- | --- -Grafana Admin | grafana:roles:permissions:admin:edit
grafana:roles:permissions:admin:read
grafana:roles:reporting:admin:edit
grafana:roles:reporting:admin:read
grafana:roles:users:admin:edit
grafana:roles:users:admin:read
grafana:roles:users:org:edit
grafana:roles:users:org:read
grafana:roles:ldap:admin:edit
grafana:roles:ldap:admin:read | Allows access to resources which [Grafana Server Admin]({{< relref "../../permissions/_index.md#grafana-server-admin-role" >}}) has permissions by default. -Admin | grafana:roles:users:org:edit
grafana:roles:users:org:read
grafana:roles:reporting:admin:edit
grafana:roles:reporting:admin:read | Allows access to resource which [Admin]({{< relref "../../permissions/organization_roles.md" >}}) has permissions by default. +Grafana Admin | fixed:permissions:admin:edit
fixed:permissions:admin:read
fixed:reporting:admin:edit
fixed:reporting:admin:read
fixed:users:admin:edit
fixed:users:admin:read
fixed:users:org:edit
fixed:users:org:read
fixed:ldap:admin:edit
fixed:ldap:admin:read | Allows access to resources which [Grafana Server Admin]({{< relref "../../permissions/_index.md#grafana-server-admin-role" >}}) has permissions by default. +Admin | fixed:users:org:edit
fixed:users:org:read
fixed:reporting:admin:edit
fixed:reporting:admin:read | Allows access to resource which [Admin]({{< relref "../../permissions/organization_roles.md" >}}) has permissions by default. diff --git a/docs/sources/enterprise/access-control/usage-scenarios.md b/docs/sources/enterprise/access-control/usage-scenarios.md index 57a516cccf6..b375f6a75a5 100644 --- a/docs/sources/enterprise/access-control/usage-scenarios.md +++ b/docs/sources/enterprise/access-control/usage-scenarios.md @@ -14,7 +14,7 @@ Before you get started, make sure to [enable fine-grained access control]({{< re ## Check all built-in role assignments You can use the [Fine-grained access control HTTP API]({{< relref "../../http_api/access_control.md#get-all-built-in-role-assignments" >}}) to see all available built-in role assignments. -The response contains a mapping between one of the organization roles (`Viewer`, `Editor`, `Admin`) or `Grafana Admin` to the custom or predefined roles. +The response contains a mapping between one of the organization roles (`Viewer`, `Editor`, `Admin`) or `Grafana Admin` to the custom or fixed roles. Example request: ``` @@ -29,7 +29,7 @@ Example response: { "version": 2, "uid": "qQui_LCMk", - "name": "grafana:roles:users:org:edit", + "name": "fixed:users:org:edit", "description": "Allows every read action for user organizations and in addition allows to administer user organizations.", "global": true, "updated": "2021-05-17T20:49:18+02:00", @@ -38,7 +38,7 @@ Example response: { "version": 1, "uid": "Kz9m_YjGz", - "name": "grafana:roles:reporting:admin:edit", + "name": "fixed:reporting:admin:edit", "description": "Gives access to edit any report or the organization's general reporting settings.", "global": true, "updated": "2021-05-13T16:24:26+02:00", @@ -51,7 +51,7 @@ Example response: { "version": 2, "uid": "qQui_LCMk", - "name": "grafana:roles:users:org:edit", + "name": "fixed:users:org:edit", "description": "Allows every read action for user organizations and in addition allows to administer user organizations.", "global": true, "updated": "2021-05-17T20:49:18+02:00", @@ -60,7 +60,7 @@ Example response: { "version": 2, "uid": "ajum_YjGk", - "name": "grafana:roles:users:admin:read", + "name": "fixed:users:admin:read", "description": "Allows to list and get users and related information.", "global": true, "updated": "2021-05-17T20:49:17+02:00", @@ -69,7 +69,7 @@ Example response: { "version": 2, "uid": "K3um_LCMk", - "name": "grafana:roles:users:admin:edit", + "name": "fixed:users:admin:edit", "description": "Allows every read action for users and in addition allows to administer users.", "global": true, "updated": "2021-05-17T20:49:17+02:00", @@ -94,7 +94,7 @@ Example response: { "version": 2, "uid": "qQui_LCMk", - "name": "grafana:roles:users:org:edit", + "name": "fixed:users:org:edit", "description": "Allows every read action for user organizations and in addition allows to administer user organizations.", "global": true, "permissions": [ @@ -203,7 +203,7 @@ In order to create reports, you would need to have `reports.admin:write` permiss If you want your users who have `Viewer` organization role to create reports, you have two options: -1. First option is to create a built-in role assignment and map `grafana:roles:reporting:admin:edit` predefined role to the `Viewer` built-in role. Note that `grafana:roles:reporting:admin:edit` predefined role allows doing more than creating reports. Refer to [predefined roles]({{< relref "./roles.md#predefined-roles" >}}) for full list of permission assignments. +1. First option is to create a built-in role assignment and map `fixed:reporting:admin:edit` fixed role to the `Viewer` built-in role. Note that `fixed:reporting:admin:edit` fixed role allows doing more than creating reports. Refer to [fixed roles]({{< relref "./roles.md#fixed-roles" >}}) for full list of permission assignments. 1. Second option is to [create a custom role]({{< ref "#create-your-custom-role" >}}) with `reports.admin:write` permission, and create a built-in role assignment for `Viewer` organization role. ## Prevent Grafana Admin from creating and inviting users @@ -213,14 +213,14 @@ In order to create users, you would need to have `users:create` permission. By d If you want to prevent Grafana Admin from creating users, you can do the following: 1. [Check all built-in role assignments]({{< ref "#check-all-built-in-role-assignments" >}}) to see what built-in role assignments are available. -1. From built-in role assignments, find the role which gives `users:create` permission. Refer to [predefined roles]({{< relref "./roles.md#predefined-roles" >}}) for full list of permission assignments. +1. From built-in role assignments, find the role which gives `users:create` permission. Refer to [fixed roles]({{< relref "./roles.md#fixed-roles" >}}) for full list of permission assignments. 1. Remove the built-in role assignment by using an [Fine-grained access control HTTP API]({{< relref "../../http_api/access_control.md" >}}) or by using [Grafana provisioning]({{< relref "./provisioning" >}}). ## Allow Editors to create new custom roles By default, Grafana Server Admin is the only user who can create and manage custom roles. If you want your users to do the same, you have two options: -1. First option is to create a built-in role assignment and map `grafana:roles:permissions:admin:edit` and `grafana:roles:permissions:admin:read` predefined roles to the `Editor` built-in role. +1. First option is to create a built-in role assignment and map `fixed:permissions:admin:edit` and `fixed:permissions:admin:read` fixed roles to the `Editor` built-in role. 1. Second option is to [create a custom role]({{< ref "#create-your-custom-role" >}}) with `roles.builtin:add` and `roles:write` permissions, and create a built-in role assignment for `Editor` organization role. Note that in any scenario, your `Editor` would be able to create and manage roles only with the permissions they have, or with a subset of them. diff --git a/docs/sources/getting-started/_index.md b/docs/sources/getting-started/_index.md index 795bda62455..85e4e6fb381 100644 --- a/docs/sources/getting-started/_index.md +++ b/docs/sources/getting-started/_index.md @@ -24,7 +24,7 @@ Refer to [Explore]({{< relref "../explore/_index.md" >}}) for more information. ## Alerts -If you're using Grafana alerting, then you can have alerts sent through a number of different [alert notifiers]({{< relref "../alerting/notifications.md" >}}), including PagerDuty, SMS, email, VictorOps, OpsGenie, or Slack. +If you're using Grafana alerting, then you can have alerts sent through a number of different [alert notifiers]({{< relref "../alerting/old-alerting/notifications.md" >}}), including PagerDuty, SMS, email, VictorOps, OpsGenie, or Slack. Alert hooks allow you to create different notifiers with a bit of code if you prefer some other channels of communication. Visually define [alert rules]({{< relref "../alerting/_index.md" >}}) for your most important metrics. diff --git a/docs/sources/http_api/access_control.md b/docs/sources/http_api/access_control.md index 5a092382f71..acc819e512b 100644 --- a/docs/sources/http_api/access_control.md +++ b/docs/sources/http_api/access_control.md @@ -87,7 +87,7 @@ Content-Type: application/json; charset=UTF-8 { "version": 1, "uid": "Kz9m_YjGz", - "name": "grafana:roles:reporting:admin:edit", + "name": "fixed:reporting:admin:edit", "description": "Gives access to edit any report or the organization's general reporting settings.", "global": true, "updated": "2021-05-13T16:24:26+02:00", @@ -96,7 +96,7 @@ Content-Type: application/json; charset=UTF-8 { "version": 5, "uid": "vi9mlLjGz", - "name": "grafana:roles:permissions:admin:read", + "name": "fixed:permissions:admin:read", "description": "Gives access to read and list roles and permissions, as well as built-in role assignments.", "global": true, "updated": "2021-05-13T22:41:49+02:00", @@ -142,7 +142,7 @@ Content-Type: application/json; charset=UTF-8 { "version": 2, "uid": "jZrmlLCGk", - "name": "grafana:roles:permissions:admin:edit", + "name": "fixed:permissions:admin:edit", "description": "Gives access to create, update and delete roles, as well as manage built-in role assignments.", "global": true, "permissions": [ @@ -176,7 +176,7 @@ Code | Description `POST /api/access-control/roles` -Creates a new custom role and maps given permissions to that role. Note that roles with the same prefix as [Predefined Roles]({{< relref "../enterprise/access-control/roles.md" >}}) can't be created. +Creates a new custom role and maps given permissions to that role. Note that roles with the same prefix as [Fixed Roles]({{< relref "../enterprise/access-control/roles.md" >}}) can't be created. #### Required permissions @@ -438,7 +438,7 @@ Content-Type: application/json; charset=UTF-8 { "version": 1, "uid": "qQui_LCMk", - "name": "grafana:roles:users:org:edit", + "name": "fixed:users:org:edit", "description": "", "global": true, "updated": "2021-05-13T16:24:26+02:00", @@ -447,7 +447,7 @@ Content-Type: application/json; charset=UTF-8 { "version": 1, "uid": "PeXmlYjMk", - "name": "grafana:roles:users:org:read", + "name": "fixed:users:org:read", "description": "", "global": true, "updated": "2021-05-13T16:24:26+02:00", @@ -458,7 +458,7 @@ Content-Type: application/json; charset=UTF-8 { "version": 1, "uid": "qQui_LCMk", - "name": "grafana:roles:users:org:edit", + "name": "fixed:users:org:edit", "description": "", "global": true, "updated": "2021-05-13T16:24:26+02:00", diff --git a/docs/sources/http_api/alerting_notification_channels.md b/docs/sources/http_api/alerting_notification_channels.md index c3225f124a0..77edc08808c 100644 --- a/docs/sources/http_api/alerting_notification_channels.md +++ b/docs/sources/http_api/alerting_notification_channels.md @@ -176,7 +176,7 @@ Content-Type: application/json ## Create notification channel -You can find the full list of [supported notifiers]({{< relref "../alerting/notifications/#list-of-supported-notifiers" >}}) on the alert notifiers page. +You can find the full list of [supported notifiers]({{< relref "../alerting/old-alerting/notifications/#list-of-supported-notifiers" >}}) on the alert notifiers page. `POST /api/alert-notifications` diff --git a/docs/sources/http_api/dashboard.md b/docs/sources/http_api/dashboard.md index 61f1052c278..b64d61f7e19 100644 --- a/docs/sources/http_api/dashboard.md +++ b/docs/sources/http_api/dashboard.md @@ -44,6 +44,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk "refresh": "25s" }, "folderId": 0, + "folderUid": "l3KqBxCMz", "message": "Made changes to xyz", "overwrite": false } @@ -55,6 +56,7 @@ JSON Body schema: - **dashboard.id** – id = null to create a new dashboard. - **dashboard.uid** – Optional unique identifier when creating a dashboard. uid = null will generate a new uid. - **folderId** – The id of the folder to save the dashboard in. +- **folderUid** – The UID of the folder to save the dashboard in. Overrides the `folderId`. - **overwrite** – Set to true if you want to overwrite existing dashboard with newer version, same dashboard title in folder or same dashboard uid. - **message** - Set a commit message for the version history. - **refresh** - Set the dashboard refresh interval. If this is lower than [the minimum refresh interval]({{< relref "../administration/configuration.md#min_refresh_interval">}}), then Grafana will ignore it and will enforce the minimum refresh interval. @@ -268,7 +270,7 @@ In case of title already exists the `status` property will be `name-exists`. `GET /api/dashboards/uid/:uid` -Will return the dashboard given the dashboard unique identifier (uid). +Will return the dashboard given the dashboard unique identifier (uid). Information about the unique identifier of a folder containing the requested dashboard might be found in the metadata. **Example Request**: @@ -298,6 +300,8 @@ Content-Type: application/json "meta": { "isStarred": false, "url": "/d/cIBgcSjkk/production-overview", + "folderId": 2, + "folderUid": "l3KqBxCMz", "slug": "production-overview" //deprecated in Grafana v5.0 } } diff --git a/docs/sources/panels/panel-editor.md b/docs/sources/panels/panel-editor.md index a9c5164fe99..be050a75d80 100644 --- a/docs/sources/panels/panel-editor.md +++ b/docs/sources/panels/panel-editor.md @@ -59,7 +59,7 @@ The section contains tabs where you enter queries, transform your data, and crea - **Query tab -** Select your data source and enter queries here. For more information, refer to [Queries]({{< relref "queries.md" >}}). - **Transform tab -** Apply data transformations. For more information, refer to [Transformations]({{< relref "transformations/_index.md" >}}). -- **Alert tab -** Write alert rules. For more information, refer to [Create alerts]({{< relref "../alerting/create-alerts.md" >}}). +- **Alert tab -** Write alert rules. For more information, refer to [Create alerts]({{< relref "../alerting/old-alerting/create-alerts.md" >}}). ### Panel and field options (side pane) diff --git a/docs/sources/shared/alerts/first-step.md b/docs/sources/shared/alerts/first-step.md new file mode 100644 index 00000000000..5102f0eb0c6 --- /dev/null +++ b/docs/sources/shared/alerts/first-step.md @@ -0,0 +1,11 @@ +--- +title: TBD... +--- + +## TBD + +Use the instructions in [Getting started with Grafana]({{< relref "../../getting-started/getting-started.md" >}}) to: + +- Install Grafana. +- Log in to Grafana. +- Create your first dashboard. diff --git a/docs/sources/whatsnew/whats-new-in-v7-4.md b/docs/sources/whatsnew/whats-new-in-v7-4.md index b0dd1e4b3c7..ed76d7cc8bf 100644 --- a/docs/sources/whatsnew/whats-new-in-v7-4.md +++ b/docs/sources/whatsnew/whats-new-in-v7-4.md @@ -107,7 +107,7 @@ You can now provide detailed information to alert notification recipients by inj {{< figure src="/img/docs/alerting/alert-notification-template-7-4.png" max-width="700px" caption="Variable support in alert notifications" >}} -For more information, refer to the [alert notification docs]({{< relref "../alerting/notifications.md#notification-templating" >}}). +For more information, refer to the [alert notification docs]({{< relref "../alerting/old-alerting/notifications.md#notification-templating" >}}). ### Content security policy support diff --git a/docs/sources/whatsnew/whats-new-in-v8-0.md b/docs/sources/whatsnew/whats-new-in-v8-0.md index c4c611bbdd0..0d5ab479a84 100644 --- a/docs/sources/whatsnew/whats-new-in-v8-0.md +++ b/docs/sources/whatsnew/whats-new-in-v8-0.md @@ -24,7 +24,7 @@ Library panels allow users to build panels that can be used in multiple dashboar ### Real-time streaming -Data sources can now send real-time updates to dashboards over a websocket connection. This can be used with the [MQTT data source](https://github.com/grafana/mqtt-datasource). +Data sources can now send real-time updates to dashboards over a websocket connection. This can be used with the [MQTT data source](https://github.com/grafana/mqtt-datasource). In addition to data source integration, events can be sent to dashboards by posting metrics to the new live endpoint: `/api/live/push endpoint`. @@ -48,7 +48,7 @@ This release introduces a new histogram panel visualization. ### Time series visualization updates -The Time series is out of beta! We are removing the `Beta` tag and graduating the Time series visualization to a stable state. +The Time series is out of beta! We are removing the `Beta` tag and graduating the Time series visualization to a stable state. - **Time series** is now the default visualization option, replacing the **Graph (old)**. - The Time series panel now supports stacking. For more information, refer to [Graph stacked time series]({{< relref "../panels/visualizations/time-series/graph-time-series-stacking.md" >}}). @@ -60,7 +60,7 @@ The Time series is out of beta! We are removing the `Beta` tag and graduating t ### Pie chart visualization updates -The Pie chart is out of beta! We are removing the `Beta` tag and graduating the Pie chart visualization to a stable state. +The Pie chart is out of beta! We are removing the `Beta` tag and graduating the Pie chart visualization to a stable state. ### Panel editor updates @@ -115,7 +115,7 @@ Grafana 8.0 includes many performance enhancements. #### Initial startup and load performance -We reduced the Grafana initial download size massively, approximately 40%. This means that on slower or mobile connections, the initial login page or home dashboard will load much faster. +We reduced the Grafana initial download size massively, approximately 40%. This means that on slower or mobile connections, the initial login page or home dashboard will load much faster. All panels that have migrated from Flot to uPlot will also render two to three times faster because the library is much more efficient. Right now, this includes the Time series, Stat, Timeline, Histogram, and Barchart panel visualizations. @@ -206,9 +206,9 @@ You can now configure generic OAuth with empty scopes. This allows OAuth Identit ##### Added OAuth support for strict parsing of role_attribute_path -You can now configure generic OAuth with strict parsing of the `role_attribute_path`. By default, if th `role_attribute_path` property does not return a role, then the user is assigned the `Viewer` role. You can disable the role assignment by setting `role_attribute_strict = true`. It denies user access if no role or an invalid role is returned. +You can now configure generic OAuth with strict parsing of the `role_attribute_path`. By default, if the `role_attribute_path` property does not return a role, then the user is assigned the `Viewer` role. You can disable the role assignment by setting `role_attribute_strict = true`. It denies user access if no role or an invalid role is returned. -#### Singlestat panel deprecated +#### Singlestat panel deprecated Support for Singlestat panel has been discontinued. When you upgrade to version 8.0, all existing Singlestat panels automatically becomes Stat panels. Stat panel is available as plugin. @@ -243,3 +243,7 @@ Documentation was updated to reflect these changes. ### Elasticsearch: Use application/x-ndjson content type for multi-search requests For multi-search requests, we now use the correct application/x-ndjson content type instead of the incorrect application/json. Although this should be transparent to most of the users, if you are running Elasticsearch behind a proxy, then be sure that your proxy correctly handles requests with this content type. + +### Prometheus: Update default HTTP method to POST for existing data sources + +The default HTTP method for Prometheus data source is now POST, previously it was GET. The POST APIs are there since January 2018 (Prometheus 2.1.0) and they have fewer limitations than the GET APIs. Users with Prometheus instance with version < 2.1.0 that use the default HTTP method should update their HTTP method to GET. diff --git a/e2e/suite1/specs/exemplars.spec.ts b/e2e/suite1/specs/exemplars.spec.ts index c93b2da6b79..d11687ed800 100644 --- a/e2e/suite1/specs/exemplars.spec.ts +++ b/e2e/suite1/specs/exemplars.spec.ts @@ -65,8 +65,6 @@ describe('Exemplars', () => { e2e.components.DataSource.Prometheus.exemplarMarker().first().trigger('mouseover'); e2e().contains('Query with gdev-tempo').click(); - - e2e().get('[aria-label="Node: app"]').should('be.visible'); e2e.components.TraceViewer.spanBar().should('have.length', 11); }); }); diff --git a/e2e/suite1/specs/trace-view-scrolling.spec.ts b/e2e/suite1/specs/trace-view-scrolling.spec.ts index ecabfd5ed8b..f6daacea4a2 100644 --- a/e2e/suite1/specs/trace-view-scrolling.spec.ts +++ b/e2e/suite1/specs/trace-view-scrolling.spec.ts @@ -29,7 +29,7 @@ describe('Trace view', () => { e2e.pages.Explore.General.scrollBar().scrollTo('center'); - // After scrolling we should have 139 spans instead of the first 100 - e2e.components.TraceViewer.spanBar().should('have.length', 139); + // After scrolling we should load more spans + e2e.components.TraceViewer.spanBar().should('have.length', 140); }); }); diff --git a/go.mod b/go.mod index 5ade367b175..87957257248 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,7 @@ require ( github.com/gosimple/slug v1.9.0 github.com/grafana/grafana-aws-sdk v0.4.0 github.com/grafana/grafana-live-sdk v0.0.6 - github.com/grafana/grafana-plugin-sdk-go v0.100.0 + github.com/grafana/grafana-plugin-sdk-go v0.101.0 github.com/grafana/loki v1.6.2-0.20210520072447-15d417efe103 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/hashicorp/go-hclog v0.16.0 @@ -76,7 +76,7 @@ require ( github.com/prometheus/client_golang v1.10.0 github.com/prometheus/client_model v0.2.0 github.com/prometheus/common v0.24.0 - github.com/prometheus/prometheus v1.8.2-0.20210421143221-52df5ef7a3be + github.com/prometheus/prometheus v1.8.2-0.20210430082741-2a4b8e12bbf2 github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 github.com/robfig/cron/v3 v3.0.1 github.com/russellhaering/goxmldsig v1.1.0 @@ -91,7 +91,7 @@ require ( github.com/weaveworks/common v0.0.0-20210419092856-009d1eebd624 github.com/xorcare/pointer v1.1.0 github.com/yudai/gojsondiff v1.0.0 - go.opentelemetry.io/collector v0.25.0 + go.opentelemetry.io/collector v0.27.0 golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a golang.org/x/exp v0.0.0-20210220032938-85be41e4509f // indirect golang.org/x/net v0.0.0-20210521195947-fe42d452be8f @@ -116,3 +116,5 @@ require ( xorm.io/core v0.7.3 xorm.io/xorm v0.8.2 ) + +replace github.com/apache/thrift => github.com/apache/thrift v0.14.1 diff --git a/go.sum b/go.sum index 823e47bd3a1..cf7f6179b19 100644 --- a/go.sum +++ b/go.sum @@ -73,7 +73,6 @@ github.com/Azure/azure-sdk-for-go v44.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo github.com/Azure/azure-sdk-for-go v45.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v46.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v48.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v51.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v51.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v52.5.0+incompatible h1:/NLBWHCnIHtZyLPc1P7WIqi4Te4CC23kIQyK3Ep/7lA= github.com/Azure/azure-sdk-for-go v52.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= @@ -181,7 +180,7 @@ github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqR github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/sarama v1.22.2-0.20190604114437-cd910a683f9f/go.mod h1:XLH1GYJnLVE0XCr6KdJGVJRTwY30moWNJ4sERjXX6fs= github.com/Shopify/sarama v1.27.1/go.mod h1:g5s5osgELxgM+Md9Qni9rzo7Rbt+vvFQI4bt/Mc93II= -github.com/Shopify/sarama v1.28.0/go.mod h1:j/2xTrU39dlzBmsxF1eQ2/DdWrxyBCl6pzz7a81o/ZY= +github.com/Shopify/sarama v1.29.0/go.mod h1:2QpgD79wpdAESqNQMxNc0KYMkycd4slxGdV3TWSVqrU= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= @@ -211,8 +210,7 @@ github.com/apache/arrow/go/arrow v0.0.0-20191024131854-af6fa24be0db/go.mod h1:VT github.com/apache/arrow/go/arrow v0.0.0-20200629181129-68b1273cbbf7/go.mod h1:QNYViu/X0HXDHw7m3KXzWSVXIbfUvJqBFe6Gj8/pYA0= github.com/apache/arrow/go/arrow v0.0.0-20210223225224-5bea62493d91 h1:rbe942bXzd2vnds4y9fYQL8X4yFltXoZsKW7KtG+TFM= github.com/apache/arrow/go/arrow v0.0.0-20210223225224-5bea62493d91/go.mod h1:c9sxoIT3YgLxH4UhLOCKaBlEojuMhVYpk4Ntv3opUTQ= -github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.14.1/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/aristanetworks/glog v0.0.0-20191112221043-67e8567f59f3/go.mod h1:KASm+qXFKs/xjSoWn30NrWBBvdTTQq+UjkhjEJHfSFA= github.com/aristanetworks/goarista v0.0.0-20190325233358-a123909ec740/go.mod h1:D/tb0zPVXnP7fmsLZjtdUhSsumbK/ij54UXjjVgMGxQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -922,8 +920,8 @@ github.com/grafana/grafana-live-sdk v0.0.6 h1:P1QFn0ZradOJp3zVpfG0STZMP+pgZrW0e0 github.com/grafana/grafana-live-sdk v0.0.6/go.mod h1:f15hHmWyLdFjmuWLsjeKeZnq/HnNQ3QkoPcaEww45AY= github.com/grafana/grafana-plugin-sdk-go v0.79.0/go.mod h1:NvxLzGkVhnoBKwzkst6CFfpMFKwAdIUZ1q8ssuLeF60= github.com/grafana/grafana-plugin-sdk-go v0.91.0/go.mod h1:Ot3k7nY7P6DXmUsDgKvNB7oG1v7PRyTdmnYVoS554bU= -github.com/grafana/grafana-plugin-sdk-go v0.100.0 h1:BryvIFdx/HrsKMt2hkxN7cJ0WrCgKpgjdJW8y8TSol0= -github.com/grafana/grafana-plugin-sdk-go v0.100.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= +github.com/grafana/grafana-plugin-sdk-go v0.101.0 h1:QyXMkgwZXUX9EQjLv5S5uDcvYjwsntqFV/dCC49Fn+w= +github.com/grafana/grafana-plugin-sdk-go v0.101.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= github.com/grafana/loki v1.6.2-0.20210520072447-15d417efe103 h1:qCmofFVwQR9QnsinstVqI1NPLMVl33jNCnOCXEAVn6E= github.com/grafana/loki v1.6.2-0.20210520072447-15d417efe103/go.mod h1:GHIsn+EohCChsdu5YouNZewqLeV9L2FNw4DEJU3P9qE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= @@ -1170,8 +1168,8 @@ github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0 github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.11.7 h1:0hzRabrMN4tSTvMfnL3SCv1ZGeAP23ynzodBgaHeMeg= -github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.12.2 h1:2KCfW3I9M7nSc5wOqXAlW2v2U6v+w6cbjvbfp+OykW8= +github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= @@ -1491,7 +1489,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac/go.mod h1:hoLfEwdY11HjRfKFH6KqnPsfxlo3BP6bJehpDv8t6sQ= +github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= github.com/prometheus/alertmanager v0.18.0/go.mod h1:WcxHBl40VSPuOaqWae6l6HpnEOVRIycEJ7i9iYkadEE= github.com/prometheus/alertmanager v0.19.0/go.mod h1:Eyp94Yi/T+kdeb2qvq66E3RGuph5T/jm/RBVh4yz1xo= github.com/prometheus/alertmanager v0.20.0/go.mod h1:9g2i48FAyZW6BtbsnvHtMHQXl2aVtrORKwKVCQ+nbrg= @@ -1584,11 +1582,11 @@ github.com/prometheus/prometheus v1.8.2-0.20201029103703-63be30dceed9/go.mod h1: github.com/prometheus/prometheus v1.8.2-0.20201119142752-3ad25a6dc3d9/go.mod h1:1MDE/bXgu4gqd5w/otko6WQpXZX9vu8QX4KbitCmaPg= github.com/prometheus/prometheus v1.8.2-0.20201119181812-c8f810083d3f/go.mod h1:1MDE/bXgu4gqd5w/otko6WQpXZX9vu8QX4KbitCmaPg= github.com/prometheus/prometheus v1.8.2-0.20210215121130-6f488061dfb4/go.mod h1:NAYujktP0dmSSpeV155mtnwX2pndLpVVK/Ps68R01TA= -github.com/prometheus/prometheus v1.8.2-0.20210217141258-a6be548dbc17/go.mod h1:dv3B1syqmkrkmo665MPCU6L8PbTXIiUeg/OEQULLNxA= github.com/prometheus/prometheus v1.8.2-0.20210315220929-1cba1741828b/go.mod h1:MS/bpdil77lPbfQeKk6OqVQ9OLnpN3Rszd0hka0EOWE= github.com/prometheus/prometheus v1.8.2-0.20210324152458-c7a62b95cea0/go.mod h1:sf7j/iAbhZahjeC0s3wwMmp5dksrJ/Za1UKdR+j6Hmw= -github.com/prometheus/prometheus v1.8.2-0.20210421143221-52df5ef7a3be h1:Kt84gUEhCC04CEqQxML1W+wzpydx1t3rmaEq1H971ZM= github.com/prometheus/prometheus v1.8.2-0.20210421143221-52df5ef7a3be/go.mod h1:WbIKsp4vWCoPHis5qQfd0QimLOR7qe79roXN5O8U8bs= +github.com/prometheus/prometheus v1.8.2-0.20210430082741-2a4b8e12bbf2 h1:AHi2TGs09Mv4v688/bjcY2PfAcu9+p4aPvsgVQ4nYDk= +github.com/prometheus/prometheus v1.8.2-0.20210430082741-2a4b8e12bbf2/go.mod h1:5aBj+GpLB+V5MCnrKm5+JAqEJwzDiLugOmDhgt7sDec= github.com/prometheus/statsd_exporter v0.20.0/go.mod h1:YL3FWCG8JBBtaUSxAg4Gz2ZYu22bS84XM89ZQXXTWmQ= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rafaeljusto/redigomock v0.0.0-20190202135759-257e089e14a1/go.mod h1:JaY6n2sDr+z2WTsXkOmNRUfDy6FN0L6Nk7x06ndm4tY= @@ -1652,7 +1650,7 @@ github.com/sercand/kuberesolver v2.4.0+incompatible/go.mod h1:lWF3GL0xptCB/vCiJP github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shirou/gopsutil v2.20.9+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shirou/gopsutil v3.21.3+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/gopsutil v3.21.4+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20200105231215-408a2507e114/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -1757,8 +1755,8 @@ github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhV github.com/timberio/go-datemath v0.1.1-0.20200323150745-74ddef604fff h1:QCdUBuN+iKWAB9HqPTkBwyKPPUHDobJ2AuELSNZwd4o= github.com/timberio/go-datemath v0.1.1-0.20200323150745-74ddef604fff/go.mod h1:m7kjsbCuO4QKP3KLfnxiUZWiOiFXmxj30HeexjL3lc0= github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= -github.com/tklauser/go-sysconf v0.3.4/go.mod h1:Cl2c8ZRWfHD5IrfHo9VN+FX9kCFjIOyVklgXycLB6ek= -github.com/tklauser/numcpus v0.2.1/go.mod h1:9aU+wOc6WjUIZEwWMP62PL/41d65P+iks1gBkr4QyP8= +github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= +github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -1824,8 +1822,10 @@ github.com/xdg-go/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:FV1RpvYFmF8 github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= +github.com/xdg/scram v1.0.3/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= +github.com/xdg/stringprep v1.0.3/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= @@ -1899,8 +1899,8 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/collector v0.25.0 h1:CVrqPgr0Kr/Se1ihS6jam1/n4hndXk3GHHAOsruSDzw= -go.opentelemetry.io/collector v0.25.0/go.mod h1:hXpdip0pVo+lISHAzPtu13QIRHgqC1zZ/4EdgoEC0fc= +go.opentelemetry.io/collector v0.27.0 h1:/lklt/NGVx+1EAWjjsk2DwAt9A9rGhIKOzo6Ob9kZqA= +go.opentelemetry.io/collector v0.27.0/go.mod h1:J2oCzkvFAkgmgrvIdQNg5Dt3QAZ+ep7HNtHPay/7nvo= go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0= go.starlark.net v0.0.0-20200901195727-6e684ef5eeee/go.mod h1:f0znQkUKRrkk36XxWbGjMqQM8wGv/xHBVE2qc3B5oFU= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -1965,6 +1965,7 @@ golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -2092,6 +2093,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210324051636-2c4c8ecb7826/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210427231257-85d9c07bbe3a/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210521195947-fe42d452be8f h1:Si4U+UcgJzya9kpiEUJKQvjr512OLli+gL4poHrz93U= golang.org/x/net v0.0.0-20210521195947-fe42d452be8f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -2226,7 +2228,6 @@ golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2235,6 +2236,7 @@ golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210314195730-07df6a141424/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2476,7 +2478,6 @@ google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210302174412-5ede27ff9881/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210312152112-fc591d9ea70f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= diff --git a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts index d263b522c41..6fb8f204618 100644 --- a/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts +++ b/packages/grafana-data/src/transformations/transformers/joinDataFrames.ts @@ -217,7 +217,7 @@ export function outerJoinDataFrames(options: JoinOptions): DataFrame | undefined //-------------------------------------------------------------------------------- // Copied from uplot -export type AlignedData = [number[], ...Array>]; +export type AlignedData = [number[], ...Array>]; // nullModes const NULL_REMOVE = 0; // nulls are converted to undefined (e.g. for spanGaps: true) diff --git a/packages/grafana-data/src/utils/index.ts b/packages/grafana-data/src/utils/index.ts index 7ace16115e9..7ae5541dcce 100644 --- a/packages/grafana-data/src/utils/index.ts +++ b/packages/grafana-data/src/utils/index.ts @@ -20,3 +20,4 @@ export { DataLinkBuiltInVars, mapInternalLinkToExplore } from './dataLinks'; export { DocsId } from './docs'; export { makeClassES5Compatible } from './makeClassES5Compatible'; export { anyToNumber } from './anyToNumber'; +export { withLoadingIndicator, WithLoadingIndicatorOptions } from './withLoadingIndicator'; diff --git a/packages/grafana-data/src/utils/withLoadingIndicator.ts b/packages/grafana-data/src/utils/withLoadingIndicator.ts new file mode 100644 index 00000000000..d4a6280824b --- /dev/null +++ b/packages/grafana-data/src/utils/withLoadingIndicator.ts @@ -0,0 +1,17 @@ +import { merge, Observable, timer } from 'rxjs'; +import { mapTo, takeUntil } from 'rxjs/operators'; + +/** + * @internal + */ +export type WithLoadingIndicatorOptions = { + whileLoading: T; + source: Observable; +}; + +/** + * @internal + */ +export function withLoadingIndicator({ whileLoading, source }: WithLoadingIndicatorOptions): Observable { + return merge(timer(200).pipe(mapTo(whileLoading), takeUntil(source)), source); +} diff --git a/packages/grafana-runtime/src/components/DataSourcePicker.tsx b/packages/grafana-runtime/src/components/DataSourcePicker.tsx index 1146c35fe02..b0af3414b0d 100644 --- a/packages/grafana-runtime/src/components/DataSourcePicker.tsx +++ b/packages/grafana-runtime/src/components/DataSourcePicker.tsx @@ -2,10 +2,11 @@ import React, { PureComponent } from 'react'; // Components -import { HorizontalGroup, PluginSignatureBadge, Select } from '@grafana/ui'; +import { HorizontalGroup, PluginSignatureBadge, Select, stylesFactory } from '@grafana/ui'; import { DataSourceInstanceSettings, isUnsignedPluginSignature, SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { getDataSourceSrv } from '../services/dataSourceSrv'; +import { css, cx } from '@emotion/css'; /** * Component props description for the {@link DataSourcePicker} @@ -138,11 +139,12 @@ export class DataSourcePicker extends PureComponent dashboard.id === homeDashboardId)} @@ -157,7 +158,9 @@ export class SharedPreferences extends PureComponent {
- +
); diff --git a/public/app/core/components/TransformersUI/FilterByValueTransformer/FilterByValueFilterEditor.tsx b/public/app/core/components/TransformersUI/FilterByValueTransformer/FilterByValueFilterEditor.tsx index a3b7a9db993..efdc3118054 100644 --- a/public/app/core/components/TransformersUI/FilterByValueTransformer/FilterByValueFilterEditor.tsx +++ b/public/app/core/components/TransformersUI/FilterByValueTransformer/FilterByValueFilterEditor.tsx @@ -21,7 +21,7 @@ export const FilterByValueFilterEditor: React.FC = (props) => { const { fieldsAsOptions, fieldByDisplayName } = fieldsInfo; const fieldName = getFieldName(filter, fieldsAsOptions) ?? ''; const field = fieldByDisplayName[fieldName]; - const matcherOptions = getMatcherOptions(field); + const matcherOptions = field ? getMatcherOptions(field) : []; const matcherId = getSelectedMatcherId(filter, matcherOptions); const editor = valueMatchersUI.getIfExists(matcherId); diff --git a/public/app/core/utils/UserProvider.tsx b/public/app/core/utils/UserProvider.tsx deleted file mode 100644 index 59d5b58d338..00000000000 --- a/public/app/core/utils/UserProvider.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import React, { PureComponent } from 'react'; -import { getBackendSrv } from '@grafana/runtime'; -import { UserDTO, Team, UserOrg, UserSession } from 'app/types'; -import { config } from 'app/core/config'; -import { dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data'; - -export interface UserAPI { - changePassword: (changePassword: ChangePasswordFields) => void; - updateUserProfile: (profile: ProfileUpdateFields) => void; - loadUser: () => void; - loadTeams: () => void; - loadOrgs: () => void; - loadSessions: () => void; - setUserOrg: (org: UserOrg) => void; - revokeUserSession: (tokenId: number) => void; -} - -export interface LoadingStates { - changePassword: boolean; - loadUser: boolean; - loadTeams: boolean; - loadOrgs: boolean; - loadSessions: boolean; - updateUserProfile: boolean; - updateUserOrg: boolean; -} - -export interface ChangePasswordFields { - oldPassword: string; - newPassword: string; - confirmNew: string; -} - -export interface ProfileUpdateFields { - name: string; - email: string; - login: string; -} - -export interface Props { - userId?: number; // passed, will load user on mount - children: ( - api: UserAPI, - states: LoadingStates, - teams: Team[], - orgs: UserOrg[], - sessions: UserSession[], - user?: UserDTO - ) => JSX.Element; -} - -export interface State { - user?: UserDTO; - teams: Team[]; - orgs: UserOrg[]; - sessions: UserSession[]; - loadingStates: LoadingStates; -} - -export class UserProvider extends PureComponent { - state: State = { - teams: [] as Team[], - orgs: [] as UserOrg[], - sessions: [] as UserSession[], - loadingStates: { - changePassword: false, - loadUser: true, - loadTeams: false, - loadOrgs: false, - loadSessions: false, - updateUserProfile: false, - updateUserOrg: false, - }, - }; - - UNSAFE_componentWillMount() { - if (this.props.userId) { - this.loadUser(); - } - } - - changePassword = async (payload: ChangePasswordFields) => { - this.setState({ loadingStates: { ...this.state.loadingStates, changePassword: true } }); - await getBackendSrv().put('/api/user/password', payload); - this.setState({ loadingStates: { ...this.state.loadingStates, changePassword: false } }); - }; - - loadUser = async () => { - this.setState({ - loadingStates: { ...this.state.loadingStates, loadUser: true }, - }); - const user = await getBackendSrv().get('/api/user'); - this.setState({ user, loadingStates: { ...this.state.loadingStates, loadUser: Object.keys(user).length === 0 } }); - }; - - loadTeams = async () => { - this.setState({ - loadingStates: { ...this.state.loadingStates, loadTeams: true }, - }); - const teams = await getBackendSrv().get('/api/user/teams'); - this.setState({ teams, loadingStates: { ...this.state.loadingStates, loadTeams: false } }); - }; - - loadOrgs = async () => { - this.setState({ - loadingStates: { ...this.state.loadingStates, loadOrgs: true }, - }); - const orgs = await getBackendSrv().get('/api/user/orgs'); - this.setState({ orgs, loadingStates: { ...this.state.loadingStates, loadOrgs: false } }); - }; - - loadSessions = async () => { - this.setState({ - loadingStates: { ...this.state.loadingStates, loadSessions: true }, - }); - - await getBackendSrv() - .get('/api/user/auth-tokens') - .then((sessions: UserSession[]) => { - sessions = sessions - // Show active sessions first - .sort((a, b) => Number(b.isActive) - Number(a.isActive)) - .map((session: UserSession) => { - return { - id: session.id, - isActive: session.isActive, - seenAt: dateTimeFormatTimeAgo(session.seenAt), - createdAt: dateTimeFormat(session.createdAt, { format: 'MMMM DD, YYYY' }), - clientIp: session.clientIp, - browser: session.browser, - browserVersion: session.browserVersion, - os: session.os, - osVersion: session.osVersion, - device: session.device, - }; - }); - - this.setState({ sessions, loadingStates: { ...this.state.loadingStates, loadSessions: false } }); - }); - }; - - revokeUserSession = async (tokenId: number) => { - await getBackendSrv() - .post('/api/user/revoke-auth-token', { - authTokenId: tokenId, - }) - .then(() => { - const sessions = this.state.sessions.filter((session: UserSession) => { - return session.id !== tokenId; - }); - - this.setState({ sessions }); - }); - }; - - setUserOrg = async (org: UserOrg) => { - this.setState({ - loadingStates: { ...this.state.loadingStates, updateUserOrg: true }, - }); - await getBackendSrv() - .post('/api/user/using/' + org.orgId, {}) - .then(() => { - window.location.href = config.appSubUrl + '/profile'; - }) - .finally(() => { - this.setState({ loadingStates: { ...this.state.loadingStates, updateUserOrg: false } }); - }); - }; - - updateUserProfile = async (payload: ProfileUpdateFields) => { - this.setState({ loadingStates: { ...this.state.loadingStates, updateUserProfile: true } }); - await getBackendSrv() - .put('/api/user', payload) - .then(this.loadUser) - .catch((e) => console.error(e)) - .finally(() => { - this.setState({ loadingStates: { ...this.state.loadingStates, updateUserProfile: false } }); - }); - }; - - render() { - const { children } = this.props; - const { loadingStates, teams, orgs, sessions, user } = this.state; - - const api: UserAPI = { - changePassword: this.changePassword, - loadUser: this.loadUser, - loadTeams: this.loadTeams, - loadOrgs: this.loadOrgs, - loadSessions: this.loadSessions, - revokeUserSession: this.revokeUserSession, - updateUserProfile: this.updateUserProfile, - setUserOrg: this.setUserOrg, - }; - - return <>{children(api, loadingStates, teams, orgs, sessions, user)}; - } -} - -export default UserProvider; diff --git a/public/app/features/alerting/unified/api/preview.ts b/public/app/features/alerting/unified/api/preview.ts new file mode 100644 index 00000000000..2d51276fced --- /dev/null +++ b/public/app/features/alerting/unified/api/preview.ts @@ -0,0 +1,83 @@ +import { + dataFrameFromJSON, + DataFrameJSON, + getDefaultTimeRange, + LoadingState, + PanelData, + withLoadingIndicator, +} from '@grafana/data'; +import { getBackendSrv, toDataQueryError } from '@grafana/runtime'; +import { Observable, of } from 'rxjs'; +import { catchError, map, share } from 'rxjs/operators'; +import { + CloudPreviewRuleRequest, + GrafanaPreviewRuleRequest, + isCloudPreviewRequest, + isGrafanaPreviewRequest, + PreviewRuleRequest, + PreviewRuleResponse, +} from '../types/preview'; +import { RuleFormType } from '../types/rule-form'; + +export function previewAlertRule(request: PreviewRuleRequest): Observable { + if (isCloudPreviewRequest(request)) { + return previewCloudAlertRule(request); + } + + if (isGrafanaPreviewRequest(request)) { + return previewGrafanaAlertRule(request); + } + + throw new Error('unsupported preview rule request'); +} + +type GrafanaPreviewRuleResponse = { + instances: DataFrameJSON[]; +}; + +function previewGrafanaAlertRule(request: GrafanaPreviewRuleRequest): Observable { + const type = RuleFormType.grafana; + + return withLoadingIndicator({ + whileLoading: createResponse(type), + source: getBackendSrv() + .fetch({ + method: 'POST', + url: `/api/v1/rule/test/grafana`, + data: request, + }) + .pipe( + map(({ data }) => { + return createResponse(type, { + state: LoadingState.Done, + series: data.instances.map(dataFrameFromJSON), + }); + }), + catchError((error: Error) => { + return of( + createResponse(type, { + state: LoadingState.Error, + error: toDataQueryError(error), + }) + ); + }), + share() + ), + }); +} + +function createResponse(ruleType: RuleFormType, data: Partial = {}): PreviewRuleResponse { + return { + ruleType, + data: { + state: LoadingState.Loading, + series: [], + timeRange: getDefaultTimeRange(), + ...data, + }, + }; +} + +function previewCloudAlertRule(request: CloudPreviewRuleRequest): Observable { + throw new Error('preview for cloud alerting rules is not implemented'); +} diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx index d90e6564e65..ab652aae7eb 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx @@ -151,7 +151,7 @@ const getStyles = (theme: GrafanaTheme2) => { background: ${theme.colors.background.primary}; border: 1px solid ${theme.colors.border.weak}; border-radius: ${theme.shape.borderRadius()}; - margin: ${theme.spacing(2)}; + margin: ${theme.spacing(0, 2, 2)}; overflow: hidden; flex: 1; `, diff --git a/public/app/features/alerting/unified/components/rule-editor/ConditionsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/ConditionsStep.tsx index d82d5940c98..e109164d4bc 100644 --- a/public/app/features/alerting/unified/components/rule-editor/ConditionsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/ConditionsStep.tsx @@ -8,6 +8,7 @@ import { timeOptions, timeValidationPattern } from '../../utils/time'; import { ConditionField } from './ConditionField'; import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker'; import { RuleEditorSection } from './RuleEditorSection'; +import { PreviewRule } from './PreviewRule'; const MIN_TIME_RANGE_STEP_S = 10; // 10 seconds @@ -142,6 +143,7 @@ export const ConditionsStep: FC = () => { )} + ); }; diff --git a/public/app/features/alerting/unified/components/rule-editor/PreviewRule.tsx b/public/app/features/alerting/unified/components/rule-editor/PreviewRule.tsx new file mode 100644 index 00000000000..0cb1db7b1ec --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/PreviewRule.tsx @@ -0,0 +1,99 @@ +import React, { useCallback, useState } from 'react'; +import { css } from '@emotion/css'; +import { useFormContext } from 'react-hook-form'; +import { takeWhile } from 'rxjs/operators'; +import { useMountedState } from 'react-use'; +import { Button, HorizontalGroup, useStyles2 } from '@grafana/ui'; +import { dateTimeFormatISO, GrafanaTheme2, LoadingState } from '@grafana/data'; +import { RuleFormType } from '../../types/rule-form'; +import { PreviewRuleRequest, PreviewRuleResponse } from '../../types/preview'; +import { previewAlertRule } from '../../api/preview'; +import { PreviewRuleResult } from './PreviewRuleResult'; + +const fields: string[] = ['type', 'dataSourceName', 'condition', 'queries', 'expression']; + +export function PreviewRule(): React.ReactElement | null { + const styles = useStyles2(getStyles); + const [preview, onPreview] = usePreview(); + const { getValues } = useFormContext(); + const [type] = getValues(fields); + + if (type === RuleFormType.cloud) { + return null; + } + + return ( +
+ + + + +
+ ); +} + +function usePreview(): [PreviewRuleResponse | undefined, () => void] { + const [preview, setPreview] = useState(); + const { getValues } = useFormContext(); + const isMounted = useMountedState(); + + const onPreview = useCallback(() => { + const values = getValues(fields); + const request = createPreviewRequest(values); + + previewAlertRule(request) + .pipe(takeWhile((response) => !isCompleted(response), true)) + .subscribe((response) => { + if (!isMounted()) { + return; + } + setPreview(response); + }); + }, [getValues, isMounted]); + + return [preview, onPreview]; +} + +function createPreviewRequest(values: any[]): PreviewRuleRequest { + const [type, dataSourceName, condition, queries, expression] = values; + + switch (type) { + case RuleFormType.cloud: + return { + dataSourceName, + expr: expression, + }; + + case RuleFormType.grafana: + return { + grafana_condition: { + condition, + data: queries, + now: dateTimeFormatISO(Date.now()), + }, + }; + + default: + throw new Error(`Alert type ${type} not supported by preview.`); + } +} + +function isCompleted(response: PreviewRuleResponse): boolean { + switch (response.data.state) { + case LoadingState.Done: + case LoadingState.Error: + return true; + default: + return false; + } +} + +function getStyles(theme: GrafanaTheme2) { + return { + container: css` + margin-top: ${theme.spacing(2)}; + `, + }; +} diff --git a/public/app/features/alerting/unified/components/rule-editor/PreviewRuleResult.tsx b/public/app/features/alerting/unified/components/rule-editor/PreviewRuleResult.tsx new file mode 100644 index 00000000000..d50eb5414e8 --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/PreviewRuleResult.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { css } from '@emotion/css'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { useStyles2 } from '@grafana/ui'; +import { PanelRenderer } from '@grafana/runtime'; +import { GrafanaTheme2, LoadingState } from '@grafana/data'; +import { PreviewRuleResponse } from '../../types/preview'; +import { RuleFormType } from '../../types/rule-form'; + +type Props = { + preview: PreviewRuleResponse | undefined; +}; + +export function PreviewRuleResult(props: Props): React.ReactElement | null { + const { preview } = props; + const styles = useStyles2(getStyles); + + if (!preview) { + return null; + } + + const { data, ruleType } = preview; + + if (data.state === LoadingState.Loading) { + return ( +
+ Loading preview... +
+ ); + } + + if (data.state === LoadingState.Error) { + return
{data.error ?? 'Failed to preview alert rule'}
; + } + + return ( +
+ + Preview based on the result of running the query, for this moment.{' '} + {ruleType === RuleFormType.grafana ? 'Configuration for `no data` and `error handling` is not applied.' : null} + +
+ + {({ width, height }) => ( +
+ +
+ )} +
+
+
+ ); +} + +function getStyles(theme: GrafanaTheme2) { + return { + container: css` + margin: ${theme.spacing(2)} 0; + `, + table: css` + flex: 1 1 auto; + height: 135px; + margin-top: ${theme.spacing(2)}; + border: 1px solid ${theme.colors.border.medium}; + border-radius: ${theme.shape.borderRadius(1)}; + `, + }; +} diff --git a/public/app/features/alerting/unified/components/rule-editor/QueryEditor.tsx b/public/app/features/alerting/unified/components/rule-editor/QueryEditor.tsx index bd480ca8f74..dbbbbd686f7 100644 --- a/public/app/features/alerting/unified/components/rule-editor/QueryEditor.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/QueryEditor.tsx @@ -34,11 +34,13 @@ interface State { } export class QueryEditor extends PureComponent { private runner: AlertingQueryRunner; + private queries: GrafanaQuery[]; constructor(props: Props) { super(props); this.state = { panelDataByRefId: {} }; this.runner = new AlertingQueryRunner(); + this.queries = props.value ?? []; } componentDidMount() { @@ -52,29 +54,34 @@ export class QueryEditor extends PureComponent { } onRunQueries = () => { - const { value = [] } = this.props; - this.runner.run(value); + const { queries } = this; + this.runner.run(queries); }; onCancelQueries = () => { this.runner.cancel(); }; + onChangeQueries = (queries: GrafanaQuery[]) => { + this.queries = queries; + this.props.onChange(queries); + }; + onDuplicateQuery = (query: GrafanaQuery) => { - const { onChange, value = [] } = this.props; - onChange(addQuery(value, query)); + const { queries } = this; + this.onChangeQueries(addQuery(queries, query)); }; onNewAlertingQuery = () => { - const { onChange, value = [] } = this.props; + const { queries } = this; const defaultDataSource = getDatasourceSrv().getInstanceSettings('default'); if (!defaultDataSource) { return; } - onChange( - addQuery(value, { + this.onChangeQueries( + addQuery(queries, { datasourceUid: defaultDataSource.uid, model: { refId: '', @@ -85,10 +92,10 @@ export class QueryEditor extends PureComponent { }; onNewExpressionQuery = () => { - const { onChange, value = [] } = this.props; + const { queries } = this; - onChange( - addQuery(value, { + this.onChangeQueries( + addQuery(queries, { datasourceUid: ExpressionDatasourceUID, model: expressionDatasource.newQuery({ type: ExpressionQueryType.classic, @@ -166,7 +173,7 @@ export class QueryEditor extends PureComponent { diff --git a/public/app/features/alerting/unified/state/AlertingQueryRunner.ts b/public/app/features/alerting/unified/state/AlertingQueryRunner.ts index c69bffd41b0..8bf5daaccc2 100644 --- a/public/app/features/alerting/unified/state/AlertingQueryRunner.ts +++ b/public/app/features/alerting/unified/state/AlertingQueryRunner.ts @@ -1,5 +1,5 @@ -import { merge, Observable, of, OperatorFunction, ReplaySubject, timer, Unsubscribable } from 'rxjs'; -import { catchError, map, mapTo, share, takeUntil } from 'rxjs/operators'; +import { Observable, of, OperatorFunction, ReplaySubject, Unsubscribable } from 'rxjs'; +import { catchError, map, share } from 'rxjs/operators'; import { v4 as uuidv4 } from 'uuid'; import { dataFrameFromJSON, @@ -9,6 +9,7 @@ import { PanelData, rangeUtil, TimeRange, + withLoadingIndicator, } from '@grafana/data'; import { FetchResponse, toDataQueryError } from '@grafana/runtime'; import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv'; @@ -107,14 +108,15 @@ const runRequest = (backendSrv: BackendSrv, queries: GrafanaQuery[]): Observable requestId: uuidv4(), }; - const runningRequest = backendSrv.fetch(request).pipe( - mapToPanelData(initial), - catchError((error) => of(mapErrorToPanelData(initial, error))), - cancelNetworkRequestsOnUnsubscribe(backendSrv, request.requestId), - share() - ); - - return merge(timer(200).pipe(mapTo(initial), takeUntil(runningRequest)), runningRequest); + return withLoadingIndicator({ + whileLoading: initial, + source: backendSrv.fetch(request).pipe( + mapToPanelData(initial), + catchError((error) => of(mapErrorToPanelData(initial, error))), + cancelNetworkRequestsOnUnsubscribe(backendSrv, request.requestId), + share() + ), + }); }; const initialState = (queries: GrafanaQuery[], state: LoadingState): Record => { diff --git a/public/app/features/alerting/unified/types/preview.ts b/public/app/features/alerting/unified/types/preview.ts new file mode 100644 index 00000000000..c46fea30851 --- /dev/null +++ b/public/app/features/alerting/unified/types/preview.ts @@ -0,0 +1,31 @@ +import { PanelData } from '@grafana/data'; +import { GrafanaQuery } from 'app/types/unified-alerting-dto'; +import { RuleFormType } from './rule-form'; + +export type PreviewRuleRequest = GrafanaPreviewRuleRequest | CloudPreviewRuleRequest; + +export type GrafanaPreviewRuleRequest = { + grafana_condition: { + condition: string; + data: GrafanaQuery[]; + now: string; + }; +}; + +export type CloudPreviewRuleRequest = { + dataSourceName: string; + expr: string; +}; + +export type PreviewRuleResponse = { + ruleType: RuleFormType; + data: PanelData; +}; + +export function isCloudPreviewRequest(request: PreviewRuleRequest): request is CloudPreviewRuleRequest { + return 'expr' in request; +} + +export function isGrafanaPreviewRequest(request: PreviewRuleRequest): request is GrafanaPreviewRuleRequest { + return 'grafana_condition' in request; +} diff --git a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx index d5dc2a382dc..33693fa2bdc 100644 --- a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx @@ -185,7 +185,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => ({ display: flex; `, settingsWrapper: css` - margin: ${theme.spacing(2)}; + margin: ${theme.spacing(0, 2, 2)}; display: flex; flex-grow: 1; `, diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPane.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPane.tsx index c10c4ef6f01..e6840e97450 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPane.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPane.tsx @@ -67,7 +67,7 @@ const getStyles = (theme: GrafanaTheme) => { display: flex; flex: 1 1 0; flex-direction: column; - padding: ${theme.spacing.md} 0 0 0; + padding: 0; `, optionsWrapper: css` flex-grow: 1; diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx index 1787ff5db20..c6685d5e09a 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx @@ -526,7 +526,7 @@ export const getStyles = stylesFactory((theme: GrafanaTheme, props: Props) => { `, panelToolbar: css` display: flex; - padding: ${paneSpacing} 0 ${paneSpacing} ${paneSpacing}; + padding: 0 0 ${paneSpacing} ${paneSpacing}; justify-content: space-between; flex-wrap: wrap; `, diff --git a/public/app/features/dashboard/components/PanelEditor/VisualizationSelectPane.tsx b/public/app/features/dashboard/components/PanelEditor/VisualizationSelectPane.tsx index a5dd75d7e8f..db885fb3deb 100644 --- a/public/app/features/dashboard/components/PanelEditor/VisualizationSelectPane.tsx +++ b/public/app/features/dashboard/components/PanelEditor/VisualizationSelectPane.tsx @@ -162,7 +162,7 @@ const getStyles = (theme: GrafanaTheme) => { openWrapper: css` display: flex; flex-direction: column; - flex: 1 1 0; + flex: 1 1 100%; height: 100%; background: ${theme.colors.bg1}; border: 1px solid ${theme.colors.border1}; diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 81afc13554b..9c94f7fbf61 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -289,7 +289,8 @@ export class UnthemedDashboardPage extends PureComponent { render() { const { dashboard, isInitSlow, initError, isPanelEditorOpen, queryParams, theme } = this.props; const { editPanel, viewPanel, scrollTop, updateScrollTop } = this.state; - const styles = getStyles(theme); + const kioskMode = getKioskMode(queryParams.kiosk); + const styles = getStyles(theme, kioskMode); if (!dashboard) { if (isInitSlow) { @@ -302,7 +303,6 @@ export class UnthemedDashboardPage extends PureComponent { // Only trigger render when the scroll has moved by 25 const approximateScrollTop = Math.round(scrollTop / 25) * 25; const inspectPanel = this.getInspectPanel(); - const kioskMode = getKioskMode(queryParams.kiosk); return (
@@ -372,7 +372,8 @@ const mapDispatchToProps = { /* * Styles */ -export const getStyles = stylesFactory((theme: GrafanaTheme2) => { +export const getStyles = stylesFactory((theme: GrafanaTheme2, kioskMode) => { + const contentPadding = kioskMode !== KioskMode.Full ? theme.spacing(0, 2, 2) : theme.spacing(2); return { dashboardContainer: css` position: absolute; @@ -391,7 +392,7 @@ export const getStyles = stylesFactory((theme: GrafanaTheme2) => { display: flex; `, dashboardContent: css` - padding: ${theme.spacing(2)}; + padding: ${contentPadding}; flex-basis: 100%; flex-grow: 1; `, diff --git a/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap b/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap index 73324d76280..1d1555c2560 100644 --- a/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap +++ b/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap @@ -128,7 +128,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1` updateAfterMountMs={500} >
{ ); } diff --git a/public/app/features/explore/NodeGraphContainer.test.tsx b/public/app/features/explore/NodeGraphContainer.test.tsx new file mode 100644 index 00000000000..01d5960af01 --- /dev/null +++ b/public/app/features/explore/NodeGraphContainer.test.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { UnconnectedNodeGraphContainer } from './NodeGraphContainer'; +import { getDefaultTimeRange, MutableDataFrame } from '@grafana/data'; +import { ExploreId } from '../../types'; + +describe('NodeGraphContainer', () => { + it('is collapsed if shown with traces', () => { + const { container } = render( + {}) as any} + withTraceView={true} + /> + ); + + // Make sure we only show header in the collapsible + expect(container.firstChild?.childNodes.length).toBe(1); + }); + + it('shows the graph if not with trace view', () => { + const { container } = render( + {}) as any} + /> + ); + + expect(container.firstChild?.childNodes.length).toBe(2); + expect(container.querySelector('svg')).toBeInTheDocument(); + }); +}); + +const emptyFrame = new MutableDataFrame(); + +export const nodes = new MutableDataFrame({ + fields: toFields([ + ['id', ['3fa414edcef6ad90']], + ['title', ['tempo-querier']], + ['subTitle', ['HTTP GET - api_traces_traceid']], + ['mainStat', ['1049.14ms (100%)']], + ['secondaryStat', ['1047.29ms (99.82%)']], + ['color', [0.9982395121342127]], + ]), +}); + +export function toFields(fields: Array<[string, any[]]>) { + return fields.map(([name, values]) => { + return { name, values }; + }); +} diff --git a/public/app/features/explore/NodeGraphContainer.tsx b/public/app/features/explore/NodeGraphContainer.tsx index a15d27e8213..24f576c976b 100644 --- a/public/app/features/explore/NodeGraphContainer.tsx +++ b/public/app/features/explore/NodeGraphContainer.tsx @@ -1,11 +1,22 @@ -import React, { useState } from 'react'; -import { Badge, Collapse } from '@grafana/ui'; -import { DataFrame, TimeRange } from '@grafana/data'; +import React from 'react'; +import { useToggle } from 'react-use'; +import { Badge, Collapse, useStyles2 } from '@grafana/ui'; +import { DataFrame, GrafanaTheme2, TimeRange } from '@grafana/data'; +import { css } from '@emotion/css'; import { ExploreId, StoreState } from '../../types'; import { splitOpen } from './state/main'; import { connect, ConnectedProps } from 'react-redux'; import { useLinks } from './utils/links'; import { NodeGraph } from '../../plugins/panel/nodeGraph'; +import { useCategorizeFrames } from '../../plugins/panel/nodeGraph/useCategorizeFrames'; + +const getStyles = (theme: GrafanaTheme2) => ({ + warningText: css` + label: warningText; + font-size: ${theme.typography.bodySmall.fontSize}; + color: ${theme.colors.text.secondary}; + `, +}); interface Props { // Edges and Nodes are separate frames @@ -13,26 +24,35 @@ interface Props { exploreId: ExploreId; range: TimeRange; splitOpen: typeof splitOpen; - short?: boolean; + // When showing the node graph together with trace view we do some changes so it works better. + withTraceView?: boolean; } export function UnconnectedNodeGraphContainer(props: Props & ConnectedProps) { - const { dataFrames, range, splitOpen, short } = props; + const { dataFrames, range, splitOpen, withTraceView } = props; const getLinks = useLinks(range, splitOpen); + const styles = useStyles2(getStyles); - const [open, setOpen] = useState(true); + const { nodes } = useCategorizeFrames(dataFrames); + const [open, toggleOpen] = useToggle(false); + const countWarning = + withTraceView && nodes[0]?.length > 1000 ? ( + ({nodes[0].length} nodes, can be slow to load) + ) : null; return ( - Node graph + Node graph{countWarning}{' '} + } - collapsible={short} - isOpen={short ? open : true} - onToggle={short ? () => setOpen(!open) : undefined} + collapsible={withTraceView} + // We allow collapsing this only when it is shown together with trace view. + isOpen={withTraceView ? open : true} + onToggle={withTraceView ? () => toggleOpen() : undefined} > -
+
diff --git a/public/app/features/explore/Wrapper.test.tsx b/public/app/features/explore/Wrapper.test.tsx index 9e58f245200..ebf3ff2d5fd 100644 --- a/public/app/features/explore/Wrapper.test.tsx +++ b/public/app/features/explore/Wrapper.test.tsx @@ -25,6 +25,7 @@ import userEvent from '@testing-library/user-event'; import { splitOpen } from './state/main'; import { Route, Router } from 'react-router-dom'; import { GrafanaRoute } from 'app/core/navigation/GrafanaRoute'; +import { initialUserState } from '../profile/state/reducers'; type Mock = jest.Mock; @@ -252,6 +253,12 @@ describe('Wrapper', () => { await screen.findByText(`elastic Editor input: error`); await screen.findByText(`loki Editor input: { label="value"}`); }); + + it('changes the document title of the explore page', async () => { + setup({ datasources: [] }); + await waitFor(() => expect(document.querySelector('head')).toMatchSnapshot()); + await waitFor(() => expect(document.title).toEqual('Explore - Grafana')); + }); }); type DatasourceSetup = { settings: DataSourceInstanceSettings; api: DataSourceApi }; @@ -293,10 +300,21 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou const store = configureStore(); store.getState().user = { + ...initialUserState, orgId: 1, timeZone: 'utc', }; + store.getState().navIndex = { + explore: { + id: 'explore', + text: 'Explore', + subTitle: 'Explore your data', + icon: 'compass', + url: '/explore', + }, + }; + locationService.push({ pathname: '/explore' }); if (options?.query) { diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx index 946388a9af3..f76de76346b 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/Wrapper.tsx @@ -1,18 +1,43 @@ -import React, { Component } from 'react'; -import { connect } from 'react-redux'; +import React, { PureComponent } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; import { ExploreId, ExploreQueryParams } from 'app/types/explore'; import { ErrorBoundaryAlert } from '@grafana/ui'; import { lastSavedUrl, resetExploreAction, richHistoryUpdatedAction } from './state/main'; import { getRichHistory } from '../../core/utils/richHistory'; import { ExplorePaneContainer } from './ExplorePaneContainer'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; +import { NavModel } from '@grafana/data'; +import { Branding } from '../../core/components/Branding/Branding'; -interface WrapperProps extends GrafanaRouteComponentProps<{}, ExploreQueryParams> { - resetExploreAction: typeof resetExploreAction; - richHistoryUpdatedAction: typeof richHistoryUpdatedAction; -} +import { getNavModel } from '../../core/selectors/navModel'; +import { StoreState } from 'app/types'; + +interface RouteProps extends GrafanaRouteComponentProps<{}, ExploreQueryParams> {} +interface OwnProps {} + +const mapStateToProps = (state: StoreState) => { + return { + navModel: getNavModel(state.navIndex, 'explore'), + }; +}; + +const mapDispatchToProps = { + resetExploreAction, + richHistoryUpdatedAction, +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + +type Props = OwnProps & RouteProps & ConnectedProps; +class WrapperUnconnected extends PureComponent { + updatePageDocumentTitle(navModel: NavModel) { + if (navModel) { + document.title = `${navModel.main.text} - ${Branding.AppTitle}`; + } else { + document.title = Branding.AppTitle; + } + } -export class Wrapper extends Component { componentWillUnmount() { this.props.resetExploreAction({}); } @@ -23,6 +48,7 @@ export class Wrapper extends Component { const richHistory = getRichHistory(); this.props.richHistoryUpdatedAction({ richHistory }); + this.updatePageDocumentTitle(this.props.navModel); } render() { @@ -46,9 +72,6 @@ export class Wrapper extends Component { } } -const mapDispatchToProps = { - resetExploreAction, - richHistoryUpdatedAction, -}; +const Wrapper = connector(WrapperUnconnected); -export default connect(null, mapDispatchToProps)(Wrapper); +export default Wrapper; diff --git a/public/app/features/explore/__snapshots__/Wrapper.test.tsx.snap b/public/app/features/explore/__snapshots__/Wrapper.test.tsx.snap new file mode 100644 index 00000000000..ed0a561d3cf --- /dev/null +++ b/public/app/features/explore/__snapshots__/Wrapper.test.tsx.snap @@ -0,0 +1,3075 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Wrapper changes the document title of the explore page 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; diff --git a/public/app/features/playlist/PlaylistPage.tsx b/public/app/features/playlist/PlaylistPage.tsx index 142f08fc260..0280ebff4ea 100644 --- a/public/app/features/playlist/PlaylistPage.tsx +++ b/public/app/features/playlist/PlaylistPage.tsx @@ -32,7 +32,7 @@ export const PlaylistPage: FC = ({ navModel }) => { = ({ navModel }) => ( - - - {( - api: UserAPI, - states: LoadingStates, - teams: Team[], - orgs: UserOrg[], - sessions: UserSession[], - user?: UserDTO - ) => { - return ( - -

Change Your Password

- {states.loadUser ? ( - - ) : ( - - )} -
- ); - }} -
-
-); - function mapStateToProps(state: StoreState) { + const userState = state.user; + const { isUpdating, user } = userState; return { navModel: getNavModel(state.navIndex, `change-password`), + isUpdating, + user, }; } -const mapDispatchToProps = {}; +const mapDispatchToProps = { + loadUser, + changePassword, +}; -export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(ChangePasswordPage)); +const connector = connect(mapStateToProps, mapDispatchToProps); + +type Props = OwnProps & ConnectedProps; + +export function ChangePasswordPage({ navModel, loadUser, isUpdating, user, changePassword }: Props) { + useMount(() => loadUser()); + + return ( + + + {user ? ( + <> +

Change Your Password

+ + + ) : null} +
+
+ ); +} + +export default hot(module)(connector(ChangePasswordPage)); diff --git a/public/app/features/profile/UserOrganizations.tsx b/public/app/features/profile/UserOrganizations.tsx index 4ade31869a9..25f86b87325 100644 --- a/public/app/features/profile/UserOrganizations.tsx +++ b/public/app/features/profile/UserOrganizations.tsx @@ -1,20 +1,15 @@ import React, { PureComponent } from 'react'; import { UserDTO, UserOrg } from 'app/types'; -import { LoadingPlaceholder, Button } from '@grafana/ui'; +import { Button, LoadingPlaceholder } from '@grafana/ui'; export interface Props { - user: UserDTO; + user: UserDTO | null; orgs: UserOrg[]; isLoading: boolean; - loadOrgs: () => void; setUserOrg: (org: UserOrg) => void; } export class UserOrganizations extends PureComponent { - componentDidMount() { - this.props.loadOrgs(); - } - render() { const { isLoading, orgs, user } = this.props; @@ -30,7 +25,7 @@ export class UserOrganizations extends PureComponent {

Organizations

- +
@@ -45,7 +40,7 @@ export class UserOrganizations extends PureComponent {
Name{org.name} {org.role} - {org.orgId === user.orgId ? ( + {org.orgId === user?.orgId ? ( diff --git a/public/app/features/profile/UserProfileEdit.tsx b/public/app/features/profile/UserProfileEdit.tsx deleted file mode 100644 index 94fa8210dfa..00000000000 --- a/public/app/features/profile/UserProfileEdit.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { FC } from 'react'; -import { connect } from 'react-redux'; -import { hot } from 'react-hot-loader'; -import { LoadingPlaceholder, VerticalGroup } from '@grafana/ui'; -import { config } from '@grafana/runtime'; -import { NavModel } from '@grafana/data'; -import { UserProvider, UserAPI, LoadingStates } from 'app/core/utils/UserProvider'; -import { getNavModel } from 'app/core/selectors/navModel'; -import { UserDTO, Team, UserOrg, UserSession, StoreState } from 'app/types'; -import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences'; -import Page from 'app/core/components/Page/Page'; -import { UserTeams } from './UserTeams'; -import { UserSessions } from './UserSessions'; -import { UserOrganizations } from './UserOrganizations'; -import { UserProfileEditForm } from './UserProfileEditForm'; - -export interface Props { - navModel: NavModel; -} - -export const UserProfileEdit: FC = ({ navModel }) => ( - - - {( - api: UserAPI, - states: LoadingStates, - teams: Team[], - orgs: UserOrg[], - sessions: UserSession[], - user?: UserDTO - ) => { - return ( - - {states.loadUser ? ( - - ) : ( - - - - - - - - - )} - - ); - }} - - -); - -function mapStateToProps(state: StoreState) { - return { - navModel: getNavModel(state.navIndex, 'profile-settings'), - }; -} - -export default hot(module)(connect(mapStateToProps, null)(UserProfileEdit)); diff --git a/public/app/features/profile/UserProfileEditForm.tsx b/public/app/features/profile/UserProfileEditForm.tsx index 15a23b1624b..35bfd89d0f3 100644 --- a/public/app/features/profile/UserProfileEditForm.tsx +++ b/public/app/features/profile/UserProfileEditForm.tsx @@ -1,11 +1,11 @@ import React, { FC } from 'react'; -import { Button, Tooltip, Icon, Form, Input, Field, FieldSet } from '@grafana/ui'; +import { Button, Field, FieldSet, Form, Icon, Input, Tooltip } from '@grafana/ui'; import { UserDTO } from 'app/types'; import config from 'app/core/config'; -import { ProfileUpdateFields } from 'app/core/utils/UserProvider'; +import { ProfileUpdateFields } from './types'; export interface Props { - user: UserDTO; + user: UserDTO | null; isSavingUser: boolean; updateProfile: (payload: ProfileUpdateFields) => void; } @@ -25,24 +25,32 @@ export const UserProfileEditForm: FC = ({ user, isSavingUser, updateProfi } /> } /> - } /> + } + />
-
diff --git a/public/app/features/profile/UserProfileEditPage.test.tsx b/public/app/features/profile/UserProfileEditPage.test.tsx new file mode 100644 index 00000000000..085a2a67301 --- /dev/null +++ b/public/app/features/profile/UserProfileEditPage.test.tsx @@ -0,0 +1,279 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { within } from '@testing-library/dom'; +import { OrgRole } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; + +import { Props, UserProfileEditPage } from './UserProfileEditPage'; +import { initialUserState } from './state/reducers'; +import { getNavModel } from '../../core/selectors/navModel'; +import { backendSrv } from '../../core/services/backend_srv'; +import { TeamPermissionLevel } from '../../types'; + +const defaultProps: Props = { + ...initialUserState, + user: { + id: 1, + name: 'Test User', + email: 'test@test.com', + login: 'test', + isDisabled: false, + isGrafanaAdmin: false, + orgId: 0, + }, + teams: [ + { + id: 0, + name: 'Team One', + email: 'team.one@test.com', + avatarUrl: '/avatar/07d881f402480a2a511a9a15b5fa82c0', + memberCount: 2000, + permission: TeamPermissionLevel.Admin, + }, + ], + orgs: [ + { + name: 'Main', + orgId: 0, + role: OrgRole.Editor, + }, + { + name: 'Second', + orgId: 1, + role: OrgRole.Viewer, + }, + { + name: 'Third', + orgId: 2, + role: OrgRole.Admin, + }, + ], + sessions: [ + { + id: 0, + browser: 'Chrome', + browserVersion: '90', + clientIp: 'localhost', + createdAt: '2021-01-01 04:00:00', + device: 'Macbook Pro', + isActive: true, + os: 'Mac OS X', + osVersion: '11', + seenAt: new Date().toUTCString(), + }, + ], + navModel: getNavModel( + { + 'profile-settings': { + icon: 'sliders-v-alt', + id: 'profile-settings', + parentItem: { + id: 'profile', + text: 'Test User', + img: '/avatar/46d229b033af06a191ff2267bca9ae56', + url: '/profile', + }, + text: 'Preferences', + url: '/profile', + }, + }, + 'profile-settings' + ), + initUserProfilePage: jest.fn().mockResolvedValue(undefined), + revokeUserSession: jest.fn().mockResolvedValue(undefined), + changeUserOrg: jest.fn().mockResolvedValue(undefined), + updateUserProfile: jest.fn().mockResolvedValue(undefined), +}; + +function getSelectors() { + const dashboardSelect = () => screen.getByLabelText(/user preferences home dashboard drop down/i); + const timepickerSelect = () => screen.getByLabelText(selectors.components.TimeZonePicker.container); + const teamsTable = () => screen.getByRole('table', { name: /user teams table/i }); + const orgsTable = () => screen.getByRole('table', { name: /user organizations table/i }); + const sessionsTable = () => screen.getByRole('table', { name: /user sessions table/i }); + return { + name: () => screen.getByRole('textbox', { name: /^name$/i }), + email: () => screen.getByRole('textbox', { name: /email/i }), + username: () => screen.getByRole('textbox', { name: /username/i }), + saveProfile: () => screen.getByRole('button', { name: /edit user profile save button/i }), + dashboardSelect, + dashboardValue: () => within(dashboardSelect()).getByText(/default/i), + timepickerSelect, + timepickerValue: () => within(timepickerSelect()).getByText(/coordinated universal time/i), + savePreferences: () => screen.getByRole('button', { name: /user preferences save button/i }), + teamsTable, + teamsRow: () => within(teamsTable()).getByRole('row', { name: /team one team.one@test\.com 2000/i }), + orgsTable, + orgsEditorRow: () => within(orgsTable()).getByRole('row', { name: /main editor current/i }), + orgsViewerRow: () => within(orgsTable()).getByRole('row', { name: /second viewer select/i }), + orgsAdminRow: () => within(orgsTable()).getByRole('row', { name: /third admin select/i }), + sessionsTable, + sessionsRow: () => + within(sessionsTable()).getByRole('row', { + name: /now 2021-01-01 04:00:00 localhost chrome on mac os x 11/i, + }), + }; +} + +async function getTestContext(overrides: Partial = {}) { + jest.clearAllMocks(); + const putSpy = jest.spyOn(backendSrv, 'put'); + const getSpy = jest + .spyOn(backendSrv, 'get') + .mockResolvedValue({ timezone: 'UTC', homeDashboardId: 0, theme: 'dark' }); + const searchSpy = jest.spyOn(backendSrv, 'search').mockResolvedValue([]); + + const props = { ...defaultProps, ...overrides }; + const { rerender } = render(); + + await waitFor(() => expect(props.initUserProfilePage).toHaveBeenCalledTimes(1)); + + return { rerender, putSpy, getSpy, searchSpy, props }; +} + +describe('UserProfileEditPage', () => { + describe('when loading user', () => { + it('should show loading placeholder', async () => { + await getTestContext({ user: null }); + + expect(screen.getByText(/loading \.\.\./i)).toBeInTheDocument(); + }); + }); + + describe('when user has loaded', () => { + it('should show edit profile form', async () => { + await getTestContext(); + + const { name, email, username, saveProfile } = getSelectors(); + expect(screen.getByText(/edit profile/i)).toBeInTheDocument(); + expect(name()).toBeInTheDocument(); + expect(name()).toHaveValue('Test User'); + expect(email()).toBeInTheDocument(); + expect(email()).toHaveValue('test@test.com'); + expect(username()).toBeInTheDocument(); + expect(username()).toHaveValue('test'); + expect(saveProfile()).toBeInTheDocument(); + }); + + it('should show shared preferences', async () => { + await getTestContext(); + + const { dashboardSelect, dashboardValue, timepickerSelect, timepickerValue, savePreferences } = getSelectors(); + expect(screen.getByRole('group', { name: /preferences/i })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: /default/i })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: /dark/i })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: /light/i })).toBeInTheDocument(); + expect(dashboardSelect()).toBeInTheDocument(); + expect(dashboardValue()).toBeInTheDocument(); + expect(timepickerSelect()).toBeInTheDocument(); + expect(timepickerValue()).toBeInTheDocument(); + expect(savePreferences()).toBeInTheDocument(); + }); + + describe('and teams are loading', () => { + it('should show teams loading placeholder', async () => { + await getTestContext({ teamsAreLoading: true }); + + expect(screen.getByText(/loading teams\.\.\./i)).toBeInTheDocument(); + }); + }); + + describe('and teams are loaded', () => { + it('should show teams', async () => { + await getTestContext(); + + const { teamsTable, teamsRow } = getSelectors(); + expect(screen.getByRole('heading', { name: /teams/i })).toBeInTheDocument(); + expect(teamsTable()).toBeInTheDocument(); + expect(teamsRow()).toBeInTheDocument(); + }); + }); + + describe('and organizations are loading', () => { + it('should show teams loading placeholder', async () => { + await getTestContext({ orgsAreLoading: true }); + + expect(screen.getByText(/loading organizations\.\.\./i)).toBeInTheDocument(); + }); + }); + + describe('and organizations are loaded', () => { + it('should show organizations', async () => { + await getTestContext(); + + const { orgsTable, orgsEditorRow, orgsViewerRow, orgsAdminRow } = getSelectors(); + expect(screen.getByRole('heading', { name: /organizations/i })).toBeInTheDocument(); + expect(orgsTable()).toBeInTheDocument(); + expect(orgsEditorRow()).toBeInTheDocument(); + expect(orgsViewerRow()).toBeInTheDocument(); + expect(orgsAdminRow()).toBeInTheDocument(); + }); + }); + + describe('and sessions are loading', () => { + it('should show teams loading placeholder', async () => { + await getTestContext({ sessionsAreLoading: true }); + + expect(screen.getByText(/loading sessions\.\.\./i)).toBeInTheDocument(); + }); + }); + + describe('and sessions are loaded', () => { + it('should show sessions', async () => { + await getTestContext(); + + const { sessionsTable, sessionsRow } = getSelectors(); + expect(sessionsTable()).toBeInTheDocument(); + expect(sessionsRow()).toBeInTheDocument(); + }); + }); + + describe('and user is edited and saved', () => { + it('should call updateUserProfile', async () => { + const { props } = await getTestContext(); + + const { email, saveProfile } = getSelectors(); + userEvent.clear(email()); + await userEvent.type(email(), 'test@test.se'); + userEvent.click(saveProfile()); + + await waitFor(() => expect(props.updateUserProfile).toHaveBeenCalledTimes(1)); + expect(props.updateUserProfile).toHaveBeenCalledWith({ + email: 'test@test.se', + login: 'test', + name: 'Test User', + }); + }); + }); + + describe('and organization is changed', () => { + it('should call changeUserOrg', async () => { + const { props } = await getTestContext(); + const orgsAdminSelectButton = () => + within(getSelectors().orgsAdminRow()).getByRole('button', { name: /select/i }); + + userEvent.click(orgsAdminSelectButton()); + + await waitFor(() => expect(props.changeUserOrg).toHaveBeenCalledTimes(1)); + expect(props.changeUserOrg).toHaveBeenCalledWith({ + name: 'Third', + orgId: 2, + role: 'Admin', + }); + }); + }); + + describe('and session is revoked', () => { + it('should call revokeUserSession', async () => { + const { props } = await getTestContext(); + const sessionsRevokeButton = () => within(getSelectors().sessionsRow()).getByRole('button'); + + userEvent.click(sessionsRevokeButton()); + + await waitFor(() => expect(props.revokeUserSession).toHaveBeenCalledTimes(1)); + expect(props.revokeUserSession).toHaveBeenCalledWith(0); + }); + }); + }); +}); diff --git a/public/app/features/profile/UserProfileEditPage.tsx b/public/app/features/profile/UserProfileEditPage.tsx new file mode 100644 index 00000000000..8ffdf34a7b7 --- /dev/null +++ b/public/app/features/profile/UserProfileEditPage.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { useMount } from 'react-use'; +import { hot } from 'react-hot-loader'; +import { NavModel } from '@grafana/data'; +import { VerticalGroup } from '@grafana/ui'; + +import { getNavModel } from 'app/core/selectors/navModel'; +import { StoreState } from 'app/types'; +import Page from 'app/core/components/Page/Page'; +import { changeUserOrg, initUserProfilePage, revokeUserSession, updateUserProfile } from './state/actions'; +import UserProfileEditForm from './UserProfileEditForm'; +import SharedPreferences from 'app/core/components/SharedPreferences/SharedPreferences'; +import { UserTeams } from './UserTeams'; +import UserOrganizations from './UserOrganizations'; +import UserSessions from './UserSessions'; + +export interface OwnProps { + navModel: NavModel; +} + +function mapStateToProps(state: StoreState) { + const userState = state.user; + const { user, teams, orgs, sessions, teamsAreLoading, orgsAreLoading, sessionsAreLoading, isUpdating } = userState; + return { + navModel: getNavModel(state.navIndex, 'profile-settings'), + orgsAreLoading, + sessionsAreLoading, + teamsAreLoading, + orgs, + sessions, + teams, + isUpdating, + user, + }; +} + +const mapDispatchToProps = { + initUserProfilePage, + revokeUserSession, + changeUserOrg, + updateUserProfile, +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + +export type Props = OwnProps & ConnectedProps; + +export function UserProfileEditPage({ + navModel, + orgsAreLoading, + sessionsAreLoading, + teamsAreLoading, + initUserProfilePage, + orgs, + sessions, + teams, + isUpdating, + user, + revokeUserSession, + changeUserOrg, + updateUserProfile, +}: Props) { + useMount(() => initUserProfilePage()); + + return ( + + + + + + + + + + + + ); +} + +export default hot(module)(connector(UserProfileEditPage)); diff --git a/public/app/features/profile/UserSessions.tsx b/public/app/features/profile/UserSessions.tsx index 545a0dd5b46..ac5f55a6135 100644 --- a/public/app/features/profile/UserSessions.tsx +++ b/public/app/features/profile/UserSessions.tsx @@ -1,20 +1,14 @@ import React, { PureComponent } from 'react'; -import { UserDTO, UserSession } from 'app/types'; -import { LoadingPlaceholder, Button, Icon } from '@grafana/ui'; +import { UserSession } from 'app/types'; +import { Button, Icon, LoadingPlaceholder } from '@grafana/ui'; export interface Props { - user: UserDTO; sessions: UserSession[]; isLoading: boolean; - loadSessions: () => void; revokeUserSession: (tokenId: number) => void; } export class UserSessions extends PureComponent { - componentDidMount() { - this.props.loadSessions(); - } - render() { const { isLoading, sessions, revokeUserSession } = this.props; @@ -28,7 +22,7 @@ export class UserSessions extends PureComponent { <>

Sessions

- +
diff --git a/public/app/features/profile/UserTeams.tsx b/public/app/features/profile/UserTeams.tsx index c9e2ffc4a88..61c701ae259 100644 --- a/public/app/features/profile/UserTeams.tsx +++ b/public/app/features/profile/UserTeams.tsx @@ -5,14 +5,9 @@ import { LoadingPlaceholder } from '@grafana/ui'; export interface Props { teams: Team[]; isLoading: boolean; - loadTeams: () => void; } export class UserTeams extends PureComponent { - componentDidMount() { - this.props.loadTeams(); - } - render() { const { isLoading, teams } = this.props; @@ -28,7 +23,7 @@ export class UserTeams extends PureComponent {

Teams

-
Last seen
+
diff --git a/public/app/features/profile/api.ts b/public/app/features/profile/api.ts new file mode 100644 index 00000000000..7adc848a5ec --- /dev/null +++ b/public/app/features/profile/api.ts @@ -0,0 +1,57 @@ +import { getBackendSrv } from '@grafana/runtime'; + +import { ChangePasswordFields, ProfileUpdateFields } from './types'; +import { Team, UserDTO, UserOrg, UserSession } from '../../types'; + +async function changePassword(payload: ChangePasswordFields): Promise { + try { + await getBackendSrv().put('/api/user/password', payload); + } catch (err) { + console.error(err); + } +} + +function loadUser(): Promise { + return getBackendSrv().get('/api/user'); +} + +function loadTeams(): Promise { + return getBackendSrv().get('/api/user/teams'); +} + +function loadOrgs(): Promise { + return getBackendSrv().get('/api/user/orgs'); +} + +function loadSessions(): Promise { + return getBackendSrv().get('/api/user/auth-tokens'); +} + +async function revokeUserSession(tokenId: number): Promise { + await getBackendSrv().post('/api/user/revoke-auth-token', { + authTokenId: tokenId, + }); +} + +async function setUserOrg(org: UserOrg): Promise { + await getBackendSrv().post('/api/user/using/' + org.orgId, {}); +} + +async function updateUserProfile(payload: ProfileUpdateFields): Promise { + try { + await getBackendSrv().put('/api/user', payload); + } catch (err) { + console.error(err); + } +} + +export const api = { + changePassword, + revokeUserSession, + loadUser, + loadSessions, + loadOrgs, + loadTeams, + setUserOrg, + updateUserProfile, +}; diff --git a/public/app/features/profile/state/actions.ts b/public/app/features/profile/state/actions.ts new file mode 100644 index 00000000000..2155b1088b2 --- /dev/null +++ b/public/app/features/profile/state/actions.ts @@ -0,0 +1,89 @@ +import { config } from '@grafana/runtime'; + +import { ChangePasswordFields, ProfileUpdateFields } from '../types'; +import { ThunkResult, UserOrg } from '../../../types'; +import { + initLoadOrgs, + initLoadSessions, + initLoadTeams, + orgsLoaded, + sessionsLoaded, + setUpdating, + teamsLoaded, + userLoaded, + userSessionRevoked, +} from './reducers'; +import { api } from '../api'; + +export function changePassword(payload: ChangePasswordFields): ThunkResult { + return async function (dispatch) { + dispatch(setUpdating({ updating: true })); + await api.changePassword(payload); + dispatch(setUpdating({ updating: false })); + }; +} + +export function initUserProfilePage(): ThunkResult { + return async function (dispatch) { + await dispatch(loadUser()); + dispatch(loadTeams()); + dispatch(loadOrgs()); + dispatch(loadSessions()); + }; +} + +export function loadUser(): ThunkResult { + return async function (dispatch) { + const user = await api.loadUser(); + dispatch(userLoaded({ user })); + }; +} + +function loadTeams(): ThunkResult { + return async function (dispatch) { + dispatch(initLoadTeams()); + const teams = await api.loadTeams(); + dispatch(teamsLoaded({ teams })); + }; +} + +function loadOrgs(): ThunkResult { + return async function (dispatch) { + dispatch(initLoadOrgs()); + const orgs = await api.loadOrgs(); + dispatch(orgsLoaded({ orgs })); + }; +} + +function loadSessions(): ThunkResult { + return async function (dispatch) { + dispatch(initLoadSessions()); + const sessions = await api.loadSessions(); + dispatch(sessionsLoaded({ sessions })); + }; +} + +export function revokeUserSession(tokenId: number): ThunkResult { + return async function (dispatch) { + dispatch(setUpdating({ updating: true })); + await api.revokeUserSession(tokenId); + dispatch(userSessionRevoked({ tokenId })); + }; +} + +export function changeUserOrg(org: UserOrg): ThunkResult { + return async function (dispatch) { + dispatch(setUpdating({ updating: true })); + await api.setUserOrg(org); + window.location.href = config.appSubUrl + '/profile'; + }; +} + +export function updateUserProfile(payload: ProfileUpdateFields): ThunkResult { + return async function (dispatch) { + dispatch(setUpdating({ updating: true })); + await api.updateUserProfile(payload); + await dispatch(loadUser()); + dispatch(setUpdating({ updating: false })); + }; +} diff --git a/public/app/features/profile/state/reducers.test.ts b/public/app/features/profile/state/reducers.test.ts new file mode 100644 index 00000000000..2bccd7434a6 --- /dev/null +++ b/public/app/features/profile/state/reducers.test.ts @@ -0,0 +1,187 @@ +import { reducerTester } from '../../../../test/core/redux/reducerTester'; +import { OrgRole, TeamPermissionLevel } from '../../../types'; +import { + initialUserState, + orgsLoaded, + sessionsLoaded, + setUpdating, + teamsLoaded, + updateTimeZone, + userLoaded, + userReducer, + userSessionRevoked, + UserState, +} from './reducers'; + +describe('userReducer', () => { + describe('when updateTimeZone is dispatched', () => { + it('then state should be correct', () => { + reducerTester() + .givenReducer(userReducer, { ...initialUserState }) + .whenActionIsDispatched(updateTimeZone({ timeZone: 'xyz' })) + .thenStateShouldEqual({ ...initialUserState, timeZone: 'xyz' }); + }); + }); + + describe('when setUpdating is dispatched', () => { + it('then state should be correct', () => { + reducerTester() + .givenReducer(userReducer, { ...initialUserState, isUpdating: false }) + .whenActionIsDispatched(setUpdating({ updating: true })) + .thenStateShouldEqual({ ...initialUserState, isUpdating: true }); + }); + }); + + describe('when userLoaded is dispatched', () => { + it('then state should be correct', () => { + reducerTester() + .givenReducer(userReducer, { ...initialUserState, user: null }) + .whenActionIsDispatched( + userLoaded({ + user: { + id: 2021, + email: 'test@test.com', + isDisabled: true, + login: 'test', + name: 'Test Account', + isGrafanaAdmin: false, + }, + }) + ) + .thenStateShouldEqual({ + ...initialUserState, + user: { + id: 2021, + email: 'test@test.com', + isDisabled: true, + login: 'test', + name: 'Test Account', + isGrafanaAdmin: false, + }, + }); + }); + }); + + describe('when teamsLoaded is dispatched', () => { + it('then state should be correct', () => { + reducerTester() + .givenReducer(userReducer, { ...initialUserState, teamsAreLoading: true }) + .whenActionIsDispatched( + teamsLoaded({ + teams: [ + { + id: 1, + email: 'team@team.com', + name: 'Team', + avatarUrl: '/avatar/12345', + memberCount: 4, + permission: TeamPermissionLevel.Admin, + }, + ], + }) + ) + .thenStateShouldEqual({ + ...initialUserState, + teamsAreLoading: false, + teams: [ + { + id: 1, + email: 'team@team.com', + name: 'Team', + avatarUrl: '/avatar/12345', + memberCount: 4, + permission: TeamPermissionLevel.Admin, + }, + ], + }); + }); + }); + + describe('when orgsLoaded is dispatched', () => { + it('then state should be correct', () => { + reducerTester() + .givenReducer(userReducer, { ...initialUserState, orgsAreLoading: true }) + .whenActionIsDispatched( + orgsLoaded({ + orgs: [{ orgId: 1, name: 'Main', role: OrgRole.Viewer }], + }) + ) + .thenStateShouldEqual({ + ...initialUserState, + orgsAreLoading: false, + orgs: [{ orgId: 1, name: 'Main', role: OrgRole.Viewer }], + }); + }); + }); + + describe('when sessionsLoaded is dispatched', () => { + it('then state should be correct', () => { + reducerTester() + .givenReducer(userReducer, { ...initialUserState, sessionsAreLoading: true }) + .whenActionIsDispatched( + sessionsLoaded({ + sessions: [ + { + id: 1, + browser: 'Chrome', + browserVersion: '90', + osVersion: '95', + clientIp: '192.168.1.1', + createdAt: '2021-01-01 04:00:00', + device: 'Computer', + os: 'Windows', + isActive: false, + seenAt: '1996-01-01 04:00:00', + }, + ], + }) + ) + .thenStateShouldEqual({ + ...initialUserState, + sessionsAreLoading: false, + sessions: [ + { + id: 1, + browser: 'Chrome', + browserVersion: '90', + osVersion: '95', + clientIp: '192.168.1.1', + createdAt: 'December 31, 2020', + device: 'Computer', + os: 'Windows', + isActive: false, + seenAt: '25 years ago', + }, + ], + }); + }); + }); + + describe('when userSessionRevoked is dispatched', () => { + it('then state should be correct', () => { + reducerTester() + .givenReducer(userReducer, { + ...initialUserState, + sessions: [ + { + id: 1, + browser: 'Chrome', + browserVersion: '90', + osVersion: '95', + clientIp: '192.168.1.1', + createdAt: '2021-01-01', + device: 'Computer', + os: 'Windows', + isActive: false, + seenAt: '1996-01-01', + }, + ], + }) + .whenActionIsDispatched(userSessionRevoked({ tokenId: 1 })) + .thenStateShouldEqual({ + ...initialUserState, + sessions: [], + }); + }); + }); +}); diff --git a/public/app/features/profile/state/reducers.ts b/public/app/features/profile/state/reducers.ts index 60571e4f711..111fa1fc3ed 100644 --- a/public/app/features/profile/state/reducers.ts +++ b/public/app/features/profile/state/reducers.ts @@ -1,40 +1,115 @@ -import { isString, isEmpty, set } from 'lodash'; -import { PayloadAction, createSlice } from '@reduxjs/toolkit'; -import { UserState, ThunkResult } from 'app/types'; +import { isEmpty, isString, set } from 'lodash'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { dateTimeFormat, dateTimeFormatTimeAgo, TimeZone } from '@grafana/data'; + +import { Team, ThunkResult, UserDTO, UserOrg, UserSession } from 'app/types'; import config from 'app/core/config'; -import { TimeZone } from '@grafana/data'; import { contextSrv } from 'app/core/core'; -export const initialState: UserState = { +export interface UserState { + orgId: number; + timeZone: TimeZone; + user: UserDTO | null; + teams: Team[]; + orgs: UserOrg[]; + sessions: UserSession[]; + teamsAreLoading: boolean; + orgsAreLoading: boolean; + sessionsAreLoading: boolean; + isUpdating: boolean; +} + +export const initialUserState: UserState = { orgId: config.bootData.user.orgId, timeZone: config.bootData.user.timezone, + orgsAreLoading: false, + sessionsAreLoading: false, + teamsAreLoading: false, + isUpdating: false, + orgs: [], + sessions: [], + teams: [], + user: null, }; export const slice = createSlice({ name: 'user/profile', - initialState, + initialState: initialUserState, reducers: { - updateTimeZone: (state, action: PayloadAction): UserState => { - return { - ...state, - timeZone: action.payload, - }; + updateTimeZone: (state, action: PayloadAction<{ timeZone: TimeZone }>) => { + state.timeZone = action.payload.timeZone; + }, + setUpdating: (state, action: PayloadAction<{ updating: boolean }>) => { + state.isUpdating = action.payload.updating; + }, + userLoaded: (state, action: PayloadAction<{ user: UserDTO }>) => { + state.user = action.payload.user; + }, + initLoadTeams: (state, action: PayloadAction) => { + state.teamsAreLoading = true; + }, + teamsLoaded: (state, action: PayloadAction<{ teams: Team[] }>) => { + state.teams = action.payload.teams; + state.teamsAreLoading = false; + }, + initLoadOrgs: (state, action: PayloadAction) => { + state.orgsAreLoading = true; + }, + orgsLoaded: (state, action: PayloadAction<{ orgs: UserOrg[] }>) => { + state.orgs = action.payload.orgs; + state.orgsAreLoading = false; + }, + initLoadSessions: (state, action: PayloadAction) => { + state.sessionsAreLoading = true; + }, + sessionsLoaded: (state, action: PayloadAction<{ sessions: UserSession[] }>) => { + const sorted = action.payload.sessions.sort((a, b) => Number(b.isActive) - Number(a.isActive)); // Show active sessions first + state.sessions = sorted.map((session) => ({ + id: session.id, + isActive: session.isActive, + seenAt: dateTimeFormatTimeAgo(session.seenAt), + createdAt: dateTimeFormat(session.createdAt, { format: 'MMMM DD, YYYY' }), + clientIp: session.clientIp, + browser: session.browser, + browserVersion: session.browserVersion, + os: session.os, + osVersion: session.osVersion, + device: session.device, + })); + state.sessionsAreLoading = false; + }, + userSessionRevoked: (state, action: PayloadAction<{ tokenId: number }>) => { + state.sessions = state.sessions.filter((session: UserSession) => { + return session.id !== action.payload.tokenId; + }); + state.isUpdating = false; }, }, }); export const updateTimeZoneForSession = (timeZone: TimeZone): ThunkResult => { return async (dispatch) => { - const { updateTimeZone } = slice.actions; - if (!isString(timeZone) || isEmpty(timeZone)) { timeZone = config?.bootData?.user?.timezone; } set(contextSrv, 'user.timezone', timeZone); - dispatch(updateTimeZone(timeZone)); + dispatch(updateTimeZone({ timeZone })); }; }; +export const { + setUpdating, + initLoadOrgs, + orgsLoaded, + initLoadTeams, + teamsLoaded, + userLoaded, + userSessionRevoked, + initLoadSessions, + sessionsLoaded, + updateTimeZone, +} = slice.actions; + export const userReducer = slice.reducer; export default { user: slice.reducer }; diff --git a/public/app/features/profile/state/selectors.ts b/public/app/features/profile/state/selectors.ts index d6d53dade9d..cd984b6a6c6 100644 --- a/public/app/features/profile/state/selectors.ts +++ b/public/app/features/profile/state/selectors.ts @@ -1,3 +1,3 @@ -import { UserState } from 'app/types'; +import { UserState } from './reducers'; export const getTimeZone = (state: UserState) => state.timeZone; diff --git a/public/app/features/profile/types.ts b/public/app/features/profile/types.ts new file mode 100644 index 00000000000..c5692e188b9 --- /dev/null +++ b/public/app/features/profile/types.ts @@ -0,0 +1,11 @@ +export interface ChangePasswordFields { + oldPassword: string; + newPassword: string; + confirmNew: string; +} + +export interface ProfileUpdateFields { + name: string; + email: string; + login: string; +} diff --git a/public/app/features/query/components/QueryGroup.tsx b/public/app/features/query/components/QueryGroup.tsx index c919376e229..034bc83cd9e 100644 --- a/public/app/features/query/components/QueryGroup.tsx +++ b/public/app/features/query/components/QueryGroup.tsx @@ -6,6 +6,7 @@ import { CustomScrollbar, HorizontalGroup, Icon, + InlineFormLabel, Modal, ScrollbarPosition, stylesFactory, @@ -201,6 +202,7 @@ export class QueryGroup extends PureComponent { return (
+ Data source
[ }, ], }, + + { + id: '$$grafana-templateVariables$$', + name: 'Template variables', + type: ResourceRowType.VariableGroup, + typeLabel: 'Variables', + children: [ + { + id: '$machine', + name: '$machine', + type: ResourceRowType.Variable, + typeLabel: 'Variable', + }, + { + id: '$workspace', + name: '$workspace', + type: ResourceRowType.Variable, + typeLabel: 'Variable', + }, + ], + }, ]; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/ResourceField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/ResourceField.tsx index ec2b5505c74..2a225431908 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/ResourceField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/ResourceField.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { Button, Icon, Modal, useStyles2 } from '@grafana/ui'; import React, { useCallback, useEffect, useState } from 'react'; +import Datasource from '../../datasource'; import { AzureQueryEditorFieldProps, AzureResourceSummaryItem } from '../../types'; import { Field } from '../Field'; import ResourcePicker from '../ResourcePicker'; @@ -27,17 +28,8 @@ const ResourceField: React.FC = ({ query, datasource const styles = useStyles2(getStyles); const { resource } = query.azureLogAnalytics; - const [resourceComponents, setResourceComponents] = useState(parseResourceDetails(resource ?? '')); const [pickerIsOpen, setPickerIsOpen] = useState(false); - useEffect(() => { - if (resource && parseResourceDetails(resource)) { - datasource.resourcePickerData.getResource(resource).then(setResourceComponents); - } else { - setResourceComponents(undefined); - } - }, [datasource.resourcePickerData, resource]); - const handleOpenPicker = useCallback(() => { setPickerIsOpen(true); }, []); @@ -60,12 +52,15 @@ const ResourceField: React.FC = ({ query, datasource [closePicker, onQueryChange, query] ); + const templateVariables = datasource.getVariables(); + return ( <> @@ -73,21 +68,53 @@ const ResourceField: React.FC = ({ query, datasource ); }; +interface ResourceLabelProps { + resource: string | undefined; + datasource: Datasource; +} + +const ResourceLabel = ({ resource, datasource }: ResourceLabelProps) => { + const [resourceComponents, setResourceComponents] = useState(parseResourceDetails(resource ?? '')); + + useEffect(() => { + if (resource && parseResourceDetails(resource)) { + datasource.resourcePickerData.getResource(resource).then(setResourceComponents); + } else { + setResourceComponents(undefined); + } + }, [datasource.resourcePickerData, resource]); + + if (!resource) { + return <>Select a resource; + } + + if (resourceComponents) { + return ; + } + + if (resource.startsWith('$')) { + return ( + + {resource} + + ); + } + + return <>{resource}; +}; + interface FormattedResourceProps { resource: AzureResourceSummaryItem; } -const FormattedResource: React.FC = ({ resource }) => { +const FormattedResource = ({ resource }: FormattedResourceProps) => { return ( {resource.subscriptionName} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedResourceTable.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedResourceTable.test.tsx index faedf6e50a8..ba1a53c7e89 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedResourceTable.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedResourceTable.test.tsx @@ -64,4 +64,35 @@ describe('AzureMonitor NestedResourceTable', () => { expect(screen.getByText('web-server')).toBeInTheDocument(); }); + + it('supports selecting variables', async () => { + const rows = createMockResourcePickerRows(); + const promise = Promise.resolve(); + const requestNestedRows = jest.fn().mockReturnValue(promise); + const onRowSelectedChange = jest.fn(); + render( + + ); + + const expandButton = screen.getAllByLabelText('Expand')[5]; + userEvent.click(expandButton); + + await act(() => promise); + + const checkbox = screen.getByLabelText('$workspace'); + userEvent.click(checkbox); + + expect(onRowSelectedChange).toHaveBeenCalledWith( + expect.objectContaining({ + id: '$workspace', + name: '$workspace', + }), + true + ); + }); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedRows.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedRows.tsx index c6e957d09c1..a238b9e5e9b 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedRows.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedRows.tsx @@ -130,6 +130,12 @@ const EntryIcon: React.FC = ({ isOpen, entry: { type } }) => { case ResourceRowType.Resource: return ; + case ResourceRowType.VariableGroup: + return ; + + case ResourceRowType.Variable: + return ; + default: return null; } @@ -157,7 +163,7 @@ const NestedEntry: React.FC = ({ const theme = useTheme2(); const styles = useStyles2(getStyles); const hasChildren = !!entry.children; - const isSelectable = entry.type === ResourceRowType.Resource; + const isSelectable = entry.type === ResourceRowType.Resource || entry.type === ResourceRowType.Variable; const handleToggleCollapse = useCallback(() => { onToggleCollapse(entry); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/index.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/index.tsx index db293fafb8d..c4f0d88e68c 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/index.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/index.tsx @@ -5,22 +5,28 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { Button, useStyles2 } from '@grafana/ui'; import ResourcePickerData from '../../resourcePicker/resourcePickerData'; -import { produce } from 'immer'; import { Space } from '../Space'; -import { findRow, parseResourceURI } from './utils'; +import { addResources, findRow, parseResourceURI } from './utils'; interface ResourcePickerProps { - resourcePickerData: Pick; + resourcePickerData: ResourcePickerData; resourceURI: string | undefined; + templateVariables: string[]; onApply: (resourceURI: string | undefined) => void; onCancel: () => void; } -const ResourcePicker = ({ resourcePickerData, resourceURI, onApply, onCancel }: ResourcePickerProps) => { +const ResourcePicker = ({ + resourcePickerData, + resourceURI, + templateVariables, + onApply, + onCancel, +}: ResourcePickerProps) => { const styles = useStyles2(getStyles); - const [rows, setRows] = useState([]); + const [azureRows, setAzureRows] = useState([]); const [internalSelected, setInternalSelected] = useState(resourceURI); // Sync the resourceURI prop to internal state @@ -28,6 +34,11 @@ const ResourcePicker = ({ resourcePickerData, resourceURI, onApply, onCancel }: setInternalSelected(resourceURI); }, [resourceURI]); + const rows = useMemo(() => { + const templateVariableRow = resourcePickerData.transformVariablesToRow(templateVariables); + return templateVariables.length ? [...azureRows, templateVariableRow] : azureRows; + }, [resourcePickerData, azureRows, templateVariables]); + // Map the selected item into an array of rows const selectedResourceRows = useMemo(() => { const found = internalSelected && findRow(rows, internalSelected); @@ -37,28 +48,18 @@ const ResourcePicker = ({ resourcePickerData, resourceURI, onApply, onCancel }: // Request resources for a expanded resource group const requestNestedRows = useCallback( async (resourceGroup: ResourceRow) => { - // If we already have children, we don't need to re-fetch them - if (resourceGroup.children?.length) { + // If we already have children, we don't need to re-fetch them. Also abort if we're expanding the special + // template variable group, though that shouldn't happen in practice + if (resourceGroup.children?.length || resourceGroup.id === ResourcePickerData.templateVariableGroupID) { return; } // fetch and set nested resources for the resourcegroup into the bigger state object const resources = await resourcePickerData.getResourcesForResourceGroup(resourceGroup); - const newRows = produce(rows, (draftState) => { - // We want to find the row again from the draftState so we can "mutate" it - const draftRow = findRow(draftState, resourceGroup.id); - - if (!draftRow) { - // This case shouldn't happen - throw new Error('Unable to find resource'); - } - - draftRow.children = resources; - }); - - setRows(newRows); + const newRows = addResources(azureRows, resourceGroup.id, resources); + setAzureRows(newRows); }, - [resourcePickerData, rows] + [resourcePickerData, azureRows] ); // Select @@ -68,7 +69,9 @@ const ResourcePicker = ({ resourcePickerData, resourceURI, onApply, onCancel }: // Request initial data on first mount useEffect(() => { - resourcePickerData.getResourcePickerData().then((initalRows) => setRows(initalRows)); + resourcePickerData.getResourcePickerData().then((initalRows) => { + setAzureRows(initalRows); + }); }, [resourcePickerData]); // Request sibling resources for a selected resource - in practice should only be on first mount @@ -87,9 +90,9 @@ const ResourcePicker = ({ resourcePickerData, resourceURI, onApply, onCancel }: const resourceGroupURI = `/subscriptions/${parsedURI?.subscriptionID}/resourceGroups/${parsedURI?.resourceGroup}`; const resourceGroupRow = findRow(rows, resourceGroupURI); - // TODO: Error UI if (!resourceGroupRow) { - throw new Error('Unable to find resource group for resource'); + // We haven't loaded the data from Azure yet + return; } requestNestedRows(resourceGroupRow); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/types.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/types.ts index 919286506bf..2f02f61837f 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/types.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/types.ts @@ -2,6 +2,8 @@ export enum ResourceRowType { Subscription = 'Subscription', ResourceGroup = 'ResourceGroup', Resource = 'Resource', + VariableGroup = 'TemplateVariableGroup', + Variable = 'TemplateVariable', } export interface ResourceRow { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts index f0d238af813..ab9f0af4b24 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts @@ -1,3 +1,4 @@ +import produce from 'immer'; import { ResourceRow, ResourceRowGroup } from './types'; const RESOURCE_URI_REGEX = /\/subscriptions\/(?.+)\/resourceGroups\/(?.+)\/providers.+\/(?[\w-_]+)/; @@ -34,3 +35,16 @@ export function findRow(rows: ResourceRowGroup, id: string): ResourceRow | undef return undefined; } + +export function addResources(rows: ResourceRowGroup, targetResourceGroupID: string, newResources: ResourceRowGroup) { + return produce(rows, (draftState) => { + const draftRow = findRow(draftState, targetResourceGroupID); + + if (!draftRow) { + // This case shouldn't happen often because we're usually coming here from a resource we already have + throw new Error('Unable to find resource'); + } + + draftRow.children = newResources; + }); +} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.ts index e36ad7c7eef..87af244c534 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.ts @@ -22,6 +22,8 @@ export default class ResourcePickerData { this.cloud = getAzureCloud(instanceSettings); } + static readonly templateVariableGroupID = '$$grafana-templateVariables$$'; + async getResourcePickerData() { const query = ` resources @@ -141,6 +143,21 @@ export default class ResourcePickerData { throw error; } } + + transformVariablesToRow(templateVariables: string[]): ResourceRow { + return { + id: ResourcePickerData.templateVariableGroupID, + name: 'Template variables', + type: ResourceRowType.VariableGroup, + typeLabel: 'Variables', + children: templateVariables.map((v) => ({ + id: v, + name: v, + type: ResourceRowType.Variable, + typeLabel: 'Variable', + })), + }; + } } function formatResourceGroupData(rawData: RawAzureResourceGroupItem[]) { diff --git a/public/app/plugins/datasource/prometheus/configuration/PromSettings.test.tsx b/public/app/plugins/datasource/prometheus/configuration/PromSettings.test.tsx index ae426979d3d..fd4525b09de 100644 --- a/public/app/plugins/datasource/prometheus/configuration/PromSettings.test.tsx +++ b/public/app/plugins/datasource/prometheus/configuration/PromSettings.test.tsx @@ -88,7 +88,7 @@ describe('PromSettings', () => { describe('PromSettings component', () => { const defaultProps = createDefaultConfigOptions(); - it('should show POST httpMethod if no httpMethod and no url', () => { + it('should show POST httpMethod if no httpMethod', () => { const options = defaultProps; options.url = ''; options.jsonData.httpMethod = ''; @@ -100,18 +100,6 @@ describe('PromSettings', () => { ); expect(screen.getByText('POST')).toBeInTheDocument(); }); - it('should show GET httpMethod if no httpMethod and url', () => { - const options = defaultProps; - options.url = 'test_url'; - options.jsonData.httpMethod = ''; - - render( -
- {}} options={options} /> -
- ); - expect(screen.getByText('GET')).toBeInTheDocument(); - }); it('should show POST httpMethod if POST httpMethod is configured', () => { const options = defaultProps; options.url = 'test_url'; diff --git a/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx b/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx index 6a0386d6364..fab61014329 100644 --- a/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx +++ b/public/app/plugins/datasource/prometheus/configuration/PromSettings.tsx @@ -20,15 +20,10 @@ type Props = Pick, 'options' | ' export const PromSettings = (props: Props) => { const { options, onOptionsChange } = props; - /** - * We want to change the default httpMethod to 'POST' for all of the new Prometheus data sources instances (no url) added in 7.5+. - * We are explicitly adding httpMethod, as previously it could be undefined and defaulted to 'GET'. - * Undefined httpMethod is still going to be considered 'GET' for backward compatibility reasons, but if users open data - * source settings it is going to be set to 'GET' explicitly and it will be selected in httpMethod dropdown as 'GET'. - * */ + // We are explicitly adding httpMethod so it is correctly displayed in dropdown. This way, it is more predictable for users. if (!options.jsonData.httpMethod) { - options.url ? (options.jsonData.httpMethod = 'GET') : (options.jsonData.httpMethod = 'POST'); + options.jsonData.httpMethod = 'POST'; } return ( diff --git a/public/app/plugins/datasource/prometheus/datasource.test.ts b/public/app/plugins/datasource/prometheus/datasource.test.ts index d4878c5ce87..3a90c7dc976 100644 --- a/public/app/plugins/datasource/prometheus/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/datasource.test.ts @@ -127,12 +127,6 @@ describe('PrometheusDatasource', () => { }); describe('customQueryParams', () => { - const promDs = new PrometheusDatasource( - { ...instanceSettings, jsonData: { customQueryParameters: 'customQuery=123' } as any }, - templateSrvStub as any, - timeSrvStub as any - ); - const target = { expr: 'test{job="testjob"}', format: 'time_series', refId: '' }; function makeQuery(target: PromQuery) { return { @@ -141,31 +135,87 @@ describe('PrometheusDatasource', () => { interval: '60s', } as any; } - it('added to metadata request', () => { - promDs.metadataRequest('/foo'); - expect(fetchMock.mock.calls.length).toBe(1); - expect(fetchMock.mock.calls[0][0].url).toBe('proxied/foo?customQuery=123'); - }); - it('adds params to timeseries query', () => { - promDs.query(makeQuery(target)); - expect(fetchMock.mock.calls.length).toBe(1); - expect(fetchMock.mock.calls[0][0].url).toBe( - 'proxied/api/v1/query_range?query=test%7Bjob%3D%22testjob%22%7D&start=60&end=180&step=60&customQuery=123' + describe('with GET http method', () => { + const promDs = new PrometheusDatasource( + { ...instanceSettings, jsonData: { customQueryParameters: 'customQuery=123', httpMethod: 'GET' } as any }, + templateSrvStub as any, + timeSrvStub as any ); - }); - it('adds params to exemplars query', () => { - promDs.query(makeQuery({ ...target, exemplar: true })); - // We do also range query for single exemplars target - expect(fetchMock.mock.calls.length).toBe(2); - expect(fetchMock.mock.calls[0][0].url).toContain('&customQuery=123'); - expect(fetchMock.mock.calls[1][0].url).toContain('&customQuery=123'); + + it('added to metadata request', () => { + promDs.metadataRequest('/foo'); + expect(fetchMock.mock.calls.length).toBe(1); + expect(fetchMock.mock.calls[0][0].url).toBe('proxied/foo?customQuery=123'); + }); + + it('adds params to timeseries query', () => { + promDs.query(makeQuery(target)); + expect(fetchMock.mock.calls.length).toBe(1); + expect(fetchMock.mock.calls[0][0].url).toBe( + 'proxied/api/v1/query_range?query=test%7Bjob%3D%22testjob%22%7D&start=60&end=180&step=60&customQuery=123' + ); + }); + it('adds params to exemplars query', () => { + promDs.query(makeQuery({ ...target, exemplar: true })); + // We do also range query for single exemplars target + expect(fetchMock.mock.calls.length).toBe(2); + expect(fetchMock.mock.calls[0][0].url).toContain('&customQuery=123'); + expect(fetchMock.mock.calls[1][0].url).toContain('&customQuery=123'); + }); + + it('adds params to instant query', () => { + promDs.query(makeQuery({ ...target, instant: true })); + expect(fetchMock.mock.calls.length).toBe(1); + expect(fetchMock.mock.calls[0][0].url).toContain('&customQuery=123'); + }); }); - it('adds params to instant query', () => { - promDs.query(makeQuery({ ...target, instant: true })); - expect(fetchMock.mock.calls.length).toBe(1); - expect(fetchMock.mock.calls[0][0].url).toContain('&customQuery=123'); + describe('with POST http method', () => { + const promDs = new PrometheusDatasource( + { ...instanceSettings, jsonData: { customQueryParameters: 'customQuery=123', httpMethod: 'POST' } as any }, + templateSrvStub as any, + timeSrvStub as any + ); + + it('added to metadata request with non-POST endpoint', () => { + promDs.metadataRequest('/foo'); + expect(fetchMock.mock.calls.length).toBe(1); + expect(fetchMock.mock.calls[0][0].url).toBe('proxied/foo?customQuery=123'); + }); + + it('added to metadata request with POST endpoint', () => { + promDs.metadataRequest('/api/v1/labels'); + expect(fetchMock.mock.calls.length).toBe(1); + expect(fetchMock.mock.calls[0][0].url).toBe('proxied/api/v1/labels'); + expect(fetchMock.mock.calls[0][0].data.customQuery).toBe('123'); + }); + + it('adds params to timeseries query', () => { + promDs.query(makeQuery(target)); + expect(fetchMock.mock.calls.length).toBe(1); + expect(fetchMock.mock.calls[0][0].url).toBe('proxied/api/v1/query_range'); + expect(fetchMock.mock.calls[0][0].data).toEqual({ + customQuery: '123', + query: 'test{job="testjob"}', + step: 60, + end: 180, + start: 60, + }); + }); + it('adds params to exemplars query', () => { + promDs.query(makeQuery({ ...target, exemplar: true })); + // We do also range query for single exemplars target + expect(fetchMock.mock.calls.length).toBe(2); + expect(fetchMock.mock.calls[0][0].data.customQuery).toBe('123'); + expect(fetchMock.mock.calls[1][0].data.customQuery).toBe('123'); + }); + + it('adds params to instant query', () => { + promDs.query(makeQuery({ ...target, instant: true })); + expect(fetchMock.mock.calls.length).toBe(1); + expect(fetchMock.mock.calls[0][0].data.customQuery).toBe('123'); + }); }); }); diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index bfcbcec3fb5..91ccc0ff170 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -78,7 +78,7 @@ export class PrometheusDatasource extends DataSourceApi this.withCredentials = instanceSettings.withCredentials; this.interval = instanceSettings.jsonData.timeInterval || '15s'; this.queryTimeout = instanceSettings.jsonData.queryTimeout; - this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET'; + this.httpMethod = instanceSettings.jsonData.httpMethod || 'POST'; this.directUrl = instanceSettings.jsonData.directUrl; this.exemplarTraceIdDestinations = instanceSettings.jsonData.exemplarTraceIdDestinations; this.ruleMappings = {}; diff --git a/public/app/plugins/datasource/testdata/QueryEditor.tsx b/public/app/plugins/datasource/testdata/QueryEditor.tsx index 1d31fcf9b93..3b969217ef4 100644 --- a/public/app/plugins/datasource/testdata/QueryEditor.tsx +++ b/public/app/plugins/datasource/testdata/QueryEditor.tsx @@ -6,7 +6,7 @@ import { useAsync } from 'react-use'; import { selectors as editorSelectors } from '@grafana/e2e-selectors'; import { Input, InlineFieldRow, InlineField, Select, TextArea, InlineSwitch } from '@grafana/ui'; import { QueryEditorProps, SelectableValue } from '@grafana/data'; -import { StreamingClientEditor, ManualEntryEditor, RandomWalkEditor } from './components'; +import { StreamingClientEditor, RandomWalkEditor } from './components'; // Types import { TestDataDataSource } from './datasource'; @@ -17,6 +17,8 @@ import { defaultCSVWaveQuery, defaultPulseQuery, defaultQuery } from './constant import { GrafanaLiveEditor } from './components/GrafanaLiveEditor'; import { NodeGraphEditor } from './components/NodeGraphEditor'; import { defaultStreamQuery } from './runStreams'; +import { CSVFileEditor } from './components/CSVFileEditor'; +import { CSVContentEditor } from './components/CSVContentEditor'; const showLabelsFor = ['random_walk', 'predictable_pulse']; const endpoints = [ @@ -38,6 +40,20 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props) query = { ...defaultQuery, ...query }; const { loading, value: scenarioList } = useAsync(async () => { + // migrate manual_entry (unusable since 7, removed in 8) + if (query.scenarioId === 'manual_entry' && (query as any).points) { + let csvContent = 'Time,Value\n'; + for (const point of (query as any).points) { + csvContent += `${point[1]},${point[0]}\n`; + } + onChange({ + refId: query.refId, + datasource: query.datasource, + scenarioId: 'csv_content', + csvContent, + }); + } + return datasource.getScenarios(); }, []); @@ -205,10 +221,11 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props) )} - {scenarioId === 'manual_entry' && } {scenarioId === 'random_walk' && } {scenarioId === 'streaming_client' && } {scenarioId === 'live' && } + {scenarioId === 'csv_file' && } + {scenarioId === 'csv_content' && } {scenarioId === 'logs' && ( diff --git a/public/app/plugins/datasource/testdata/components/CSVContentEditor.tsx b/public/app/plugins/datasource/testdata/components/CSVContentEditor.tsx new file mode 100644 index 00000000000..a2fa57694d2 --- /dev/null +++ b/public/app/plugins/datasource/testdata/components/CSVContentEditor.tsx @@ -0,0 +1,21 @@ +import React, { ChangeEvent } from 'react'; +import { InlineField, TextArea } from '@grafana/ui'; +import { EditorProps } from '../QueryEditor'; + +export const CSVContentEditor = ({ onChange, query }: EditorProps) => { + const onContent = (e: ChangeEvent) => { + onChange({ ...query, csvContent: e.currentTarget.value }); + }; + + return ( + +