diff --git a/.circleci/config.yml b/.circleci/config.yml index dba6c5f8bd0..ec1fcfb411f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -127,7 +127,7 @@ jobs: build-all: docker: - - image: grafana/build-container:1.2.1 + - image: grafana/build-container:1.2.2 working_directory: /go/src/github.com/grafana/grafana steps: - checkout @@ -200,51 +200,51 @@ jobs: - dist/grafana* grafana-docker-master: - docker: - - image: docker:stable-git + machine: + image: circleci/classic:201808-01 steps: - checkout - attach_workspace: at: . - - setup_remote_docker - run: docker info - - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker + - run: docker run --privileged linuxkit/binfmt:v0.6 + - run: cp dist/grafana-latest.linux-*.tar.gz packaging/docker - run: cd packaging/docker && ./build-deploy.sh "master-${CIRCLE_SHA1}" - - run: rm packaging/docker/grafana-latest.linux-x64.tar.gz + - run: rm packaging/docker/grafana-latest.linux-*.tar.gz - run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz - run: cd packaging/docker && ./build-enterprise.sh "master" grafana-docker-pr: - docker: - - image: docker:stable-git + machine: + image: circleci/classic:201808-01 steps: - checkout - attach_workspace: at: . - - setup_remote_docker - run: docker info - - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker + - run: docker run --privileged linuxkit/binfmt:v0.6 + - run: cp dist/grafana-latest.linux-*.tar.gz packaging/docker - run: cd packaging/docker && ./build.sh "${CIRCLE_SHA1}" grafana-docker-release: - docker: - - image: docker:stable-git - steps: - - checkout - - attach_workspace: - at: . - - setup_remote_docker - - run: docker info - - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker - - run: cd packaging/docker && ./build-deploy.sh "${CIRCLE_TAG}" - - run: rm packaging/docker/grafana-latest.linux-x64.tar.gz - - run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz - - run: cd packaging/docker && ./build-enterprise.sh "${CIRCLE_TAG}" + machine: + image: circleci/classic:201808-01 + steps: + - checkout + - attach_workspace: + at: . + - run: docker info + - run: docker run --privileged linuxkit/binfmt:v0.6 + - run: cp dist/grafana-latest.linux-*.tar.gz packaging/docker + - run: cd packaging/docker && ./build-deploy.sh "${CIRCLE_TAG}" + - run: rm packaging/docker/grafana-latest.linux-*.tar.gz + - run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz + - run: cd packaging/docker && ./build-enterprise.sh "${CIRCLE_TAG}" build-enterprise: docker: - - image: grafana/build-container:1.2.1 + - image: grafana/build-container:1.2.2 working_directory: /go/src/github.com/grafana/grafana steps: - checkout @@ -276,7 +276,7 @@ jobs: build-all-enterprise: docker: - - image: grafana/build-container:1.2.1 + - image: grafana/build-container:1.2.2 working_directory: /go/src/github.com/grafana/grafana steps: - checkout @@ -323,7 +323,7 @@ jobs: deploy-enterprise-master: docker: - - image: grafana/grafana-ci-deploy:1.0.0 + - image: grafana/grafana-ci-deploy:1.1.0 steps: - attach_workspace: at: . @@ -346,7 +346,7 @@ jobs: deploy-enterprise-release: docker: - - image: grafana/grafana-ci-deploy:1.0.0 + - image: grafana/grafana-ci-deploy:1.1.0 steps: - attach_workspace: at: . @@ -365,10 +365,20 @@ jobs: - run: name: Deploy to Grafana.com command: './scripts/build/publish.sh --enterprise' + - run: + name: Load GPG private key + command: './scripts/build/load-signing-key.sh' + - run: + name: Update Debian repository + command: './scripts/build/update_repo/update-deb.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"' + - run: + name: Update RPM repository + command: './scripts/build/update_repo/update-rpm.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"' + deploy-master: docker: - - image: grafana/grafana-ci-deploy:1.0.0 + - image: grafana/grafana-ci-deploy:1.1.0 steps: - attach_workspace: at: . @@ -398,8 +408,9 @@ jobs: deploy-release: docker: - - image: grafana/grafana-ci-deploy:1.0.0 + - image: grafana/grafana-ci-deploy:1.1.0 steps: + - checkout - attach_workspace: at: . - run: @@ -417,6 +428,15 @@ jobs: - run: name: Deploy to Grafana.com command: './scripts/build/publish.sh' + - run: + name: Load GPG private key + command: './scripts/build/load-signing-key.sh' + - run: + name: Update Debian repository + command: './scripts/build/update_repo/update-deb.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"' + - run: + name: Update RPM repository + command: './scripts/build/update_repo/update-rpm.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"' workflows: version: 2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 867253ccd18..9225d6545e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### New Features * **Alerting**: Adds support for Google Hangouts Chat notifications [#11221](https://github.com/grafana/grafana/issues/11221), thx [@PatrickSchuster](https://github.com/PatrickSchuster) +* **Elasticsearch**: Support bucket script pipeline aggregations [#5968](https://github.com/grafana/grafana/issues/5968) * **Snapshots**: Enable deletion of public snapshot [#14109](https://github.com/grafana/grafana/issues/14109) ### Minor @@ -11,15 +12,33 @@ * **Auth**: Prevent password reset when login form is disabled or either LDAP or Auth Proxy is enabled [#14246](https://github.com/grafana/grafana/issues/14246), thx [@SilverFire](https://github.com/SilverFire) * **Dataproxy**: Override incoming Authorization header [#13815](https://github.com/grafana/grafana/issues/13815), thx [@kornholi](https://github.com/kornholi) * **Admin**: Fix prevent removing last grafana admin permissions [#11067](https://github.com/grafana/grafana/issues/11067), thx [@danielbh](https://github.com/danielbh) -* **Templating**: Escaping "Custom" template variables [#13754](https://github.com/grafana/grafana/issues/13754), thx [@IntegersOfK](https://github.com/IntegersOfK) +* **Templating**: Escaping "Custom" template variables [#13754](https://github.com/grafana/grafana/issues/13754), thx [@IntegersOfK]req(https://github.com/IntegersOfK) * **Admin**: When multiple user invitations, all links are the same as the first user who was invited [#14483](https://github.com/grafana/grafana/issues/14483) * **LDAP**: Upgrade go-ldap to v3 [#14548](https://github.com/grafana/grafana/issues/14548) * **Proxy whitelist**: Add CIDR capability to auth_proxy whitelist [#14546](https://github.com/grafana/grafana/issues/14546), thx [@jacobrichard](https://github.com/jacobrichard) * **OAuth**: Support OAuth providers that are not RFC6749 compliant [#14562](https://github.com/grafana/grafana/issues/14562), thx [@tdabasinskas](https://github.com/tdabasinskas) * **Units**: Add blood glucose level units mg/dL and mmol/L [#14519](https://github.com/grafana/grafana/issues/14519), thx [@kjedamzik](https://github.com/kjedamzik) +* **Stackdriver**: Aggregating series returns more than one series [#14581](https://github.com/grafana/grafana/issues/14581) and [#13914](https://github.com/grafana/grafana/issues/13914), thx [@kinok](https://github.com/kinok) +* **Provisioning**: Fixes bug causing infinite growth in dashboard_version table. [#12864](https://github.com/grafana/grafana/issues/12864) ### Bug fixes * **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486) +* **Prometheus**: Query for annotation always uses 60s step regardless of dashboard range, fixes [#14795](https://github.com/grafana/grafana/issues/14795) + +# 5.4.3 (2019-01-14) + +### Tech + +* **Docker**: Build and publish docker images for armv7 and arm64 [#14617](https://github.com/grafana/grafana/pull/14617), thx [@johanneswuerbach](https://github.com/johanneswuerbach) +* **Backend**: Upgrade to golang 1.11.4 [#14580](https://github.com/grafana/grafana/issues/14580) +* **MySQL** only update session in mysql database when required [#14540](https://github.com/grafana/grafana/pull/14540) + +### Bug fixes +* **Alerting** Invalid frequency causes division by zero in alert scheduler [#14810](https://github.com/grafana/grafana/issues/14810) +* **Dashboard** Dashboard links do not update when time range changes [#14493](https://github.com/grafana/grafana/issues/14493) +* **Limits** Support more than 1000 datasources per org [#13883](https://github.com/grafana/grafana/issues/13883) +* **Backend** fix signed in user for orgId=0 result should return active org id [#14574](https://github.com/grafana/grafana/pull/14574) +* **Provisioning** Adds orgId to user dto for provisioned dashboards [#14678](https://github.com/grafana/grafana/pull/14678) # 5.4.2 (2018-12-13) diff --git a/README.md b/README.md index 1ce4ffbe109..ff5da04f209 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,9 @@ GRAFANA_TEST_DB=postgres go test ./pkg/... If you have any idea for an improvement or found a bug, do not hesitate to open an issue. And if you have time clone this repo and submit a pull request and help me make Grafana -the kickass metrics & devops dashboard we all dream about! +the kickass metrics & devops dashboard we all dream about! + +Read the [contributing](https://github.com/grafana/grafana/blob/master/CONTRIBUTING.md) guide then check the [`beginner friendly`](https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22beginner+friendly%22) label to find issues that are easy and that we would like help with. ## Plugin development diff --git a/build.go b/build.go index 9d5216de1d0..4486cd3deb9 100644 --- a/build.go +++ b/build.go @@ -164,6 +164,8 @@ func makeLatestDistCopies() { "_amd64.deb": "dist/grafana_latest_amd64.deb", ".x86_64.rpm": "dist/grafana-latest-1.x86_64.rpm", ".linux-amd64.tar.gz": "dist/grafana-latest.linux-x64.tar.gz", + ".linux-armv7.tar.gz": "dist/grafana-latest.linux-armv7.tar.gz", + ".linux-arm64.tar.gz": "dist/grafana-latest.linux-arm64.tar.gz", } for _, file := range files { diff --git a/devenv/dashboards.yaml b/devenv/dashboards.yaml index 226c1a8b335..c59d7127769 100644 --- a/devenv/dashboards.yaml +++ b/devenv/dashboards.yaml @@ -4,6 +4,6 @@ providers: - name: 'gdev dashboards' folder: 'gdev dashboards' type: file + updateIntervalSeconds: 15 options: path: devenv/dev-dashboards - diff --git a/devenv/dev-dashboards-without-uid/panel_tests_graph.json b/devenv/dev-dashboards-without-uid/panel_tests_graph.json new file mode 100644 index 00000000000..c47a932ce7f --- /dev/null +++ b/devenv/dev-dashboards-without-uid/panel_tests_graph.json @@ -0,0 +1,1674 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 0 + }, + "id": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "no_data_points", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "No Data Points Warning", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 0 + }, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "datapoints_outside_range", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Datapoints Outside Range Warning", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 0 + }, + "id": 3, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "random_walk", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Random walk series", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 16, + "x": 0, + "y": 7 + }, + "id": 4, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "random_walk", + "target": "" + } + ], + "thresholds": [], + "timeFrom": "2s", + "timeShift": null, + "title": "Millisecond res x-axis and tooltip", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "content": "Just verify that the tooltip time has millisecond resolution ", + "editable": true, + "error": false, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 7 + }, + "id": 6, + "links": [], + "mode": "markdown", + "title": "", + "type": "text" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 9, + "w": 16, + "x": 0, + "y": 14 + }, + "id": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "B-series", + "yaxis": 2 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "2000,3000,4000,1000,3000,10000", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "2 yaxis and axis labels", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "percent", + "label": "Perecent", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": "Pressure", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "content": "Verify that axis labels look ok", + "editable": true, + "error": false, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 14 + }, + "id": 7, + "links": [], + "mode": "markdown", + "title": "", + "type": "text" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 23 + }, + "id": 8, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,null,null,null,null,null,null,100,10,10,20,30,40,10", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "null value connected", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 23 + }, + "id": 10, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,null,null,null,null,null,null,100,10,10,20,30,40,10", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "null value null as zero", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "content": "Should be a long line connecting the null region in the `connected` mode, and in zero it should just be a line with zero value at the null points. ", + "editable": true, + "error": false, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 23 + }, + "id": 13, + "links": [], + "mode": "markdown", + "title": "", + "type": "text" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 16, + "x": 0, + "y": 30 + }, + "id": 9, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "B-series", + "zindex": -3 + } + ], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "hide": false, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,null,null,null,null,null,null,100,10,10,20,30,40,10", + "target": "" + }, + { + "alias": "", + "hide": false, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10,20,30,40,40,40,100,10,20,20", + "target": "" + }, + { + "alias": "", + "hide": false, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10,20,30,40,40,40,100,10,20,20", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Stacking value ontop of nulls", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "content": "Stacking values on top of nulls, should treat the null values as zero. ", + "editable": true, + "error": false, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 30 + }, + "id": 14, + "links": [], + "mode": "markdown", + "title": "", + "type": "text" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 16, + "x": 0, + "y": 37 + }, + "id": 12, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "B-series", + "zindex": -3 + } + ], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "hide": false, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10", + "target": "" + }, + { + "alias": "", + "hide": false, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10", + "target": "" + }, + { + "alias": "", + "hide": false, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Stacking all series null segment", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "content": "Stacking when all values are null should leave a gap in the graph", + "editable": true, + "error": false, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 37 + }, + "id": 15, + "links": [], + "mode": "markdown", + "title": "", + "type": "text" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 0, + "gridPos": { + "h": 7, + "w": 16, + "x": 0, + "y": 44 + }, + "id": 21, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "C-series", + "steppedLine": true + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "hide": false, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,null,40,null,90,null,null,100,null,null,100,null,null,80,null", + "target": "" + }, + { + "alias": "", + "hide": false, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "20,null40,null,null,50,null,70,null,100,null,10,null,30,null", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Null between points", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "content": "Left is showing null between values for a normal line graph and staircase graph. Orphaned data points should be rendered as points", + "editable": true, + "error": false, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 44 + }, + "id": 22, + "links": [], + "mode": "markdown", + "title": "", + "type": "text" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "decimals": 3, + "fill": 1, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 51 + }, + "id": 20, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Legend Table Single Series Should Take Minimum Height", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "decimals": 3, + "fill": 1, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 58 + }, + "id": 16, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Legend Table No Scroll Visible", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "decimals": 3, + "fill": 1, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 58 + }, + "id": 17, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "E", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "F", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "G", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "H", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "I", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "J", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Legend Table Should Scroll", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "decimals": 3, + "fill": 1, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 65 + }, + "id": 18, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Legend Table No Scroll Visible", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "decimals": 3, + "fill": 1, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 65 + }, + "id": 19, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "E", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "F", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "G", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "H", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "I", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "J", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "K", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "L", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Legend Table No Scroll Visible", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": false, + "revision": 8, + "schemaVersion": 16, + "style": "dark", + "tags": [ + "gdev", + "panel-tests" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "browser", + "title": "Panel Tests - Graph", + "version": 1 +} diff --git a/devenv/dev-dashboards-without-uid/panel_tests_graph_time_regions.json b/devenv/dev-dashboards-without-uid/panel_tests_graph_time_regions.json new file mode 100644 index 00000000000..98121cccba6 --- /dev/null +++ b/devenv/dev-dashboards-without-uid/panel_tests_graph_time_regions.json @@ -0,0 +1,510 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "fill": 2, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenarioId": "random_walk", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [ + { + "colorMode": "gray", + "fill": true, + "fillColor": "rgba(255, 255, 255, 0.03)", + "from": "08:30", + "fromDayOfWeek": 1, + "line": false, + "lineColor": "rgba(255, 255, 255, 0.2)", + "op": "time", + "to": "16:45", + "toDayOfWeek": 5 + } + ], + "timeShift": null, + "title": "Business Hours", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "fill": 2, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 4, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "random_walk", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [ + { + "colorMode": "red", + "fill": true, + "fillColor": "rgba(255, 255, 255, 0.03)", + "from": "20:00", + "fromDayOfWeek": 7, + "line": false, + "lineColor": "rgba(255, 255, 255, 0.2)", + "op": "time", + "to": "23:00", + "toDayOfWeek": 7 + } + ], + "timeShift": null, + "title": "Sunday's 20-23", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "A-series": "#d683ce" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "fill": 2, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 3, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 0.5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenarioId": "random_walk", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [ + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(255, 0, 0, 0.22)", + "from": "", + "fromDayOfWeek": 1, + "line": true, + "lineColor": "rgba(255, 0, 0, 0.32)", + "op": "time", + "to": "", + "toDayOfWeek": 1 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(255, 127, 0, 0.22)", + "fromDayOfWeek": 2, + "line": true, + "lineColor": "rgba(255, 127, 0, 0.32)", + "op": "time", + "toDayOfWeek": 2 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(255, 255, 0, 0.22)", + "fromDayOfWeek": 3, + "line": true, + "lineColor": "rgba(255, 255, 0, 0.22)", + "op": "time", + "toDayOfWeek": 3 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(0, 255, 0, 0.22)", + "fromDayOfWeek": 4, + "line": true, + "lineColor": "rgba(0, 255, 0, 0.32)", + "op": "time", + "toDayOfWeek": 4 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(0, 0, 255, 0.22)", + "fromDayOfWeek": 5, + "line": true, + "lineColor": "rgba(0, 0, 255, 0.32)", + "op": "time", + "toDayOfWeek": 5 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(75, 0, 130, 0.22)", + "fromDayOfWeek": 6, + "line": true, + "lineColor": "rgba(75, 0, 130, 0.32)", + "op": "time", + "toDayOfWeek": 6 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(148, 0, 211, 0.22)", + "fromDayOfWeek": 7, + "line": true, + "lineColor": "rgba(148, 0, 211, 0.32)", + "op": "time", + "toDayOfWeek": 7 + } + ], + "timeShift": null, + "title": "Each day of week", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "fill": 2, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 24 + }, + "id": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "random_walk", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [ + { + "colorMode": "red", + "fill": false, + "from": "05:00", + "line": true, + "op": "time" + } + ], + "timeShift": null, + "title": "05:00", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": false, + "schemaVersion": 16, + "style": "dark", + "tags": [ + "gdev", + "panel-tests" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-30d", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "browser", + "title": "Panel Tests - Graph (Time Regions)", + "version": 1 +} \ No newline at end of file diff --git a/devenv/dev-dashboards-without-uid/panel_tests_polystat.json b/devenv/dev-dashboards-without-uid/panel_tests_polystat.json new file mode 100644 index 00000000000..d18586f88d4 --- /dev/null +++ b/devenv/dev-dashboards-without-uid/panel_tests_polystat.json @@ -0,0 +1,3342 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "animationModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "d3DivId": "d3_svg_4", + "datasource": "gdev-testdata", + "decimals": 2, + "displayModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "fontSizes": [ + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 22, + 24, + 26, + 28, + 30, + 32, + 34, + 36, + 38, + 40, + 42, + 44, + 46, + 48, + 50, + 52, + 54, + 56, + 58, + 60, + 62, + 64, + 66, + 68, + 70 + ], + "fontTypes": [ + "Open Sans", + "Arial", + "Avant Garde", + "Bookman", + "Consolas", + "Courier", + "Courier New", + "Futura", + "Garamond", + "Helvetica", + "Palatino", + "Times", + "Times New Roman", + "Verdana" + ], + "format": "none", + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 4, + "links": [], + "notcolors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "operatorName": "avg", + "operatorOptions": [ + { + "text": "Average", + "value": "avg" + }, + { + "text": "Count", + "value": "count" + }, + { + "text": "Current", + "value": "current" + }, + { + "text": "Delta", + "value": "delta" + }, + { + "text": "Difference", + "value": "diff" + }, + { + "text": "First", + "value": "first" + }, + { + "text": "Log Min", + "value": "logmin" + }, + { + "text": "Max", + "value": "max" + }, + { + "text": "Min", + "value": "min" + }, + { + "text": "Name", + "value": "name" + }, + { + "text": "Time of Last Point", + "value": "last_time" + }, + { + "text": "Time Step", + "value": "time_step" + }, + { + "text": "Total", + "value": "total" + } + ], + "polystat": { + "animationSpeed": 2500, + "columnAutoSize": true, + "columns": "", + "defaultClickThrough": "", + "defaultClickThroughSanitize": true, + "displayLimit": 100, + "fontAutoScale": true, + "fontSize": 12, + "globalDisplayMode": "all", + "globalOperatorName": "avg", + "gradientEnabled": true, + "hexagonSortByDirection": "asc", + "hexagonSortByField": "name", + "maxMetrics": 0, + "polygonBorderColor": "black", + "polygonBorderSize": 2, + "radius": "", + "radiusAutoSize": true, + "rowAutoSize": true, + "rows": "", + "shape": "hexagon_pointed_top", + "tooltipDisplayMode": "all", + "tooltipDisplayTextTriggeredEmpty": "OK", + "tooltipFontSize": 12, + "tooltipFontType": "Open Sans", + "tooltipPrimarySortDirection": "desc", + "tooltipPrimarySortField": "thresholdLevel", + "tooltipSecondarySortDirection": "desc", + "tooltipSecondarySortField": "value", + "tooltipTimestampEnabled": true + }, + "savedComposites": [], + "savedOverrides": [], + "shapes": [ + { + "text": "Hexagon Pointed Top", + "value": "hexagon_pointed_top" + }, + { + "text": "Hexagon Flat Top", + "value": "hexagon_flat_top" + }, + { + "text": "Circle", + "value": "circle" + }, + { + "text": "Cross", + "value": "cross" + }, + { + "text": "Diamond", + "value": "diamond" + }, + { + "text": "Square", + "value": "square" + }, + { + "text": "Star", + "value": "star" + }, + { + "text": "Triangle", + "value": "triangle" + }, + { + "text": "Wye", + "value": "wye" + } + ], + "sortDirections": [ + { + "text": "Ascending", + "value": "asc" + }, + { + "text": "Descending", + "value": "desc" + } + ], + "sortFields": [ + { + "text": "Name", + "value": "name" + }, + { + "text": "Threshold Level", + "value": "thresholdLevel" + }, + { + "text": "Value", + "value": "value" + } + ], + "svgContainer": {}, + "targets": [ + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "random_walk" + }, + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "B", + "scenarioId": "random_walk" + }, + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "C", + "scenarioId": "random_walk" + }, + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "D", + "scenarioId": "random_walk" + }, + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "E", + "scenarioId": "random_walk" + } + ], + "thresholdStates": [ + { + "text": "ok", + "value": 0 + }, + { + "text": "warning", + "value": 1 + }, + { + "text": "critical", + "value": 2 + }, + { + "text": "custom", + "value": 3 + } + ], + "title": "Poor use of space", + "type": "grafana-polystat-panel", + "unitFormats": [ + { + "submenu": [ + { + "text": "none", + "value": "none" + }, + { + "text": "short", + "value": "short" + }, + { + "text": "percent (0-100)", + "value": "percent" + }, + { + "text": "percent (0.0-1.0)", + "value": "percentunit" + }, + { + "text": "Humidity (%H)", + "value": "humidity" + }, + { + "text": "decibel", + "value": "dB" + }, + { + "text": "hexadecimal (0x)", + "value": "hex0x" + }, + { + "text": "hexadecimal", + "value": "hex" + }, + { + "text": "scientific notation", + "value": "sci" + }, + { + "text": "locale format", + "value": "locale" + } + ], + "text": "none" + }, + { + "submenu": [ + { + "text": "Dollars ($)", + "value": "currencyUSD" + }, + { + "text": "Pounds (£)", + "value": "currencyGBP" + }, + { + "text": "Euro (€)", + "value": "currencyEUR" + }, + { + "text": "Yen (¥)", + "value": "currencyJPY" + }, + { + "text": "Rubles (₽)", + "value": "currencyRUB" + }, + { + "text": "Hryvnias (₴)", + "value": "currencyUAH" + }, + { + "text": "Real (R$)", + "value": "currencyBRL" + }, + { + "text": "Danish Krone (kr)", + "value": "currencyDKK" + }, + { + "text": "Icelandic Króna (kr)", + "value": "currencyISK" + }, + { + "text": "Norwegian Krone (kr)", + "value": "currencyNOK" + }, + { + "text": "Swedish Krona (kr)", + "value": "currencySEK" + }, + { + "text": "Czech koruna (czk)", + "value": "currencyCZK" + }, + { + "text": "Swiss franc (CHF)", + "value": "currencyCHF" + }, + { + "text": "Polish Złoty (PLN)", + "value": "currencyPLN" + }, + { + "text": "Bitcoin (฿)", + "value": "currencyBTC" + } + ], + "text": "currency" + }, + { + "submenu": [ + { + "text": "Hertz (1/s)", + "value": "hertz" + }, + { + "text": "nanoseconds (ns)", + "value": "ns" + }, + { + "text": "microseconds (µs)", + "value": "µs" + }, + { + "text": "milliseconds (ms)", + "value": "ms" + }, + { + "text": "seconds (s)", + "value": "s" + }, + { + "text": "minutes (m)", + "value": "m" + }, + { + "text": "hours (h)", + "value": "h" + }, + { + "text": "days (d)", + "value": "d" + }, + { + "text": "duration (ms)", + "value": "dtdurationms" + }, + { + "text": "duration (s)", + "value": "dtdurations" + }, + { + "text": "duration (hh:mm:ss)", + "value": "dthms" + }, + { + "text": "Timeticks (s/100)", + "value": "timeticks" + } + ], + "text": "time" + }, + { + "submenu": [ + { + "text": "YYYY-MM-DD HH:mm:ss", + "value": "dateTimeAsIso" + }, + { + "text": "DD/MM/YYYY h:mm:ss a", + "value": "dateTimeAsUS" + }, + { + "text": "From Now", + "value": "dateTimeFromNow" + } + ], + "text": "date & time" + }, + { + "submenu": [ + { + "text": "bits", + "value": "bits" + }, + { + "text": "bytes", + "value": "bytes" + }, + { + "text": "kibibytes", + "value": "kbytes" + }, + { + "text": "mebibytes", + "value": "mbytes" + }, + { + "text": "gibibytes", + "value": "gbytes" + } + ], + "text": "data (IEC)" + }, + { + "submenu": [ + { + "text": "bits", + "value": "decbits" + }, + { + "text": "bytes", + "value": "decbytes" + }, + { + "text": "kilobytes", + "value": "deckbytes" + }, + { + "text": "megabytes", + "value": "decmbytes" + }, + { + "text": "gigabytes", + "value": "decgbytes" + } + ], + "text": "data (Metric)" + }, + { + "submenu": [ + { + "text": "packets/sec", + "value": "pps" + }, + { + "text": "bits/sec", + "value": "bps" + }, + { + "text": "bytes/sec", + "value": "Bps" + }, + { + "text": "kilobits/sec", + "value": "Kbits" + }, + { + "text": "kilobytes/sec", + "value": "KBs" + }, + { + "text": "megabits/sec", + "value": "Mbits" + }, + { + "text": "megabytes/sec", + "value": "MBs" + }, + { + "text": "gigabytes/sec", + "value": "GBs" + }, + { + "text": "gigabits/sec", + "value": "Gbits" + } + ], + "text": "data rate" + }, + { + "submenu": [ + { + "text": "hashes/sec", + "value": "Hs" + }, + { + "text": "kilohashes/sec", + "value": "KHs" + }, + { + "text": "megahashes/sec", + "value": "MHs" + }, + { + "text": "gigahashes/sec", + "value": "GHs" + }, + { + "text": "terahashes/sec", + "value": "THs" + }, + { + "text": "petahashes/sec", + "value": "PHs" + }, + { + "text": "exahashes/sec", + "value": "EHs" + } + ], + "text": "hash rate" + }, + { + "submenu": [ + { + "text": "ops/sec (ops)", + "value": "ops" + }, + { + "text": "requests/sec (rps)", + "value": "reqps" + }, + { + "text": "reads/sec (rps)", + "value": "rps" + }, + { + "text": "writes/sec (wps)", + "value": "wps" + }, + { + "text": "I/O ops/sec (iops)", + "value": "iops" + }, + { + "text": "ops/min (opm)", + "value": "opm" + }, + { + "text": "reads/min (rpm)", + "value": "rpm" + }, + { + "text": "writes/min (wpm)", + "value": "wpm" + } + ], + "text": "throughput" + }, + { + "submenu": [ + { + "text": "millimetre (mm)", + "value": "lengthmm" + }, + { + "text": "meter (m)", + "value": "lengthm" + }, + { + "text": "feet (ft)", + "value": "lengthft" + }, + { + "text": "kilometer (km)", + "value": "lengthkm" + }, + { + "text": "mile (mi)", + "value": "lengthmi" + } + ], + "text": "length" + }, + { + "submenu": [ + { + "text": "Square Meters (m²)", + "value": "areaM2" + }, + { + "text": "Square Feet (ft²)", + "value": "areaF2" + }, + { + "text": "Square Miles (mi²)", + "value": "areaMI2" + } + ], + "text": "area" + }, + { + "submenu": [ + { + "text": "milligram (mg)", + "value": "massmg" + }, + { + "text": "gram (g)", + "value": "massg" + }, + { + "text": "kilogram (kg)", + "value": "masskg" + }, + { + "text": "metric ton (t)", + "value": "masst" + } + ], + "text": "mass" + }, + { + "submenu": [ + { + "text": "metres/second (m/s)", + "value": "velocityms" + }, + { + "text": "kilometers/hour (km/h)", + "value": "velocitykmh" + }, + { + "text": "miles/hour (mph)", + "value": "velocitymph" + }, + { + "text": "knot (kn)", + "value": "velocityknot" + } + ], + "text": "velocity" + }, + { + "submenu": [ + { + "text": "millilitre (mL)", + "value": "mlitre" + }, + { + "text": "litre (L)", + "value": "litre" + }, + { + "text": "cubic metre", + "value": "m3" + }, + { + "text": "Normal cubic metre", + "value": "Nm3" + }, + { + "text": "cubic decimetre", + "value": "dm3" + }, + { + "text": "gallons", + "value": "gallons" + } + ], + "text": "volume" + }, + { + "submenu": [ + { + "text": "Watt (W)", + "value": "watt" + }, + { + "text": "Kilowatt (kW)", + "value": "kwatt" + }, + { + "text": "Milliwatt (mW)", + "value": "mwatt" + }, + { + "text": "Watt per square metre (W/m²)", + "value": "Wm2" + }, + { + "text": "Volt-ampere (VA)", + "value": "voltamp" + }, + { + "text": "Kilovolt-ampere (kVA)", + "value": "kvoltamp" + }, + { + "text": "Volt-ampere reactive (var)", + "value": "voltampreact" + }, + { + "text": "Kilovolt-ampere reactive (kvar)", + "value": "kvoltampreact" + }, + { + "text": "Watt-hour (Wh)", + "value": "watth" + }, + { + "text": "Kilowatt-hour (kWh)", + "value": "kwatth" + }, + { + "text": "Kilowatt-min (kWm)", + "value": "kwattm" + }, + { + "text": "Joule (J)", + "value": "joule" + }, + { + "text": "Electron volt (eV)", + "value": "ev" + }, + { + "text": "Ampere (A)", + "value": "amp" + }, + { + "text": "Kiloampere (kA)", + "value": "kamp" + }, + { + "text": "Milliampere (mA)", + "value": "mamp" + }, + { + "text": "Volt (V)", + "value": "volt" + }, + { + "text": "Kilovolt (kV)", + "value": "kvolt" + }, + { + "text": "Millivolt (mV)", + "value": "mvolt" + }, + { + "text": "Decibel-milliwatt (dBm)", + "value": "dBm" + }, + { + "text": "Ohm (Ω)", + "value": "ohm" + }, + { + "text": "Lumens (Lm)", + "value": "lumens" + } + ], + "text": "energy" + }, + { + "submenu": [ + { + "text": "Celsius (°C)", + "value": "celsius" + }, + { + "text": "Farenheit (°F)", + "value": "farenheit" + }, + { + "text": "Kelvin (K)", + "value": "kelvin" + } + ], + "text": "temperature" + }, + { + "submenu": [ + { + "text": "Millibars", + "value": "pressurembar" + }, + { + "text": "Bars", + "value": "pressurebar" + }, + { + "text": "Kilobars", + "value": "pressurekbar" + }, + { + "text": "Hectopascals", + "value": "pressurehpa" + }, + { + "text": "Kilopascals", + "value": "pressurekpa" + }, + { + "text": "Inches of mercury", + "value": "pressurehg" + }, + { + "text": "PSI", + "value": "pressurepsi" + } + ], + "text": "pressure" + }, + { + "submenu": [ + { + "text": "Newton-meters (Nm)", + "value": "forceNm" + }, + { + "text": "Kilonewton-meters (kNm)", + "value": "forcekNm" + }, + { + "text": "Newtons (N)", + "value": "forceN" + }, + { + "text": "Kilonewtons (kN)", + "value": "forcekN" + } + ], + "text": "force" + }, + { + "submenu": [ + { + "text": "Gallons/min (gpm)", + "value": "flowgpm" + }, + { + "text": "Cubic meters/sec (cms)", + "value": "flowcms" + }, + { + "text": "Cubic feet/sec (cfs)", + "value": "flowcfs" + }, + { + "text": "Cubic feet/min (cfm)", + "value": "flowcfm" + }, + { + "text": "Litre/hour", + "value": "litreh" + }, + { + "text": "Litre/min (l/min)", + "value": "flowlpm" + }, + { + "text": "milliLitre/min (mL/min)", + "value": "flowmlpm" + } + ], + "text": "flow" + }, + { + "submenu": [ + { + "text": "Degrees (°)", + "value": "degree" + }, + { + "text": "Radians", + "value": "radian" + }, + { + "text": "Gradian", + "value": "grad" + } + ], + "text": "angle" + }, + { + "submenu": [ + { + "text": "Meters/sec²", + "value": "accMS2" + }, + { + "text": "Feet/sec²", + "value": "accFS2" + }, + { + "text": "G unit", + "value": "accG" + } + ], + "text": "acceleration" + }, + { + "submenu": [ + { + "text": "Becquerel (Bq)", + "value": "radbq" + }, + { + "text": "curie (Ci)", + "value": "radci" + }, + { + "text": "Gray (Gy)", + "value": "radgy" + }, + { + "text": "rad", + "value": "radrad" + }, + { + "text": "Sievert (Sv)", + "value": "radsv" + }, + { + "text": "rem", + "value": "radrem" + }, + { + "text": "Exposure (C/kg)", + "value": "radexpckg" + }, + { + "text": "roentgen (R)", + "value": "radr" + }, + { + "text": "Sievert/hour (Sv/h)", + "value": "radsvh" + } + ], + "text": "radiation" + }, + { + "submenu": [ + { + "text": "parts-per-million (ppm)", + "value": "ppm" + }, + { + "text": "parts-per-billion (ppb)", + "value": "conppb" + }, + { + "text": "nanogram per cubic metre (ng/m³)", + "value": "conngm3" + }, + { + "text": "nanogram per normal cubic metre (ng/Nm³)", + "value": "conngNm3" + }, + { + "text": "microgram per cubic metre (μg/m³)", + "value": "conμgm3" + }, + { + "text": "microgram per normal cubic metre (μg/Nm³)", + "value": "conμgNm3" + }, + { + "text": "milligram per cubic metre (mg/m³)", + "value": "conmgm3" + }, + { + "text": "milligram per normal cubic metre (mg/Nm³)", + "value": "conmgNm3" + }, + { + "text": "gram per cubic metre (g/m³)", + "value": "congm3" + }, + { + "text": "gram per normal cubic metre (g/Nm³)", + "value": "congNm3" + } + ], + "text": "concentration" + } + ] + }, + { + "animationModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "d3DivId": "d3_svg_5", + "datasource": "gdev-testdata", + "decimals": 2, + "displayModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "fontSizes": [ + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 22, + 24, + 26, + 28, + 30, + 32, + 34, + 36, + 38, + 40, + 42, + 44, + 46, + 48, + 50, + 52, + 54, + 56, + 58, + 60, + 62, + 64, + 66, + 68, + 70 + ], + "fontTypes": [ + "Open Sans", + "Arial", + "Avant Garde", + "Bookman", + "Consolas", + "Courier", + "Courier New", + "Futura", + "Garamond", + "Helvetica", + "Palatino", + "Times", + "Times New Roman", + "Verdana" + ], + "format": "none", + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 5, + "links": [], + "notcolors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "operatorName": "avg", + "operatorOptions": [ + { + "text": "Average", + "value": "avg" + }, + { + "text": "Count", + "value": "count" + }, + { + "text": "Current", + "value": "current" + }, + { + "text": "Delta", + "value": "delta" + }, + { + "text": "Difference", + "value": "diff" + }, + { + "text": "First", + "value": "first" + }, + { + "text": "Log Min", + "value": "logmin" + }, + { + "text": "Max", + "value": "max" + }, + { + "text": "Min", + "value": "min" + }, + { + "text": "Name", + "value": "name" + }, + { + "text": "Time of Last Point", + "value": "last_time" + }, + { + "text": "Time Step", + "value": "time_step" + }, + { + "text": "Total", + "value": "total" + } + ], + "polystat": { + "animationSpeed": 2500, + "columnAutoSize": true, + "columns": "", + "defaultClickThrough": "", + "defaultClickThroughSanitize": true, + "displayLimit": 100, + "fontAutoScale": true, + "fontSize": 12, + "globalDisplayMode": "all", + "globalOperatorName": "avg", + "gradientEnabled": true, + "hexagonSortByDirection": "asc", + "hexagonSortByField": "name", + "maxMetrics": 0, + "polygonBorderColor": "black", + "polygonBorderSize": 2, + "radius": "", + "radiusAutoSize": true, + "rowAutoSize": true, + "rows": "", + "shape": "hexagon_pointed_top", + "tooltipDisplayMode": "all", + "tooltipDisplayTextTriggeredEmpty": "OK", + "tooltipFontSize": 12, + "tooltipFontType": "Open Sans", + "tooltipPrimarySortDirection": "desc", + "tooltipPrimarySortField": "thresholdLevel", + "tooltipSecondarySortDirection": "desc", + "tooltipSecondarySortField": "value", + "tooltipTimestampEnabled": true + }, + "savedComposites": [ + { + "compositeName": "comp", + "members": [ + { + "seriesName": "A-series" + }, + { + "seriesName": "B-series" + } + ], + "enabled": true, + "clickThrough": "", + "hideMembers": true, + "showName": true, + "showValue": true, + "animateMode": "all", + "thresholdLevel": 0, + "sanitizeURLEnabled": true, + "sanitizedURL": "" + } + ], + "savedOverrides": [], + "shapes": [ + { + "text": "Hexagon Pointed Top", + "value": "hexagon_pointed_top" + }, + { + "text": "Hexagon Flat Top", + "value": "hexagon_flat_top" + }, + { + "text": "Circle", + "value": "circle" + }, + { + "text": "Cross", + "value": "cross" + }, + { + "text": "Diamond", + "value": "diamond" + }, + { + "text": "Square", + "value": "square" + }, + { + "text": "Star", + "value": "star" + }, + { + "text": "Triangle", + "value": "triangle" + }, + { + "text": "Wye", + "value": "wye" + } + ], + "sortDirections": [ + { + "text": "Ascending", + "value": "asc" + }, + { + "text": "Descending", + "value": "desc" + } + ], + "sortFields": [ + { + "text": "Name", + "value": "name" + }, + { + "text": "Threshold Level", + "value": "thresholdLevel" + }, + { + "text": "Value", + "value": "value" + } + ], + "svgContainer": {}, + "targets": [ + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "random_walk" + }, + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "B", + "scenarioId": "random_walk" + }, + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "C", + "scenarioId": "random_walk" + }, + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "D", + "scenarioId": "random_walk" + }, + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "E", + "scenarioId": "random_walk" + } + ], + "thresholdStates": [ + { + "text": "ok", + "value": 0 + }, + { + "text": "warning", + "value": 1 + }, + { + "text": "critical", + "value": 2 + }, + { + "text": "custom", + "value": 3 + } + ], + "title": "Composite crash", + "type": "grafana-polystat-panel", + "unitFormats": [ + { + "submenu": [ + { + "text": "none", + "value": "none" + }, + { + "text": "short", + "value": "short" + }, + { + "text": "percent (0-100)", + "value": "percent" + }, + { + "text": "percent (0.0-1.0)", + "value": "percentunit" + }, + { + "text": "Humidity (%H)", + "value": "humidity" + }, + { + "text": "decibel", + "value": "dB" + }, + { + "text": "hexadecimal (0x)", + "value": "hex0x" + }, + { + "text": "hexadecimal", + "value": "hex" + }, + { + "text": "scientific notation", + "value": "sci" + }, + { + "text": "locale format", + "value": "locale" + } + ], + "text": "none" + }, + { + "submenu": [ + { + "text": "Dollars ($)", + "value": "currencyUSD" + }, + { + "text": "Pounds (£)", + "value": "currencyGBP" + }, + { + "text": "Euro (€)", + "value": "currencyEUR" + }, + { + "text": "Yen (¥)", + "value": "currencyJPY" + }, + { + "text": "Rubles (₽)", + "value": "currencyRUB" + }, + { + "text": "Hryvnias (₴)", + "value": "currencyUAH" + }, + { + "text": "Real (R$)", + "value": "currencyBRL" + }, + { + "text": "Danish Krone (kr)", + "value": "currencyDKK" + }, + { + "text": "Icelandic Króna (kr)", + "value": "currencyISK" + }, + { + "text": "Norwegian Krone (kr)", + "value": "currencyNOK" + }, + { + "text": "Swedish Krona (kr)", + "value": "currencySEK" + }, + { + "text": "Czech koruna (czk)", + "value": "currencyCZK" + }, + { + "text": "Swiss franc (CHF)", + "value": "currencyCHF" + }, + { + "text": "Polish Złoty (PLN)", + "value": "currencyPLN" + }, + { + "text": "Bitcoin (฿)", + "value": "currencyBTC" + } + ], + "text": "currency" + }, + { + "submenu": [ + { + "text": "Hertz (1/s)", + "value": "hertz" + }, + { + "text": "nanoseconds (ns)", + "value": "ns" + }, + { + "text": "microseconds (µs)", + "value": "µs" + }, + { + "text": "milliseconds (ms)", + "value": "ms" + }, + { + "text": "seconds (s)", + "value": "s" + }, + { + "text": "minutes (m)", + "value": "m" + }, + { + "text": "hours (h)", + "value": "h" + }, + { + "text": "days (d)", + "value": "d" + }, + { + "text": "duration (ms)", + "value": "dtdurationms" + }, + { + "text": "duration (s)", + "value": "dtdurations" + }, + { + "text": "duration (hh:mm:ss)", + "value": "dthms" + }, + { + "text": "Timeticks (s/100)", + "value": "timeticks" + } + ], + "text": "time" + }, + { + "submenu": [ + { + "text": "YYYY-MM-DD HH:mm:ss", + "value": "dateTimeAsIso" + }, + { + "text": "DD/MM/YYYY h:mm:ss a", + "value": "dateTimeAsUS" + }, + { + "text": "From Now", + "value": "dateTimeFromNow" + } + ], + "text": "date & time" + }, + { + "submenu": [ + { + "text": "bits", + "value": "bits" + }, + { + "text": "bytes", + "value": "bytes" + }, + { + "text": "kibibytes", + "value": "kbytes" + }, + { + "text": "mebibytes", + "value": "mbytes" + }, + { + "text": "gibibytes", + "value": "gbytes" + } + ], + "text": "data (IEC)" + }, + { + "submenu": [ + { + "text": "bits", + "value": "decbits" + }, + { + "text": "bytes", + "value": "decbytes" + }, + { + "text": "kilobytes", + "value": "deckbytes" + }, + { + "text": "megabytes", + "value": "decmbytes" + }, + { + "text": "gigabytes", + "value": "decgbytes" + } + ], + "text": "data (Metric)" + }, + { + "submenu": [ + { + "text": "packets/sec", + "value": "pps" + }, + { + "text": "bits/sec", + "value": "bps" + }, + { + "text": "bytes/sec", + "value": "Bps" + }, + { + "text": "kilobits/sec", + "value": "Kbits" + }, + { + "text": "kilobytes/sec", + "value": "KBs" + }, + { + "text": "megabits/sec", + "value": "Mbits" + }, + { + "text": "megabytes/sec", + "value": "MBs" + }, + { + "text": "gigabytes/sec", + "value": "GBs" + }, + { + "text": "gigabits/sec", + "value": "Gbits" + } + ], + "text": "data rate" + }, + { + "submenu": [ + { + "text": "hashes/sec", + "value": "Hs" + }, + { + "text": "kilohashes/sec", + "value": "KHs" + }, + { + "text": "megahashes/sec", + "value": "MHs" + }, + { + "text": "gigahashes/sec", + "value": "GHs" + }, + { + "text": "terahashes/sec", + "value": "THs" + }, + { + "text": "petahashes/sec", + "value": "PHs" + }, + { + "text": "exahashes/sec", + "value": "EHs" + } + ], + "text": "hash rate" + }, + { + "submenu": [ + { + "text": "ops/sec (ops)", + "value": "ops" + }, + { + "text": "requests/sec (rps)", + "value": "reqps" + }, + { + "text": "reads/sec (rps)", + "value": "rps" + }, + { + "text": "writes/sec (wps)", + "value": "wps" + }, + { + "text": "I/O ops/sec (iops)", + "value": "iops" + }, + { + "text": "ops/min (opm)", + "value": "opm" + }, + { + "text": "reads/min (rpm)", + "value": "rpm" + }, + { + "text": "writes/min (wpm)", + "value": "wpm" + } + ], + "text": "throughput" + }, + { + "submenu": [ + { + "text": "millimetre (mm)", + "value": "lengthmm" + }, + { + "text": "meter (m)", + "value": "lengthm" + }, + { + "text": "feet (ft)", + "value": "lengthft" + }, + { + "text": "kilometer (km)", + "value": "lengthkm" + }, + { + "text": "mile (mi)", + "value": "lengthmi" + } + ], + "text": "length" + }, + { + "submenu": [ + { + "text": "Square Meters (m²)", + "value": "areaM2" + }, + { + "text": "Square Feet (ft²)", + "value": "areaF2" + }, + { + "text": "Square Miles (mi²)", + "value": "areaMI2" + } + ], + "text": "area" + }, + { + "submenu": [ + { + "text": "milligram (mg)", + "value": "massmg" + }, + { + "text": "gram (g)", + "value": "massg" + }, + { + "text": "kilogram (kg)", + "value": "masskg" + }, + { + "text": "metric ton (t)", + "value": "masst" + } + ], + "text": "mass" + }, + { + "submenu": [ + { + "text": "metres/second (m/s)", + "value": "velocityms" + }, + { + "text": "kilometers/hour (km/h)", + "value": "velocitykmh" + }, + { + "text": "miles/hour (mph)", + "value": "velocitymph" + }, + { + "text": "knot (kn)", + "value": "velocityknot" + } + ], + "text": "velocity" + }, + { + "submenu": [ + { + "text": "millilitre (mL)", + "value": "mlitre" + }, + { + "text": "litre (L)", + "value": "litre" + }, + { + "text": "cubic metre", + "value": "m3" + }, + { + "text": "Normal cubic metre", + "value": "Nm3" + }, + { + "text": "cubic decimetre", + "value": "dm3" + }, + { + "text": "gallons", + "value": "gallons" + } + ], + "text": "volume" + }, + { + "submenu": [ + { + "text": "Watt (W)", + "value": "watt" + }, + { + "text": "Kilowatt (kW)", + "value": "kwatt" + }, + { + "text": "Milliwatt (mW)", + "value": "mwatt" + }, + { + "text": "Watt per square metre (W/m²)", + "value": "Wm2" + }, + { + "text": "Volt-ampere (VA)", + "value": "voltamp" + }, + { + "text": "Kilovolt-ampere (kVA)", + "value": "kvoltamp" + }, + { + "text": "Volt-ampere reactive (var)", + "value": "voltampreact" + }, + { + "text": "Kilovolt-ampere reactive (kvar)", + "value": "kvoltampreact" + }, + { + "text": "Watt-hour (Wh)", + "value": "watth" + }, + { + "text": "Kilowatt-hour (kWh)", + "value": "kwatth" + }, + { + "text": "Kilowatt-min (kWm)", + "value": "kwattm" + }, + { + "text": "Joule (J)", + "value": "joule" + }, + { + "text": "Electron volt (eV)", + "value": "ev" + }, + { + "text": "Ampere (A)", + "value": "amp" + }, + { + "text": "Kiloampere (kA)", + "value": "kamp" + }, + { + "text": "Milliampere (mA)", + "value": "mamp" + }, + { + "text": "Volt (V)", + "value": "volt" + }, + { + "text": "Kilovolt (kV)", + "value": "kvolt" + }, + { + "text": "Millivolt (mV)", + "value": "mvolt" + }, + { + "text": "Decibel-milliwatt (dBm)", + "value": "dBm" + }, + { + "text": "Ohm (Ω)", + "value": "ohm" + }, + { + "text": "Lumens (Lm)", + "value": "lumens" + } + ], + "text": "energy" + }, + { + "submenu": [ + { + "text": "Celsius (°C)", + "value": "celsius" + }, + { + "text": "Farenheit (°F)", + "value": "farenheit" + }, + { + "text": "Kelvin (K)", + "value": "kelvin" + } + ], + "text": "temperature" + }, + { + "submenu": [ + { + "text": "Millibars", + "value": "pressurembar" + }, + { + "text": "Bars", + "value": "pressurebar" + }, + { + "text": "Kilobars", + "value": "pressurekbar" + }, + { + "text": "Hectopascals", + "value": "pressurehpa" + }, + { + "text": "Kilopascals", + "value": "pressurekpa" + }, + { + "text": "Inches of mercury", + "value": "pressurehg" + }, + { + "text": "PSI", + "value": "pressurepsi" + } + ], + "text": "pressure" + }, + { + "submenu": [ + { + "text": "Newton-meters (Nm)", + "value": "forceNm" + }, + { + "text": "Kilonewton-meters (kNm)", + "value": "forcekNm" + }, + { + "text": "Newtons (N)", + "value": "forceN" + }, + { + "text": "Kilonewtons (kN)", + "value": "forcekN" + } + ], + "text": "force" + }, + { + "submenu": [ + { + "text": "Gallons/min (gpm)", + "value": "flowgpm" + }, + { + "text": "Cubic meters/sec (cms)", + "value": "flowcms" + }, + { + "text": "Cubic feet/sec (cfs)", + "value": "flowcfs" + }, + { + "text": "Cubic feet/min (cfm)", + "value": "flowcfm" + }, + { + "text": "Litre/hour", + "value": "litreh" + }, + { + "text": "Litre/min (l/min)", + "value": "flowlpm" + }, + { + "text": "milliLitre/min (mL/min)", + "value": "flowmlpm" + } + ], + "text": "flow" + }, + { + "submenu": [ + { + "text": "Degrees (°)", + "value": "degree" + }, + { + "text": "Radians", + "value": "radian" + }, + { + "text": "Gradian", + "value": "grad" + } + ], + "text": "angle" + }, + { + "submenu": [ + { + "text": "Meters/sec²", + "value": "accMS2" + }, + { + "text": "Feet/sec²", + "value": "accFS2" + }, + { + "text": "G unit", + "value": "accG" + } + ], + "text": "acceleration" + }, + { + "submenu": [ + { + "text": "Becquerel (Bq)", + "value": "radbq" + }, + { + "text": "curie (Ci)", + "value": "radci" + }, + { + "text": "Gray (Gy)", + "value": "radgy" + }, + { + "text": "rad", + "value": "radrad" + }, + { + "text": "Sievert (Sv)", + "value": "radsv" + }, + { + "text": "rem", + "value": "radrem" + }, + { + "text": "Exposure (C/kg)", + "value": "radexpckg" + }, + { + "text": "roentgen (R)", + "value": "radr" + }, + { + "text": "Sievert/hour (Sv/h)", + "value": "radsvh" + } + ], + "text": "radiation" + }, + { + "submenu": [ + { + "text": "parts-per-million (ppm)", + "value": "ppm" + }, + { + "text": "parts-per-billion (ppb)", + "value": "conppb" + }, + { + "text": "nanogram per cubic metre (ng/m³)", + "value": "conngm3" + }, + { + "text": "nanogram per normal cubic metre (ng/Nm³)", + "value": "conngNm3" + }, + { + "text": "microgram per cubic metre (μg/m³)", + "value": "conμgm3" + }, + { + "text": "microgram per normal cubic metre (μg/Nm³)", + "value": "conμgNm3" + }, + { + "text": "milligram per cubic metre (mg/m³)", + "value": "conmgm3" + }, + { + "text": "milligram per normal cubic metre (mg/Nm³)", + "value": "conmgNm3" + }, + { + "text": "gram per cubic metre (g/m³)", + "value": "congm3" + }, + { + "text": "gram per normal cubic metre (g/Nm³)", + "value": "congNm3" + } + ], + "text": "concentration" + } + ] + }, + { + "animationModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "d3DivId": "d3_svg_2", + "datasource": "gdev-testdata", + "decimals": 2, + "displayModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "fontSizes": [ + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 22, + 24, + 26, + 28, + 30, + 32, + 34, + 36, + 38, + 40, + 42, + 44, + 46, + 48, + 50, + 52, + 54, + 56, + 58, + 60, + 62, + 64, + 66, + 68, + 70 + ], + "fontTypes": [ + "Open Sans", + "Arial", + "Avant Garde", + "Bookman", + "Consolas", + "Courier", + "Courier New", + "Futura", + "Garamond", + "Helvetica", + "Palatino", + "Times", + "Times New Roman", + "Verdana" + ], + "format": "none", + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 2, + "links": [], + "notcolors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "operatorName": "avg", + "operatorOptions": [ + { + "text": "Average", + "value": "avg" + }, + { + "text": "Count", + "value": "count" + }, + { + "text": "Current", + "value": "current" + }, + { + "text": "Delta", + "value": "delta" + }, + { + "text": "Difference", + "value": "diff" + }, + { + "text": "First", + "value": "first" + }, + { + "text": "Log Min", + "value": "logmin" + }, + { + "text": "Max", + "value": "max" + }, + { + "text": "Min", + "value": "min" + }, + { + "text": "Name", + "value": "name" + }, + { + "text": "Time of Last Point", + "value": "last_time" + }, + { + "text": "Time Step", + "value": "time_step" + }, + { + "text": "Total", + "value": "total" + } + ], + "polystat": { + "animationSpeed": 2500, + "columnAutoSize": true, + "columns": 1, + "defaultClickThrough": "", + "defaultClickThroughSanitize": true, + "displayLimit": 100, + "fontAutoScale": true, + "fontSize": 12, + "globalDisplayMode": "all", + "globalOperatorName": "avg", + "gradientEnabled": true, + "hexagonSortByDirection": "asc", + "hexagonSortByField": "name", + "maxMetrics": 0, + "polygonBorderColor": "black", + "polygonBorderSize": 2, + "radius": "", + "radiusAutoSize": true, + "rowAutoSize": true, + "rows": 1, + "shape": "hexagon_pointed_top", + "tooltipDisplayMode": "all", + "tooltipDisplayTextTriggeredEmpty": "OK", + "tooltipFontSize": 12, + "tooltipFontType": "Open Sans", + "tooltipPrimarySortDirection": "desc", + "tooltipPrimarySortField": "thresholdLevel", + "tooltipSecondarySortDirection": "desc", + "tooltipSecondarySortField": "value", + "tooltipTimestampEnabled": true + }, + "savedComposites": [], + "savedOverrides": [], + "shapes": [ + { + "text": "Hexagon Pointed Top", + "value": "hexagon_pointed_top" + }, + { + "text": "Hexagon Flat Top", + "value": "hexagon_flat_top" + }, + { + "text": "Circle", + "value": "circle" + }, + { + "text": "Cross", + "value": "cross" + }, + { + "text": "Diamond", + "value": "diamond" + }, + { + "text": "Square", + "value": "square" + }, + { + "text": "Star", + "value": "star" + }, + { + "text": "Triangle", + "value": "triangle" + }, + { + "text": "Wye", + "value": "wye" + } + ], + "sortDirections": [ + { + "text": "Ascending", + "value": "asc" + }, + { + "text": "Descending", + "value": "desc" + } + ], + "sortFields": [ + { + "text": "Name", + "value": "name" + }, + { + "text": "Threshold Level", + "value": "thresholdLevel" + }, + { + "text": "Value", + "value": "value" + } + ], + "svgContainer": {}, + "targets": [ + { + "alias": "Sensor-A", + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + }, + { + "alias": "Sensor-B", + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "3433,23432,55" + }, + { + "alias": "Sensor-C", + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "1,2,3,4,5,6" + }, + { + "alias": "Sensor-E", + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + ], + "thresholdStates": [ + { + "text": "ok", + "value": 0 + }, + { + "text": "warning", + "value": 1 + }, + { + "text": "critical", + "value": 2 + }, + { + "text": "custom", + "value": 3 + } + ], + "title": "No Value in Sensor-C Bug", + "type": "grafana-polystat-panel", + "unitFormats": [ + { + "submenu": [ + { + "text": "none", + "value": "none" + }, + { + "text": "short", + "value": "short" + }, + { + "text": "percent (0-100)", + "value": "percent" + }, + { + "text": "percent (0.0-1.0)", + "value": "percentunit" + }, + { + "text": "Humidity (%H)", + "value": "humidity" + }, + { + "text": "decibel", + "value": "dB" + }, + { + "text": "hexadecimal (0x)", + "value": "hex0x" + }, + { + "text": "hexadecimal", + "value": "hex" + }, + { + "text": "scientific notation", + "value": "sci" + }, + { + "text": "locale format", + "value": "locale" + } + ], + "text": "none" + }, + { + "submenu": [ + { + "text": "Dollars ($)", + "value": "currencyUSD" + }, + { + "text": "Pounds (£)", + "value": "currencyGBP" + }, + { + "text": "Euro (€)", + "value": "currencyEUR" + }, + { + "text": "Yen (¥)", + "value": "currencyJPY" + }, + { + "text": "Rubles (₽)", + "value": "currencyRUB" + }, + { + "text": "Hryvnias (₴)", + "value": "currencyUAH" + }, + { + "text": "Real (R$)", + "value": "currencyBRL" + }, + { + "text": "Danish Krone (kr)", + "value": "currencyDKK" + }, + { + "text": "Icelandic Króna (kr)", + "value": "currencyISK" + }, + { + "text": "Norwegian Krone (kr)", + "value": "currencyNOK" + }, + { + "text": "Swedish Krona (kr)", + "value": "currencySEK" + }, + { + "text": "Czech koruna (czk)", + "value": "currencyCZK" + }, + { + "text": "Swiss franc (CHF)", + "value": "currencyCHF" + }, + { + "text": "Polish Złoty (PLN)", + "value": "currencyPLN" + }, + { + "text": "Bitcoin (฿)", + "value": "currencyBTC" + } + ], + "text": "currency" + }, + { + "submenu": [ + { + "text": "Hertz (1/s)", + "value": "hertz" + }, + { + "text": "nanoseconds (ns)", + "value": "ns" + }, + { + "text": "microseconds (µs)", + "value": "µs" + }, + { + "text": "milliseconds (ms)", + "value": "ms" + }, + { + "text": "seconds (s)", + "value": "s" + }, + { + "text": "minutes (m)", + "value": "m" + }, + { + "text": "hours (h)", + "value": "h" + }, + { + "text": "days (d)", + "value": "d" + }, + { + "text": "duration (ms)", + "value": "dtdurationms" + }, + { + "text": "duration (s)", + "value": "dtdurations" + }, + { + "text": "duration (hh:mm:ss)", + "value": "dthms" + }, + { + "text": "Timeticks (s/100)", + "value": "timeticks" + } + ], + "text": "time" + }, + { + "submenu": [ + { + "text": "YYYY-MM-DD HH:mm:ss", + "value": "dateTimeAsIso" + }, + { + "text": "DD/MM/YYYY h:mm:ss a", + "value": "dateTimeAsUS" + }, + { + "text": "From Now", + "value": "dateTimeFromNow" + } + ], + "text": "date & time" + }, + { + "submenu": [ + { + "text": "bits", + "value": "bits" + }, + { + "text": "bytes", + "value": "bytes" + }, + { + "text": "kibibytes", + "value": "kbytes" + }, + { + "text": "mebibytes", + "value": "mbytes" + }, + { + "text": "gibibytes", + "value": "gbytes" + } + ], + "text": "data (IEC)" + }, + { + "submenu": [ + { + "text": "bits", + "value": "decbits" + }, + { + "text": "bytes", + "value": "decbytes" + }, + { + "text": "kilobytes", + "value": "deckbytes" + }, + { + "text": "megabytes", + "value": "decmbytes" + }, + { + "text": "gigabytes", + "value": "decgbytes" + } + ], + "text": "data (Metric)" + }, + { + "submenu": [ + { + "text": "packets/sec", + "value": "pps" + }, + { + "text": "bits/sec", + "value": "bps" + }, + { + "text": "bytes/sec", + "value": "Bps" + }, + { + "text": "kilobits/sec", + "value": "Kbits" + }, + { + "text": "kilobytes/sec", + "value": "KBs" + }, + { + "text": "megabits/sec", + "value": "Mbits" + }, + { + "text": "megabytes/sec", + "value": "MBs" + }, + { + "text": "gigabytes/sec", + "value": "GBs" + }, + { + "text": "gigabits/sec", + "value": "Gbits" + } + ], + "text": "data rate" + }, + { + "submenu": [ + { + "text": "hashes/sec", + "value": "Hs" + }, + { + "text": "kilohashes/sec", + "value": "KHs" + }, + { + "text": "megahashes/sec", + "value": "MHs" + }, + { + "text": "gigahashes/sec", + "value": "GHs" + }, + { + "text": "terahashes/sec", + "value": "THs" + }, + { + "text": "petahashes/sec", + "value": "PHs" + }, + { + "text": "exahashes/sec", + "value": "EHs" + } + ], + "text": "hash rate" + }, + { + "submenu": [ + { + "text": "ops/sec (ops)", + "value": "ops" + }, + { + "text": "requests/sec (rps)", + "value": "reqps" + }, + { + "text": "reads/sec (rps)", + "value": "rps" + }, + { + "text": "writes/sec (wps)", + "value": "wps" + }, + { + "text": "I/O ops/sec (iops)", + "value": "iops" + }, + { + "text": "ops/min (opm)", + "value": "opm" + }, + { + "text": "reads/min (rpm)", + "value": "rpm" + }, + { + "text": "writes/min (wpm)", + "value": "wpm" + } + ], + "text": "throughput" + }, + { + "submenu": [ + { + "text": "millimetre (mm)", + "value": "lengthmm" + }, + { + "text": "meter (m)", + "value": "lengthm" + }, + { + "text": "feet (ft)", + "value": "lengthft" + }, + { + "text": "kilometer (km)", + "value": "lengthkm" + }, + { + "text": "mile (mi)", + "value": "lengthmi" + } + ], + "text": "length" + }, + { + "submenu": [ + { + "text": "Square Meters (m²)", + "value": "areaM2" + }, + { + "text": "Square Feet (ft²)", + "value": "areaF2" + }, + { + "text": "Square Miles (mi²)", + "value": "areaMI2" + } + ], + "text": "area" + }, + { + "submenu": [ + { + "text": "milligram (mg)", + "value": "massmg" + }, + { + "text": "gram (g)", + "value": "massg" + }, + { + "text": "kilogram (kg)", + "value": "masskg" + }, + { + "text": "metric ton (t)", + "value": "masst" + } + ], + "text": "mass" + }, + { + "submenu": [ + { + "text": "metres/second (m/s)", + "value": "velocityms" + }, + { + "text": "kilometers/hour (km/h)", + "value": "velocitykmh" + }, + { + "text": "miles/hour (mph)", + "value": "velocitymph" + }, + { + "text": "knot (kn)", + "value": "velocityknot" + } + ], + "text": "velocity" + }, + { + "submenu": [ + { + "text": "millilitre (mL)", + "value": "mlitre" + }, + { + "text": "litre (L)", + "value": "litre" + }, + { + "text": "cubic metre", + "value": "m3" + }, + { + "text": "Normal cubic metre", + "value": "Nm3" + }, + { + "text": "cubic decimetre", + "value": "dm3" + }, + { + "text": "gallons", + "value": "gallons" + } + ], + "text": "volume" + }, + { + "submenu": [ + { + "text": "Watt (W)", + "value": "watt" + }, + { + "text": "Kilowatt (kW)", + "value": "kwatt" + }, + { + "text": "Milliwatt (mW)", + "value": "mwatt" + }, + { + "text": "Watt per square metre (W/m²)", + "value": "Wm2" + }, + { + "text": "Volt-ampere (VA)", + "value": "voltamp" + }, + { + "text": "Kilovolt-ampere (kVA)", + "value": "kvoltamp" + }, + { + "text": "Volt-ampere reactive (var)", + "value": "voltampreact" + }, + { + "text": "Kilovolt-ampere reactive (kvar)", + "value": "kvoltampreact" + }, + { + "text": "Watt-hour (Wh)", + "value": "watth" + }, + { + "text": "Kilowatt-hour (kWh)", + "value": "kwatth" + }, + { + "text": "Kilowatt-min (kWm)", + "value": "kwattm" + }, + { + "text": "Joule (J)", + "value": "joule" + }, + { + "text": "Electron volt (eV)", + "value": "ev" + }, + { + "text": "Ampere (A)", + "value": "amp" + }, + { + "text": "Kiloampere (kA)", + "value": "kamp" + }, + { + "text": "Milliampere (mA)", + "value": "mamp" + }, + { + "text": "Volt (V)", + "value": "volt" + }, + { + "text": "Kilovolt (kV)", + "value": "kvolt" + }, + { + "text": "Millivolt (mV)", + "value": "mvolt" + }, + { + "text": "Decibel-milliwatt (dBm)", + "value": "dBm" + }, + { + "text": "Ohm (Ω)", + "value": "ohm" + }, + { + "text": "Lumens (Lm)", + "value": "lumens" + } + ], + "text": "energy" + }, + { + "submenu": [ + { + "text": "Celsius (°C)", + "value": "celsius" + }, + { + "text": "Farenheit (°F)", + "value": "farenheit" + }, + { + "text": "Kelvin (K)", + "value": "kelvin" + } + ], + "text": "temperature" + }, + { + "submenu": [ + { + "text": "Millibars", + "value": "pressurembar" + }, + { + "text": "Bars", + "value": "pressurebar" + }, + { + "text": "Kilobars", + "value": "pressurekbar" + }, + { + "text": "Hectopascals", + "value": "pressurehpa" + }, + { + "text": "Kilopascals", + "value": "pressurekpa" + }, + { + "text": "Inches of mercury", + "value": "pressurehg" + }, + { + "text": "PSI", + "value": "pressurepsi" + } + ], + "text": "pressure" + }, + { + "submenu": [ + { + "text": "Newton-meters (Nm)", + "value": "forceNm" + }, + { + "text": "Kilonewton-meters (kNm)", + "value": "forcekNm" + }, + { + "text": "Newtons (N)", + "value": "forceN" + }, + { + "text": "Kilonewtons (kN)", + "value": "forcekN" + } + ], + "text": "force" + }, + { + "submenu": [ + { + "text": "Gallons/min (gpm)", + "value": "flowgpm" + }, + { + "text": "Cubic meters/sec (cms)", + "value": "flowcms" + }, + { + "text": "Cubic feet/sec (cfs)", + "value": "flowcfs" + }, + { + "text": "Cubic feet/min (cfm)", + "value": "flowcfm" + }, + { + "text": "Litre/hour", + "value": "litreh" + }, + { + "text": "Litre/min (l/min)", + "value": "flowlpm" + }, + { + "text": "milliLitre/min (mL/min)", + "value": "flowmlpm" + } + ], + "text": "flow" + }, + { + "submenu": [ + { + "text": "Degrees (°)", + "value": "degree" + }, + { + "text": "Radians", + "value": "radian" + }, + { + "text": "Gradian", + "value": "grad" + } + ], + "text": "angle" + }, + { + "submenu": [ + { + "text": "Meters/sec²", + "value": "accMS2" + }, + { + "text": "Feet/sec²", + "value": "accFS2" + }, + { + "text": "G unit", + "value": "accG" + } + ], + "text": "acceleration" + }, + { + "submenu": [ + { + "text": "Becquerel (Bq)", + "value": "radbq" + }, + { + "text": "curie (Ci)", + "value": "radci" + }, + { + "text": "Gray (Gy)", + "value": "radgy" + }, + { + "text": "rad", + "value": "radrad" + }, + { + "text": "Sievert (Sv)", + "value": "radsv" + }, + { + "text": "rem", + "value": "radrem" + }, + { + "text": "Exposure (C/kg)", + "value": "radexpckg" + }, + { + "text": "roentgen (R)", + "value": "radr" + }, + { + "text": "Sievert/hour (Sv/h)", + "value": "radsvh" + } + ], + "text": "radiation" + }, + { + "submenu": [ + { + "text": "parts-per-million (ppm)", + "value": "ppm" + }, + { + "text": "parts-per-billion (ppb)", + "value": "conppb" + }, + { + "text": "nanogram per cubic metre (ng/m³)", + "value": "conngm3" + }, + { + "text": "nanogram per normal cubic metre (ng/Nm³)", + "value": "conngNm3" + }, + { + "text": "microgram per cubic metre (μg/m³)", + "value": "conμgm3" + }, + { + "text": "microgram per normal cubic metre (μg/Nm³)", + "value": "conμgNm3" + }, + { + "text": "milligram per cubic metre (mg/m³)", + "value": "conmgm3" + }, + { + "text": "milligram per normal cubic metre (mg/Nm³)", + "value": "conmgNm3" + }, + { + "text": "gram per cubic metre (g/m³)", + "value": "congm3" + }, + { + "text": "gram per normal cubic metre (g/Nm³)", + "value": "congNm3" + } + ], + "text": "concentration" + } + ] + } + ], + "schemaVersion": 16, + "style": "dark", + "tags": [ + "panel-test", + "gdev" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "Panel Tests - Polystat", + "version": 5 +} diff --git a/devenv/dev-dashboards/datasource_tests_elasticsearch_compare.json b/devenv/dev-dashboards/datasource_tests_elasticsearch_compare.json index f07c6f46332..c631ea0a151 100644 --- a/devenv/dev-dashboards/datasource_tests_elasticsearch_compare.json +++ b/devenv/dev-dashboards/datasource_tests_elasticsearch_compare.json @@ -17,7 +17,7 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "iteration": 1542304484522, + "iteration": 1545263815779, "links": [ { "icon": "external link", @@ -66,6 +66,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -168,6 +169,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -270,6 +272,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -372,6 +375,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -474,6 +478,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -576,6 +581,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -2249,6 +2255,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -2366,6 +2373,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -2483,6 +2491,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -2600,6 +2609,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -2717,6 +2727,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -2834,6 +2845,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -2951,6 +2963,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -3068,6 +3081,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -3185,6 +3199,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -3302,6 +3317,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -3419,6 +3435,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -3536,6 +3553,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -3667,6 +3685,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -3780,6 +3799,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -3893,6 +3913,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -4006,6 +4027,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -4119,6 +4141,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -4232,6 +4255,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -4345,6 +4369,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -4458,6 +4483,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -4571,6 +4597,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -4684,6 +4711,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -4797,6 +4825,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -4910,6 +4939,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -5008,6 +5038,512 @@ "x": 0, "y": 4 }, + "id": 60, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$version_one", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 5 + }, + "id": 63, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "stack": false, + "steppedLine": false, + "targets": [ + { + "bucketAggs": [ + { + "field": "@timestamp", + "id": "2", + "settings": { + "interval": "auto", + "min_doc_count": 0, + "trimEdges": 0 + }, + "type": "date_histogram" + } + ], + "metrics": [ + { + "field": "select field", + "hide": true, + "id": "1", + "type": "count" + }, + { + "field": "select field", + "id": "3", + "meta": {}, + "pipelineVariables": [ + { + "name": "var1", + "pipelineAgg": "1" + } + ], + "settings": { + "script": "params.var1 * 1000" + }, + "type": "bucket_script" + } + ], + "refId": "A", + "timeField": "@timestamp" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "count * 1000 (version one) - interval auto", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$version_two", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 5 + }, + "id": 64, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "stack": false, + "steppedLine": false, + "targets": [ + { + "bucketAggs": [ + { + "field": "@timestamp", + "id": "2", + "settings": { + "interval": "auto", + "min_doc_count": 0, + "trimEdges": 0 + }, + "type": "date_histogram" + } + ], + "metrics": [ + { + "field": "select field", + "hide": true, + "id": "1", + "type": "count" + }, + { + "field": "select field", + "id": "3", + "meta": {}, + "pipelineVariables": [ + { + "name": "var1", + "pipelineAgg": "1" + } + ], + "settings": { + "script": "params.var1 * 1000" + }, + "type": "bucket_script" + } + ], + "refId": "A", + "timeField": "@timestamp" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "count * 1000 (version two) - interval auto", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$version_one", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 13 + }, + "id": 65, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "stack": false, + "steppedLine": false, + "targets": [ + { + "bucketAggs": [ + { + "field": "@timestamp", + "id": "2", + "settings": { + "interval": "auto", + "min_doc_count": 0, + "trimEdges": 0 + }, + "type": "date_histogram" + } + ], + "metrics": [ + { + "field": "select field", + "hide": true, + "id": "1", + "type": "count" + }, + { + "field": "@value", + "hide": true, + "id": "3", + "meta": {}, + "settings": {}, + "type": "avg" + }, + { + "field": "select field", + "id": "4", + "meta": {}, + "pipelineVariables": [ + { + "name": "var1", + "pipelineAgg": "1" + }, + { + "name": "var2", + "pipelineAgg": "3" + } + ], + "settings": { + "script": "params.var1 * params.var2" + }, + "type": "bucket_script" + } + ], + "refId": "A", + "timeField": "@timestamp" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "count * avg (version one) - interval auto", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$version_two", + "fill": 1, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 13 + }, + "id": 66, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "stack": false, + "steppedLine": false, + "targets": [ + { + "bucketAggs": [ + { + "field": "@timestamp", + "id": "2", + "settings": { + "interval": "auto", + "min_doc_count": 0, + "trimEdges": 0 + }, + "type": "date_histogram" + } + ], + "metrics": [ + { + "field": "select field", + "hide": true, + "id": "1", + "type": "count" + }, + { + "field": "@value", + "hide": true, + "id": "3", + "meta": {}, + "settings": {}, + "type": "avg" + }, + { + "field": "select field", + "id": "4", + "meta": {}, + "pipelineVariables": [ + { + "name": "var1", + "pipelineAgg": "1" + }, + { + "name": "var2", + "pipelineAgg": "3" + } + ], + "settings": { + "script": "params.var1 * params.var2" + }, + "type": "bucket_script" + } + ], + "refId": "A", + "timeField": "@timestamp" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "count * avg (version two) - interval auto", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "title": "Basic date histogram with bucket script aggregation", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, "id": 54, "panels": [ { @@ -5042,6 +5578,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -5193,6 +5730,7 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "paceLength": 10, "percentage": false, "pointradius": 5, "points": false, @@ -5328,8 +5866,8 @@ "list": [ { "current": { - "text": "gdev-elasticsearch-v2-metrics", - "value": "gdev-elasticsearch-v2-metrics" + "text": "gdev-elasticsearch-v5-metrics", + "value": "gdev-elasticsearch-v5-metrics" }, "hide": 0, "label": "Version One", @@ -5343,8 +5881,8 @@ }, { "current": { - "text": "gdev-elasticsearch-v5-metrics", - "value": "gdev-elasticsearch-v5-metrics" + "text": "gdev-elasticsearch-v6-metrics", + "value": "gdev-elasticsearch-v6-metrics" }, "hide": 0, "label": "Version Two", @@ -5359,7 +5897,7 @@ ] }, "time": { - "from": "now-3h", + "from": "now-1h", "to": "now" }, "timepicker": { @@ -5390,5 +5928,5 @@ "timezone": "", "title": "Datasource tests - Elasticsearch comparison", "uid": "fuFWehBmk", - "version": 10 + "version": 4 } \ No newline at end of file diff --git a/devenv/docker/blocks/influxdb/influxdb.conf b/devenv/docker/blocks/influxdb/influxdb.conf index c0331ce7449..120739dd896 100644 --- a/devenv/docker/blocks/influxdb/influxdb.conf +++ b/devenv/docker/blocks/influxdb/influxdb.conf @@ -69,6 +69,7 @@ reporting-disabled = false unix-socket-enabled = false # enable http service over unix domain socket # bind-socket = "/var/run/influxdb.sock" + flux-enabled = true [subscriber] enabled = true diff --git a/docs/sources/features/explore/index.md b/docs/sources/features/explore/index.md index 438943b3243..0ba3d2f7d44 100644 --- a/docs/sources/features/explore/index.md +++ b/docs/sources/features/explore/index.md @@ -1,5 +1,6 @@ +++ title = "Explore" +keywords = ["explore", "loki", "logs"] type = "docs" [menu.docs] name = "Explore" @@ -8,7 +9,11 @@ parent = "features" weight = 5 +++ -# Introduction +# Explore + +> Explore is only available in Grafana 6.0 and above. + +## Introduction One of the major new features of Grafana 6.0 is the new query-focused Explore workflow for troubleshooting and/or for data exploration. diff --git a/docs/sources/http_api/admin.md b/docs/sources/http_api/admin.md index 2d4be21bb78..a27fd2aac14 100644 --- a/docs/sources/http_api/admin.md +++ b/docs/sources/http_api/admin.md @@ -285,7 +285,7 @@ Content-Type: application/json HTTP/1.1 200 Content-Type: application/json -{message: "User permissions updated"} +{"message": "User permissions updated"} ``` ## Delete global User @@ -308,7 +308,7 @@ Content-Type: application/json HTTP/1.1 200 Content-Type: application/json -{message: "User deleted"} +{"message": "User deleted"} ``` ## Pause all alerts @@ -339,5 +339,5 @@ JSON Body schema: HTTP/1.1 200 Content-Type: application/json -{state: "new state", message: "alerts pause/un paused", "alertsAffected": 100} +{"state": "new state", "message": "alerts pause/un paused", "alertsAffected": 100} ``` diff --git a/docs/sources/http_api/folder_permissions.md b/docs/sources/http_api/folder_permissions.md index 284ab70866f..39cbbc3720a 100644 --- a/docs/sources/http_api/folder_permissions.md +++ b/docs/sources/http_api/folder_permissions.md @@ -105,7 +105,7 @@ POST /api/folders/nErXDvCkzz/permissions Accept: application/json Content-Type: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk - +{ "items": [ { "role": "Viewer", diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 7ed44572533..d26af5277a1 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -34,32 +34,29 @@ sudo dpkg -i grafana__amd64.deb Example: ```bash -wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.4_amd64.deb +wget https://dl.grafana.com/oss/release/grafana_5.4.2_amd64.deb sudo apt-get install -y adduser libfontconfig -sudo dpkg -i grafana_5.1.4_amd64.deb +sudo dpkg -i grafana_5.4.2_amd64.deb ``` ## APT Repository -Add the following line to your `/etc/apt/sources.list` file. +Create a file `/etc/apt/sources.list.d/grafana.list` and add the following to it. ```bash -deb https://packagecloud.io/grafana/stable/debian/ stretch main +deb https://packages.grafana.com/oss/deb stable main ``` -Use the above line even if you are on Ubuntu or another Debian version. -There is also a testing repository if you want beta or release -candidates. +There is a separate repository if you want beta releases. ```bash -deb https://packagecloud.io/grafana/testing/debian/ stretch main +deb https://packages.grafana.com/oss/deb beta main ``` -Then add the [Package Cloud](https://packagecloud.io/grafana) key. This -allows you to install signed packages. +Use the above line even if you are on Ubuntu or another Debian version. Then add our gpg key. This allows you to install signed packages. ```bash -curl https://packagecloud.io/gpg.key | sudo apt-key add - +curl https://packages.grafana.com/gpg.key | sudo apt-key add - ``` Update your Apt repositories and install Grafana diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 5bf3b7ed745..156f703e164 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -32,7 +32,7 @@ $ sudo yum install Example: ```bash -$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm +$ sudo yum install https://dl.grafana.com/oss/release/grafana-5.4.2-1.x86_64.rpm ``` Or install manually using `rpm`. First execute @@ -44,7 +44,7 @@ $ wget Example: ```bash -$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm +$ wget https://dl.grafana.com/oss/release/grafana-5.4.2-1.x86_64.rpm ``` ### On CentOS / Fedora / Redhat: @@ -67,19 +67,27 @@ Add the following to a new file at `/etc/yum.repos.d/grafana.repo` ```bash [grafana] name=grafana -baseurl=https://packagecloud.io/grafana/stable/el/7/$basearch +baseurl=https://packages.grafana.com/oss/rpm repo_gpgcheck=1 enabled=1 gpgcheck=1 -gpgkey=https://packagecloud.io/gpg.key https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana +gpgkey=https://packages.grafana.com/gpg.key sslverify=1 sslcacert=/etc/pki/tls/certs/ca-bundle.crt ``` -There is also a testing repository if you want beta or release candidates. +There is a separate repository if you want beta releases. ```bash -baseurl=https://packagecloud.io/grafana/testing/el/7/$basearch +[grafana] +name=grafana +baseurl=https://packages.grafana.com/oss/rpm-beta +repo_gpgcheck=1 +enabled=1 +gpgcheck=1 +gpgkey=https://packages.grafana.com/gpg.key +sslverify=1 +sslcacert=/etc/pki/tls/certs/ca-bundle.crt ``` Then install Grafana via the `yum` command. @@ -91,7 +99,7 @@ $ sudo yum install grafana ### RPM GPG Key The RPMs are signed, you can verify the signature with this [public GPG -key](https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana). +key](https://packages.grafana.com/gpg.key). ## Package details diff --git a/docs/sources/reference/dashboard.md b/docs/sources/reference/dashboard.md index 6be12600da5..3d96923bc72 100644 --- a/docs/sources/reference/dashboard.md +++ b/docs/sources/reference/dashboard.md @@ -51,7 +51,7 @@ When a user creates a new dashboard, a new dashboard JSON object is initialized "list": [] }, "refresh": "5s", - "schemaVersion": 16, + "schemaVersion": 17, "version": 0, "links": [] } diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index f20cc0ccfc9..71ce6bdd2ae 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -292,9 +292,11 @@ The `direction` controls how the panels will be arranged. By choosing `horizontal` the panels will be arranged side-by-side. Grafana will automatically adjust the width of each repeated panel so that the whole row is filled. Currently, you cannot mix other panels on a row with a repeated -panel. Each panel will never be smaller that the provided `Min width` if you have many selected values. +panel. -By choosing `vertical` the panels will be arranged from top to bottom in a column. The `Min width` doesn't have any effect in this case. The width of the repeated panels will be the same as of the first panel (the original template) being repeated. +Set `Max per row` to tell grafana how many panels per row you want at most. It defaults to *4* if you don't set anything. + +By choosing `vertical` the panels will be arranged from top to bottom in a column. The width of the repeated panels will be the same as of the first panel (the original template) being repeated. Only make changes to the first panel (the original template). To have the changes take effect on all panels you need to trigger a dynamic dashboard re-build. You can do this by either changing the variable value (that is the basis for the repeat) or reload the dashboard. diff --git a/latest.json b/latest.json index 21a3f38e1af..dc83446f588 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "5.4.2", - "testing": "5.4.2" + "stable": "5.4.3", + "testing": "5.4.3" } diff --git a/package.json b/package.json index eefe2cbbe53..470101ff0c4 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "@types/jquery": "^1.10.35", "@types/node": "^8.0.31", "@types/react": "^16.7.6", - "@types/react-custom-scrollbars": "^4.0.5", "@types/react-dom": "^16.0.9", "@types/react-select": "^2.0.4", "angular-mocks": "1.6.6", @@ -65,6 +64,7 @@ "html-webpack-plugin": "^3.2.0", "husky": "^0.14.3", "jest": "^23.6.0", + "jest-date-mock": "^1.0.6", "lint-staged": "^6.0.0", "load-grunt-tasks": "3.5.2", "mini-css-extract-plugin": "^0.4.0", @@ -72,8 +72,8 @@ "ng-annotate-loader": "^0.6.1", "ng-annotate-webpack-plugin": "^0.3.0", "ngtemplate-loader": "^2.0.1", - "npm": "^5.4.2", "node-sass": "^4.11.0", + "npm": "^5.4.2", "optimize-css-assets-webpack-plugin": "^4.0.2", "phantomjs-prebuilt": "^2.1.15", "postcss-browser-reporter": "^0.5.0", @@ -167,7 +167,6 @@ "prop-types": "^15.6.2", "rc-cascader": "^0.14.0", "react": "^16.6.3", - "react-custom-scrollbars": "^4.2.1", "react-dom": "^16.6.3", "react-grid-layout": "0.16.6", "react-highlight-words": "0.11.0", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 2fb210e3b46..91695dc5647 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -11,23 +11,34 @@ "license": "ISC", "dependencies": { "@torkelo/react-select": "2.1.1", + "@types/react-test-renderer": "^16.0.3", + "@types/react-transition-group": "^2.0.15", "classnames": "^2.2.5", "jquery": "^3.2.1", "lodash": "^4.17.10", "moment": "^2.22.2", "react": "^16.6.3", + "react-custom-scrollbars": "^4.2.1", "react-dom": "^16.6.3", "react-highlight-words": "0.11.0", "react-popper": "^1.3.0", "react-transition-group": "^2.2.1", - "react-virtualized": "^9.21.0" + "react-virtualized": "^9.21.0", + "tether": "^1.4.0", + "tether-drop": "https://github.com/torkelo/drop/tarball/master", + "tinycolor2": "^1.4.1" }, "devDependencies": { + "@types/classnames": "^2.2.6", "@types/jest": "^23.3.2", + "@types/jquery": "^1.10.35", "@types/lodash": "^4.14.119", "@types/react": "^16.7.6", - "@types/classnames": "^2.2.6", - "@types/jquery": "^1.10.35", + "@types/react-custom-scrollbars": "^4.0.5", + "@types/react-test-renderer": "^16.0.3", + "@types/tether-drop": "^1.4.8", + "@types/tinycolor2": "^1.4.1", + "react-test-renderer": "^16.7.0", "typescript": "^3.2.2" } } diff --git a/public/app/core/specs/ColorPalette.test.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPalette.test.tsx similarity index 80% rename from public/app/core/specs/ColorPalette.test.tsx rename to packages/grafana-ui/src/components/ColorPicker/ColorPalette.test.tsx index fb1124aa975..0714180de54 100644 --- a/public/app/core/specs/ColorPalette.test.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPalette.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import renderer from 'react-test-renderer'; -import { ColorPalette } from '../components/colorpicker/ColorPalette'; +import { ColorPalette } from './ColorPalette'; describe('CollorPalette', () => { it('renders correctly', () => { diff --git a/public/app/core/components/colorpicker/ColorPalette.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPalette.tsx similarity index 90% rename from public/app/core/components/colorpicker/ColorPalette.tsx rename to packages/grafana-ui/src/components/ColorPicker/ColorPalette.tsx index edb2629d16d..03ed9949361 100644 --- a/public/app/core/components/colorpicker/ColorPalette.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPalette.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { sortedColors } from 'app/core/utils/colors'; +import { sortedColors } from '../../utils'; export interface Props { color: string; @@ -9,13 +9,13 @@ export interface Props { export class ColorPalette extends React.Component { paletteColors: string[]; - constructor(props) { + constructor(props: Props) { super(props); this.paletteColors = sortedColors; this.onColorSelect = this.onColorSelect.bind(this); } - onColorSelect(color) { + onColorSelect(color: string) { return () => { this.props.onColorSelect(color); }; diff --git a/public/app/core/components/colorpicker/ColorPicker.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx similarity index 83% rename from public/app/core/components/colorpicker/ColorPicker.tsx rename to packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx index 9541001b0a8..485aa5f03d3 100644 --- a/public/app/core/components/colorpicker/ColorPicker.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx @@ -2,7 +2,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import Drop from 'tether-drop'; import { ColorPickerPopover } from './ColorPickerPopover'; -import { react2AngularDirective } from 'app/core/utils/react2angular'; export interface Props { color: string; @@ -10,7 +9,7 @@ export interface Props { } export class ColorPicker extends React.Component { - pickerElem: HTMLElement; + pickerElem: HTMLElement | null; colorPickerDrop: any; openColorPicker = () => { @@ -20,7 +19,7 @@ export class ColorPicker extends React.Component { ReactDOM.render(dropContent, dropContentElem); const drop = new Drop({ - target: this.pickerElem, + target: this.pickerElem as Element, content: dropContentElem, position: 'top center', classes: 'drop-popover', @@ -28,6 +27,7 @@ export class ColorPicker extends React.Component { hoverCloseDelay: 200, tetherOptions: { constraints: [{ to: 'scrollParent', attachment: 'none both' }], + attachment: 'bottom center', }, }); @@ -45,7 +45,7 @@ export class ColorPicker extends React.Component { }, 100); }; - onColorSelect = color => { + onColorSelect = (color: string) => { this.props.onChange(color); }; @@ -59,8 +59,3 @@ export class ColorPicker extends React.Component { ); } } - -react2AngularDirective('colorPicker', ColorPicker, [ - 'color', - ['onChange', { watchDepth: 'reference', wrapApply: true }], -]); diff --git a/public/app/core/components/colorpicker/ColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx similarity index 83% rename from public/app/core/components/colorpicker/ColorPickerPopover.tsx rename to packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx index c42bcfa1d06..e8305c99319 100644 --- a/public/app/core/components/colorpicker/ColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx @@ -14,7 +14,7 @@ export interface Props { export class ColorPickerPopover extends React.Component { pickerNavElem: any; - constructor(props) { + constructor(props: Props) { super(props); this.state = { tab: 'palette', @@ -23,60 +23,51 @@ export class ColorPickerPopover extends React.Component { }; } - setPickerNavElem(elem) { + setPickerNavElem(elem: any) { this.pickerNavElem = $(elem); } - setColor(color) { + setColor(color: string) { const newColor = tinycolor(color); if (newColor.isValid()) { - this.setState({ - color: newColor.toString(), - colorString: newColor.toString(), - }); + this.setState({ color: newColor.toString(), colorString: newColor.toString() }); this.props.onColorSelect(color); } } - sampleColorSelected(color) { + sampleColorSelected(color: string) { this.setColor(color); } - spectrumColorSelected(color) { + spectrumColorSelected(color: any) { const rgbColor = color.toRgbString(); this.setColor(rgbColor); } - onColorStringChange(e) { + onColorStringChange(e: any) { const colorString = e.target.value; - this.setState({ - colorString: colorString, - }); + this.setState({ colorString: colorString }); const newColor = tinycolor(colorString); if (newColor.isValid()) { // Update only color state const newColorString = newColor.toString(); - this.setState({ - color: newColorString, - }); + this.setState({ color: newColorString }); this.props.onColorSelect(newColorString); } } - onColorStringBlur(e) { + onColorStringBlur(e: any) { const colorString = e.target.value; this.setColor(colorString); } componentDidMount() { this.pickerNavElem.find('li:first').addClass('active'); - this.pickerNavElem.on('show', e => { + this.pickerNavElem.on('show', (e: any) => { // use href attr (#name => name) const tab = e.target.hash.slice(1); - this.setState({ - tab: tab, - }); + this.setState({ tab: tab }); }); } diff --git a/public/app/core/components/colorpicker/SeriesColorPicker.tsx b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.tsx similarity index 96% rename from public/app/core/components/colorpicker/SeriesColorPicker.tsx rename to packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.tsx index 32b7554e38d..7c3848f6868 100644 --- a/public/app/core/components/colorpicker/SeriesColorPicker.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.tsx @@ -21,7 +21,7 @@ export class SeriesColorPicker extends React.Component { onToggleAxis: () => {}, }; - constructor(props) { + constructor(props: SeriesColorPickerProps) { super(props); } @@ -51,6 +51,7 @@ export class SeriesColorPicker extends React.Component { remove: true, tetherOptions: { constraints: [{ to: 'scrollParent', attachment: 'none both' }], + attachment: 'bottom center', }, }); diff --git a/public/app/core/components/colorpicker/SeriesColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx similarity index 85% rename from public/app/core/components/colorpicker/SeriesColorPickerPopover.tsx rename to packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx index 085d554300d..541a77ddabc 100644 --- a/public/app/core/components/colorpicker/SeriesColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { ColorPickerPopover } from './ColorPickerPopover'; -import { react2AngularDirective } from 'app/core/utils/react2angular'; export interface SeriesColorPickerPopoverProps { color: string; @@ -22,7 +21,7 @@ export class SeriesColorPickerPopover extends React.PureComponent void; + onToggleAxis?: () => void; } interface AxisSelectorState { @@ -30,7 +29,7 @@ interface AxisSelectorState { } export class AxisSelector extends React.PureComponent { - constructor(props) { + constructor(props: AxisSelectorProps) { super(props); this.state = { yaxis: this.props.yaxis, @@ -42,7 +41,10 @@ export class AxisSelector extends React.PureComponent { elem: any; isMoving: boolean; - constructor(props) { + constructor(props: Props) { super(props); this.onSpectrumMove = this.onSpectrumMove.bind(this); this.setComponentElem = this.setComponentElem.bind(this); } - setComponentElem(elem) { + setComponentElem(elem: any) { this.elem = $(elem); } - onSpectrumMove(color) { + onSpectrumMove(color: any) { this.isMoving = true; this.props.onColorSelect(color); } @@ -46,7 +46,7 @@ export class SpectrumPicker extends React.Component { this.elem.spectrum('set', this.props.color); } - componentWillUpdate(nextProps) { + componentWillUpdate(nextProps: any) { // If user move pointer over spectrum field this produce 'move' event and component // may update props.color. We don't want to update spectrum color in this case, so we can use // isMoving flag for tracking moving state. Flag should be cleared in componentDidUpdate() which diff --git a/public/sass/components/_color_picker.scss b/packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss similarity index 100% rename from public/sass/components/_color_picker.scss rename to packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss diff --git a/public/app/core/specs/__snapshots__/ColorPalette.test.tsx.snap b/packages/grafana-ui/src/components/ColorPicker/__snapshots__/ColorPalette.test.tsx.snap similarity index 100% rename from public/app/core/specs/__snapshots__/ColorPalette.test.tsx.snap rename to packages/grafana-ui/src/components/ColorPicker/__snapshots__/ColorPalette.test.tsx.snap diff --git a/public/app/core/components/CustomScrollbar/CustomScrollbar.test.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.test.tsx similarity index 100% rename from public/app/core/components/CustomScrollbar/CustomScrollbar.test.tsx rename to packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.test.tsx diff --git a/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx similarity index 72% rename from public/app/core/components/CustomScrollbar/CustomScrollbar.tsx rename to packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx index 590c11c0615..519e755b474 100644 --- a/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx +++ b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx @@ -6,36 +6,39 @@ interface Props { autoHide?: boolean; autoHideTimeout?: number; autoHideDuration?: number; + autoMaxHeight?: string; hideTracksWhenNotNeeded?: boolean; } /** * Wraps component into component from `react-custom-scrollbars` */ -class CustomScrollbar extends PureComponent { +export class CustomScrollbar extends PureComponent { static defaultProps: Partial = { customClassName: 'custom-scrollbars', autoHide: true, autoHideTimeout: 200, autoHideDuration: 200, + autoMaxHeight: '100%', hideTracksWhenNotNeeded: false, }; render() { - const { customClassName, children, ...scrollProps } = this.props; + const { customClassName, children, autoMaxHeight } = this.props; return (
} renderTrackVertical={props =>
} renderThumbHorizontal={props =>
} renderThumbVertical={props =>
} renderView={props =>
} - {...scrollProps} > {children} diff --git a/packages/grafana-ui/src/components/CustomScrollbar/_CustomScrollbar.scss b/packages/grafana-ui/src/components/CustomScrollbar/_CustomScrollbar.scss new file mode 100644 index 00000000000..c0a8077fb63 --- /dev/null +++ b/packages/grafana-ui/src/components/CustomScrollbar/_CustomScrollbar.scss @@ -0,0 +1,40 @@ +.custom-scrollbars { + // Fix for Firefox. For some reason sometimes .view container gets a height of its content, but in order to + // make scroll working it should fit outer container size (scroll appears only when inner container size is + // greater than outer one). + display: flex; + flex-grow: 1; + + .view { + display: flex; + flex-grow: 1; + flex-direction: column; + } + + .track-vertical { + border-radius: 3px; + width: 6px !important; + right: 2px; + bottom: 2px; + top: 2px; + } + + .track-horizontal { + border-radius: 3px; + height: 6px !important; + + right: 2px; + bottom: 2px; + left: 2px; + } + + .thumb-vertical { + @include gradient-vertical($scrollbarBackground, $scrollbarBackground2); + border-radius: 6px; + } + + .thumb-horizontal { + @include gradient-horizontal($scrollbarBackground, $scrollbarBackground2); + border-radius: 6px; + } +} \ No newline at end of file diff --git a/public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap b/packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap similarity index 83% rename from public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap rename to packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap index 310eb714af9..60b4a2e0aa5 100644 --- a/public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap +++ b/packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap @@ -6,8 +6,8 @@ exports[`CustomScrollbar renders correctly 1`] = ` style={ Object { "height": "auto", - "maxHeight": "inherit", - "minHeight": "inherit", + "maxHeight": "100%", + "minHeight": "0", "overflow": "hidden", "position": "relative", "width": "100%", @@ -23,8 +23,8 @@ exports[`CustomScrollbar renders correctly 1`] = ` "left": undefined, "marginBottom": 0, "marginRight": 0, - "maxHeight": "calc(inherit + 0px)", - "minHeight": "calc(inherit + 0px)", + "maxHeight": "calc(100% + 0px)", + "minHeight": "calc(0 + 0px)", "overflow": "scroll", "position": "relative", "right": undefined, @@ -42,9 +42,7 @@ exports[`CustomScrollbar renders correctly 1`] = ` Object { "display": "none", "height": 6, - "opacity": 0, "position": "absolute", - "transition": "opacity 200ms", } } > @@ -64,9 +62,7 @@ exports[`CustomScrollbar renders correctly 1`] = ` style={ Object { "display": "none", - "opacity": 0, "position": "absolute", - "transition": "opacity 200ms", "width": 6, } } diff --git a/packages/grafana-ui/src/forms/GfFormLabel/GfFormLabel.tsx b/packages/grafana-ui/src/components/GfFormLabel/GfFormLabel.tsx similarity index 100% rename from packages/grafana-ui/src/forms/GfFormLabel/GfFormLabel.tsx rename to packages/grafana-ui/src/components/GfFormLabel/GfFormLabel.tsx diff --git a/packages/grafana-ui/src/visualizations/Graph/Graph.tsx b/packages/grafana-ui/src/components/Graph/Graph.tsx similarity index 98% rename from packages/grafana-ui/src/visualizations/Graph/Graph.tsx rename to packages/grafana-ui/src/components/Graph/Graph.tsx index 51afb33802d..ad038cebcda 100644 --- a/packages/grafana-ui/src/visualizations/Graph/Graph.tsx +++ b/packages/grafana-ui/src/components/Graph/Graph.tsx @@ -98,6 +98,7 @@ export class Graph extends PureComponent { $.plot(this.element, timeSeries, flotOptions); } catch (err) { console.log('Graph rendering error', err, flotOptions, timeSeries); + throw new Error('Error rendering panel'); } } diff --git a/public/app/core/components/Label/Label.tsx b/packages/grafana-ui/src/components/Label/Label.tsx similarity index 62% rename from public/app/core/components/Label/Label.tsx rename to packages/grafana-ui/src/components/Label/Label.tsx index 362c3c577f7..270b0161226 100644 --- a/public/app/core/components/Label/Label.tsx +++ b/packages/grafana-ui/src/components/Label/Label.tsx @@ -1,5 +1,5 @@ import React, { SFC, ReactNode } from 'react'; -import Tooltip from '../Tooltip/Tooltip'; +import { Tooltip } from '../Tooltip/Tooltip'; interface Props { tooltip?: string; @@ -14,8 +14,10 @@ export const Label: SFC = props => { {props.children} {props.tooltip && ( - - + +
+ +
)}
diff --git a/packages/grafana-ui/src/components/LoadingPlaceholder/LoadingPlaceholder.tsx b/packages/grafana-ui/src/components/LoadingPlaceholder/LoadingPlaceholder.tsx new file mode 100644 index 00000000000..01048014f8a --- /dev/null +++ b/packages/grafana-ui/src/components/LoadingPlaceholder/LoadingPlaceholder.tsx @@ -0,0 +1,11 @@ +import React, { SFC } from 'react'; + +interface LoadingPlaceholderProps { + text: string; +} + +export const LoadingPlaceholder: SFC = ({ text }) => ( +
+ {text} +
+); diff --git a/packages/grafana-ui/src/components/PanelOptionsGrid/PanelOptionsGrid.tsx b/packages/grafana-ui/src/components/PanelOptionsGrid/PanelOptionsGrid.tsx new file mode 100644 index 00000000000..0636ec4a9da --- /dev/null +++ b/packages/grafana-ui/src/components/PanelOptionsGrid/PanelOptionsGrid.tsx @@ -0,0 +1,15 @@ +import React, { SFC } from 'react'; + +interface Props { + cols?: number; + children: JSX.Element[] | JSX.Element; +} + +export const PanelOptionsGrid: SFC = ({ children }) => { + + return ( +
+ {children} +
+ ); +}; diff --git a/packages/grafana-ui/src/components/PanelOptionsGrid/_PanelOptionsGrid.scss b/packages/grafana-ui/src/components/PanelOptionsGrid/_PanelOptionsGrid.scss new file mode 100644 index 00000000000..1cd26867a97 --- /dev/null +++ b/packages/grafana-ui/src/components/PanelOptionsGrid/_PanelOptionsGrid.scss @@ -0,0 +1,10 @@ +.panel-options-grid { + display: grid; + grid-template-columns: repeat(1, 1fr); + grid-row-gap: 10px; + grid-column-gap: 10px; + + @include media-breakpoint-up(lg) { + grid-template-columns: repeat(3, 1fr); + } +} diff --git a/public/app/features/dashboard/dashgrid/PanelOptionSection.tsx b/packages/grafana-ui/src/components/PanelOptionsGroup/PanelOptionsGroup.tsx similarity index 65% rename from public/app/features/dashboard/dashgrid/PanelOptionSection.tsx rename to packages/grafana-ui/src/components/PanelOptionsGroup/PanelOptionsGroup.tsx index f38d99d2237..7ce4b8335ff 100644 --- a/public/app/features/dashboard/dashgrid/PanelOptionSection.tsx +++ b/packages/grafana-ui/src/components/PanelOptionsGroup/PanelOptionsGroup.tsx @@ -7,11 +7,11 @@ interface Props { children: JSX.Element | JSX.Element[]; } -export const PanelOptionSection: SFC = props => { +export const PanelOptionsGroup: SFC = props => { return ( -
+
{props.title && ( -
+
{props.title} {props.onClose && (
)} -
{props.children}
+
{props.children}
); }; diff --git a/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss new file mode 100644 index 00000000000..9f5d4f02695 --- /dev/null +++ b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss @@ -0,0 +1,27 @@ +.panel-options-group { + margin-bottom: 10px; + border: $panel-options-group-border; + border-radius: $border-radius; + background: $page-bg; +} + +.panel-options-group__header { + padding: 4px 20px; + font-size: 1.1rem; + background: $panel-options-group-header-bg; + position: relative; + + .btn { + position: absolute; + right: 0; + top: 0px; + } +} + +.panel-options-group__body { + padding: 20px; + + &--queries { + min-height: 200px; + } +} diff --git a/public/app/core/components/Portal/Portal.tsx b/packages/grafana-ui/src/components/Portal/Portal.tsx similarity index 77% rename from public/app/core/components/Portal/Portal.tsx rename to packages/grafana-ui/src/components/Portal/Portal.tsx index 25d54a64209..bc6fa37f2d0 100644 --- a/public/app/core/components/Portal/Portal.tsx +++ b/packages/grafana-ui/src/components/Portal/Portal.tsx @@ -6,16 +6,13 @@ interface Props { root?: HTMLElement; } -export default class BodyPortal extends PureComponent { +export class Portal extends PureComponent { node: HTMLElement = document.createElement('div'); portalRoot: HTMLElement; - constructor(props) { + constructor(props: Props) { super(props); - const { - className, - root = document.body - } = this.props; + const { className, root = document.body } = this.props; if (className) { this.node.classList.add(className); diff --git a/public/app/core/components/Select/IndicatorsContainer.tsx b/packages/grafana-ui/src/components/Select/IndicatorsContainer.tsx similarity index 73% rename from public/app/core/components/Select/IndicatorsContainer.tsx rename to packages/grafana-ui/src/components/Select/IndicatorsContainer.tsx index d4de51a2cef..260fe6ebbdf 100644 --- a/public/app/core/components/Select/IndicatorsContainer.tsx +++ b/packages/grafana-ui/src/components/Select/IndicatorsContainer.tsx @@ -1,7 +1,10 @@ import React from 'react'; + +// Ignoring because I couldn't get @types/react-select work wih Torkel's fork +// @ts-ignore import { components } from '@torkelo/react-select'; -export const IndicatorsContainer = props => { +export const IndicatorsContainer = (props: any) => { const isOpen = props.selectProps.menuIsOpen; return ( diff --git a/public/app/core/components/Select/NoOptionsMessage.tsx b/packages/grafana-ui/src/components/Select/NoOptionsMessage.tsx similarity index 83% rename from public/app/core/components/Select/NoOptionsMessage.tsx rename to packages/grafana-ui/src/components/Select/NoOptionsMessage.tsx index 5fe229340a4..1cec06a5301 100644 --- a/public/app/core/components/Select/NoOptionsMessage.tsx +++ b/packages/grafana-ui/src/components/Select/NoOptionsMessage.tsx @@ -1,5 +1,9 @@ import React from 'react'; + +// Ignoring because I couldn't get @types/react-select work wih Torkel's fork +// @ts-ignore import { components } from '@torkelo/react-select'; +// @ts-ignore import { OptionProps } from '@torkelo/react-select/lib/components/Option'; export interface Props { diff --git a/public/app/core/components/Select/Select.tsx b/packages/grafana-ui/src/components/Select/Select.tsx similarity index 89% rename from public/app/core/components/Select/Select.tsx rename to packages/grafana-ui/src/components/Select/Select.tsx index 893eb1a6655..348133c6c28 100644 --- a/public/app/core/components/Select/Select.tsx +++ b/packages/grafana-ui/src/components/Select/Select.tsx @@ -1,17 +1,22 @@ // Libraries import classNames from 'classnames'; import React, { PureComponent } from 'react'; + +// Ignoring because I couldn't get @types/react-select work wih Torkel's fork +// @ts-ignore import { default as ReactSelect } from '@torkelo/react-select'; +// @ts-ignore import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async'; +// @ts-ignore import { components } from '@torkelo/react-select'; // Components -import { Option, SingleValue } from './PickerOption'; -import OptionGroup from './OptionGroup'; +import { SelectOption, SingleValue } from './SelectOption'; +import SelectOptionGroup from './SelectOptionGroup'; import IndicatorsContainer from './IndicatorsContainer'; import NoOptionsMessage from './NoOptionsMessage'; -import ResetStyles from './ResetStyles'; -import CustomScrollbar from '../CustomScrollbar/CustomScrollbar'; +import resetSelectStyles from './resetSelectStyles'; +import { CustomScrollbar } from '@grafana/ui'; export interface SelectOptionItem { label?: string; @@ -53,10 +58,10 @@ interface AsyncProps { loadingMessage?: () => string; } -export const MenuList = props => { +export const MenuList = (props: any) => { return ( - {props.children} + {props.children} ); }; @@ -112,11 +117,11 @@ export class Select extends PureComponent { classNamePrefix="gf-form-select-box" className={selectClassNames} components={{ - Option, + Option: SelectOption, SingleValue, IndicatorsContainer, MenuList, - Group: OptionGroup, + Group: SelectOptionGroup, }} defaultValue={defaultValue} value={value} @@ -127,7 +132,7 @@ export class Select extends PureComponent { onChange={onChange} options={options} placeholder={placeholder || 'Choose'} - styles={ResetStyles} + styles={resetSelectStyles()} isDisabled={isDisabled} isLoading={isLoading} isClearable={isClearable} @@ -212,7 +217,7 @@ export class AsyncSelect extends PureComponent { isLoading={isLoading} defaultOptions={defaultOptions} placeholder={placeholder || 'Choose'} - styles={ResetStyles} + styles={resetSelectStyles()} loadingMessage={loadingMessage} noOptionsMessage={noOptionsMessage} isDisabled={isDisabled} diff --git a/public/app/core/components/Select/PickerOption.test.tsx b/packages/grafana-ui/src/components/Select/SelectOption.test.tsx similarity index 51% rename from public/app/core/components/Select/PickerOption.test.tsx rename to packages/grafana-ui/src/components/Select/SelectOption.test.tsx index 6b4aedcfcc0..1876e438d75 100644 --- a/public/app/core/components/Select/PickerOption.test.tsx +++ b/packages/grafana-ui/src/components/Select/SelectOption.test.tsx @@ -1,11 +1,12 @@ import React from 'react'; import renderer from 'react-test-renderer'; -import PickerOption from './PickerOption'; +import SelectOption from './SelectOption'; +import { OptionProps } from 'react-select/lib/components/Option'; -const model = { +const model: OptionProps = { + data: jest.fn(), cx: jest.fn(), clearValue: jest.fn(), - onSelect: jest.fn(), getStyles: jest.fn(), getValue: jest.fn(), hasValue: true, @@ -18,21 +19,31 @@ const model = { isFocused: false, isSelected: false, innerRef: null, - innerProps: null, - label: 'Option label', - type: null, - children: 'Model title', - data: { - title: 'Model title', - imgUrl: 'url/to/avatar', - label: 'User picker label', + innerProps: { + id: '', + key: '', + onClick: jest.fn(), + onMouseOver: jest.fn(), + tabIndex: 1, }, + label: 'Option label', + type: 'option', + children: 'Model title', className: 'class-for-user-picker', }; -describe('PickerOption', () => { +describe('SelectOption', () => { it('renders correctly', () => { - const tree = renderer.create().toJSON(); + const tree = renderer + .create( + + ) + .toJSON(); expect(tree).toMatchSnapshot(); }); }); diff --git a/public/app/core/components/Select/PickerOption.tsx b/packages/grafana-ui/src/components/Select/SelectOption.tsx similarity index 84% rename from public/app/core/components/Select/PickerOption.tsx rename to packages/grafana-ui/src/components/Select/SelectOption.tsx index d263f6f832b..5f94c2182f9 100644 --- a/public/app/core/components/Select/PickerOption.tsx +++ b/packages/grafana-ui/src/components/Select/SelectOption.tsx @@ -1,4 +1,7 @@ import React from 'react'; + +// Ignoring because I couldn't get @types/react-select work wih Torkel's fork +// @ts-ignore import { components } from '@torkelo/react-select'; import { OptionProps } from 'react-select/lib/components/Option'; @@ -10,7 +13,7 @@ interface ExtendedOptionProps extends OptionProps { }; } -export const Option = (props: ExtendedOptionProps) => { +export const SelectOption = (props: ExtendedOptionProps) => { const { children, isSelected, data } = props; return ( @@ -28,7 +31,7 @@ export const Option = (props: ExtendedOptionProps) => { }; // was not able to type this without typescript error -export const SingleValue = props => { +export const SingleValue = (props: any) => { const { children, data } = props; return ( @@ -41,4 +44,4 @@ export const SingleValue = props => { ); }; -export default Option; +export default SelectOption; diff --git a/public/app/core/components/Select/OptionGroup.tsx b/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx similarity index 72% rename from public/app/core/components/Select/OptionGroup.tsx rename to packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx index a001f58c681..efc5e4516fc 100644 --- a/public/app/core/components/Select/OptionGroup.tsx +++ b/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx @@ -2,21 +2,27 @@ import React, { PureComponent } from 'react'; import { GroupProps } from 'react-select/lib/components/Group'; interface ExtendedGroupProps extends GroupProps { - data: any; + data: { + label: string; + expanded: boolean; + options: any[]; + }; } interface State { expanded: boolean; } -export default class OptionGroup extends PureComponent { +export default class SelectOptionGroup extends PureComponent { state = { expanded: false, }; componentDidMount() { - if (this.props.selectProps) { - const value = this.props.selectProps.value[this.props.selectProps.value.length - 1]; + if (this.props.data.expanded) { + this.setState({ expanded: true }); + } else if (this.props.selectProps && this.props.selectProps.value) { + const { value } = this.props.selectProps.value; if (value && this.props.options.some(option => option.value === value)) { this.setState({ expanded: true }); @@ -24,7 +30,7 @@ export default class OptionGroup extends PureComponent +exports[`SelectOption renders correctly 1`] = ` +
diff --git a/packages/grafana-ui/src/components/Select/resetSelectStyles.ts b/packages/grafana-ui/src/components/Select/resetSelectStyles.ts new file mode 100644 index 00000000000..a980741c17c --- /dev/null +++ b/packages/grafana-ui/src/components/Select/resetSelectStyles.ts @@ -0,0 +1,27 @@ +export default function resetSelectStyles() { + return { + clearIndicator: () => ({}), + container: () => ({}), + control: () => ({}), + dropdownIndicator: () => ({}), + group: () => ({}), + groupHeading: () => ({}), + indicatorsContainer: () => ({}), + indicatorSeparator: () => ({}), + input: () => ({}), + loadingIndicator: () => ({}), + loadingMessage: () => ({}), + menu: () => ({}), + menuList: ({ maxHeight }: { maxHeight: number }) => ({ + maxHeight, + }), + multiValue: () => ({}), + multiValueLabel: () => ({}), + multiValueRemove: () => ({}), + noOptionsMessage: () => ({}), + option: () => ({}), + placeholder: () => ({}), + singleValue: () => ({}), + valueContainer: () => ({}), + }; +} diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx new file mode 100644 index 00000000000..845ff5f6bf4 --- /dev/null +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx @@ -0,0 +1,173 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { ThresholdsEditor, Props } from './ThresholdsEditor'; + +const setup = (propOverrides?: object) => { + const props: Props = { + onChange: jest.fn(), + thresholds: [], + }; + + Object.assign(props, propOverrides); + + return shallow().instance() as ThresholdsEditor; +}; + +describe('Initialization', () => { + it('should add a base threshold if missing', () => { + const instance = setup(); + + expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]); + }); +}); + +describe('Add threshold', () => { + it('should not add threshold at index 0', () => { + const instance = setup(); + + instance.onAddThreshold(0); + + expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]); + }); + + it('should add threshold', () => { + const instance = setup(); + + instance.onAddThreshold(1); + + expect(instance.state.thresholds).toEqual([ + { index: 1, value: 50, color: '#EAB839' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, + ]); + }); + + it('should add another threshold above a first', () => { + const instance = setup({ + thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }], + }); + + instance.onAddThreshold(2); + + expect(instance.state.thresholds).toEqual([ + { index: 2, value: 75, color: '#6ED0E0' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, + ]); + }); + + it('should add another threshold between first and second index', () => { + const instance = setup({ + thresholds: [ + { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 2, value: 75, color: '#6ED0E0' }, + ], + }); + + instance.onAddThreshold(2); + + expect(instance.state.thresholds).toEqual([ + { index: 3, value: 75, color: '#6ED0E0' }, + { index: 2, value: 62.5, color: '#EF843C' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, + ]); + }); +}); + +describe('Remove threshold', () => { + it('should not remove threshold at index 0', () => { + const thresholds = [ + { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 2, value: 75, color: '#6ED0E0' }, + ]; + const instance = setup({ thresholds }); + + instance.onRemoveThreshold(thresholds[0]); + + expect(instance.state.thresholds).toEqual(thresholds); + }); + + it('should remove threshold', () => { + const thresholds = [ + { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 2, value: 75, color: '#6ED0E0' }, + ]; + const instance = setup({ + thresholds, + }); + + instance.onRemoveThreshold(thresholds[1]); + + expect(instance.state.thresholds).toEqual([ + { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 75, color: '#6ED0E0' }, + ]); + }); +}); + +describe('change threshold value', () => { + it('should not change threshold at index 0', () => { + const thresholds = [ + { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 2, value: 75, color: '#6ED0E0' }, + ]; + const instance = setup({ thresholds }); + + const mockEvent = { target: { value: 12 } }; + + instance.onChangeThresholdValue(mockEvent, thresholds[0]); + + expect(instance.state.thresholds).toEqual(thresholds); + }); + + it('should update value', () => { + const instance = setup(); + const thresholds = [ + { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 2, value: 75, color: '#6ED0E0' }, + ]; + + instance.state = { + thresholds, + }; + + const mockEvent = { target: { value: 78 } }; + + instance.onChangeThresholdValue(mockEvent, thresholds[1]); + + expect(instance.state.thresholds).toEqual([ + { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 78, color: '#EAB839' }, + { index: 2, value: 75, color: '#6ED0E0' }, + ]); + }); +}); + +describe('on blur threshold value', () => { + it('should resort rows and update indexes', () => { + const instance = setup(); + const thresholds = [ + { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 78, color: '#EAB839' }, + { index: 2, value: 75, color: '#6ED0E0' }, + ]; + + instance.state = { + thresholds, + }; + + instance.onBlur(); + + expect(instance.state.thresholds).toEqual([ + { index: 2, value: 78, color: '#EAB839' }, + { index: 1, value: 75, color: '#6ED0E0' }, + { index: 0, value: -Infinity, color: '#7EB26D' }, + ]); + }); +}); diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx new file mode 100644 index 00000000000..6e8d88051f3 --- /dev/null +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -0,0 +1,206 @@ +import React, { PureComponent } from 'react'; +// import tinycolor, { ColorInput } from 'tinycolor2'; + +import { Threshold } from '../../types'; +import { ColorPicker } from '../ColorPicker/ColorPicker'; +import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup'; +import { colors } from '../../utils'; + +export interface Props { + thresholds: Threshold[]; + onChange: (thresholds: Threshold[]) => void; +} + +interface State { + thresholds: Threshold[]; +} + +export class ThresholdsEditor extends PureComponent { + constructor(props: Props) { + super(props); + + const thresholds: Threshold[] = + props.thresholds.length > 0 ? props.thresholds : [{ index: 0, value: -Infinity, color: colors[0] }]; + this.state = { thresholds }; + } + + onAddThreshold = (index: number) => { + const { thresholds } = this.state; + const maxValue = 100; + const minValue = 0; + + if (index === 0) { + return; + } + + const newThresholds = thresholds.map(threshold => { + if (threshold.index >= index) { + const index = threshold.index + 1; + threshold = { ...threshold, index }; + } + return threshold; + }); + + // Setting value to a value between the previous thresholds + const beforeThreshold = newThresholds.filter(t => t.index === index - 1 && t.index !== 0)[0]; + const afterThreshold = newThresholds.filter(t => t.index === index + 1 && t.index !== 0)[0]; + const beforeThresholdValue = beforeThreshold !== undefined ? beforeThreshold.value : minValue; + const afterThresholdValue = afterThreshold !== undefined ? afterThreshold.value : maxValue; + const value = afterThresholdValue - (afterThresholdValue - beforeThresholdValue) / 2; + + // Set a color + const color = colors.filter(c => newThresholds.some(t => t.color === c) === false)[0]; + + this.setState( + { + thresholds: this.sortThresholds([ + ...newThresholds, + { + index, + value: value as number, + color, + }, + ]), + }, + () => this.updateGauge() + ); + }; + + onRemoveThreshold = (threshold: Threshold) => { + if (threshold.index === 0) { + return; + } + + this.setState( + prevState => { + const newThresholds = prevState.thresholds.map(t => { + if (t.index > threshold.index) { + const index = t.index - 1; + t = { ...t, index }; + } + return t; + }); + + return { + thresholds: newThresholds.filter(t => t !== threshold), + }; + }, + () => this.updateGauge() + ); + }; + + onChangeThresholdValue = (event: any, threshold: Threshold) => { + if (threshold.index === 0) { + return; + } + + const { thresholds } = this.state; + const parsedValue = parseInt(event.target.value, 10); + const value = isNaN(parsedValue) ? null : parsedValue; + + const newThresholds = thresholds.map(t => { + if (t === threshold) { + t = { ...t, value: value as number }; + } + + return t; + }); + + this.setState({ thresholds: newThresholds }); + }; + + onChangeThresholdColor = (threshold: Threshold, color: string) => { + const { thresholds } = this.state; + + const newThresholds = thresholds.map(t => { + if (t === threshold) { + t = { ...t, color: color }; + } + + return t; + }); + + this.setState( + { + thresholds: newThresholds, + }, + () => this.updateGauge() + ); + }; + + onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds); + onBlur = () => { + this.setState(prevState => { + const sortThresholds = this.sortThresholds([...prevState.thresholds]); + let index = sortThresholds.length - 1; + sortThresholds.forEach(t => { + t.index = index--; + }); + return { thresholds: sortThresholds }; + }); + + this.updateGauge(); + }; + + updateGauge = () => { + this.props.onChange(this.state.thresholds); + }; + + sortThresholds = (thresholds: Threshold[]) => { + return thresholds.sort((t1, t2) => { + return t2.value - t1.value; + }); + }; + + renderInput = (threshold: Threshold) => { + const value = threshold.index === 0 ? 'Base' : threshold.value; + return ( +
+ +
+ {threshold.color && ( +
+ this.onChangeThresholdColor(threshold, color)} /> +
+ )} +
+
+ this.onChangeThresholdValue(event, threshold)} + value={value} + onBlur={this.onBlur} + readOnly={threshold.index === 0} + /> +
+ {threshold.index > 0 && ( +
this.onRemoveThreshold(threshold)}> + +
+ )} +
+ ); + }; + + render() { + const { thresholds } = this.state; + + return ( + +
+ {thresholds.map((threshold, index) => { + return ( +
+
this.onAddThreshold(threshold.index + 1)}> + +
+
+
{this.renderInput(threshold)}
+
+ ); + })} +
+ + ); + } +} diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss new file mode 100644 index 00000000000..61278321572 --- /dev/null +++ b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss @@ -0,0 +1,105 @@ +.thresholds { + margin-bottom: 10px; +} + +.thresholds-row { + display: flex; + flex-direction: row; + height: 70px; +} + +.thresholds-row:first-child > .thresholds-row-color-indicator { + border-top-left-radius: $border-radius; + border-top-right-radius: $border-radius; + overflow: hidden; +} + +.thresholds-row:last-child > .thresholds-row-color-indicator { + border-bottom-left-radius: $border-radius; + border-bottom-right-radius: $border-radius; + overflow: hidden; +} + +.thresholds-row-add-button { + align-self: center; + margin-right: 5px; + color: $green; + height: 24px; + width: 24px; + background-color: $green; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.thresholds-row-add-button > i { + color: $white; +} + +.thresholds-row-color-indicator { + width: 10px; +} + +.thresholds-row-input { + margin-top: 49px; + margin-left: 2px; +} + +.thresholds-row-input-inner { + display: flex; + justify-content: center; + flex-direction: row; +} + +.thresholds-row-input-inner > *:last-child { + border-top-right-radius: $border-radius; + border-bottom-right-radius: $border-radius; +} + +.thresholds-row-input-inner-arrow { + align-self: center; + width: 0; + height: 0; + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + border-right: 6px solid $input-label-border-color; +} + +.thresholds-row-input-inner-value > input { + height: $gf-form-input-height; + padding: $input-padding-y $input-padding-x; + width: 150px; + border-top: 1px solid $input-label-border-color; + border-bottom: 1px solid $input-label-border-color; +} + +.thresholds-row-input-inner-color { + width: 42px; + display: flex; + align-items: center; + justify-content: center; + background-color: $input-bg; + border: 1px solid $input-label-border-color; +} + +.thresholds-row-input-inner-color-colorpicker { + border-radius: 10px; + overflow: hidden; + display: flex; + align-items: center; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25); +} + +.thresholds-row-input-inner-remove { + display: flex; + align-items: center; + justify-content: center; + height: $gf-form-input-height; + padding: $input-padding-y $input-padding-x; + width: 42px; + background-color: $input-label-bg; + border: 1px solid $input-label-border-color; + cursor: pointer; +} diff --git a/public/app/core/components/Tooltip/Popper.tsx b/packages/grafana-ui/src/components/Tooltip/Popper.tsx similarity index 61% rename from public/app/core/components/Tooltip/Popper.tsx rename to packages/grafana-ui/src/components/Tooltip/Popper.tsx index 36cf0fe837e..eb64df1cb6e 100644 --- a/public/app/core/components/Tooltip/Popper.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Popper.tsx @@ -1,49 +1,54 @@ import React, { PureComponent } from 'react'; -import Portal from 'app/core/components/Portal/Portal'; -import { Manager, Popper as ReactPopper, Reference } from 'react-popper'; +import * as PopperJS from 'popper.js'; +import { Manager, Popper as ReactPopper } from 'react-popper'; +import { Portal } from '@grafana/ui'; import Transition from 'react-transition-group/Transition'; +export enum Themes { + Default = 'popper__background--default', + Error = 'popper__background--error', + Brand = 'popper__background--brand', +} + const defaultTransitionStyles = { transition: 'opacity 200ms linear', opacity: 0, }; -const transitionStyles = { +const transitionStyles: {[key: string]: object} = { exited: { opacity: 0 }, entering: { opacity: 0 }, entered: { opacity: 1 }, exiting: { opacity: 0 }, }; -interface Props { +interface Props extends React.DOMAttributes { renderContent: (content: any) => any; show: boolean; - placement?: any; + placement?: PopperJS.Placement; content: string | ((props: any) => JSX.Element); - refClassName?: string; + referenceElement: PopperJS.ReferenceObject; + theme?: Themes; } class Popper extends PureComponent { render() { - const { children, renderContent, show, placement, refClassName } = this.props; + const { renderContent, show, placement, onMouseEnter, onMouseLeave, theme } = this.props; const { content } = this.props; + const popperBackgroundClassName = 'popper__background' + (theme ? ' ' + theme : ''); + return ( - - {({ ref }) => ( -
- {children} -
- )} -
{transitionState => ( - + {({ ref, style, placement, arrowProps }) => { return (
{ data-placement={placement} className="popper" > -
+
{renderContent(content)}
diff --git a/packages/grafana-ui/src/components/Tooltip/PopperController.tsx b/packages/grafana-ui/src/components/Tooltip/PopperController.tsx new file mode 100644 index 00000000000..5f4010ac58a --- /dev/null +++ b/packages/grafana-ui/src/components/Tooltip/PopperController.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import * as PopperJS from 'popper.js'; +import { Themes } from './Popper'; + +type PopperContent = string | (() => JSX.Element); + +export interface UsingPopperProps { + show?: boolean; + placement?: PopperJS.Placement; + content: PopperContent; + children: JSX.Element; + renderContent?: (content: PopperContent) => JSX.Element; + theme?: Themes; +} + +type PopperControllerRenderProp = ( + showPopper: () => void, + hidePopper: () => void, + popperProps: { + show: boolean; + placement: PopperJS.Placement; + content: string | ((props: any) => JSX.Element); + renderContent: (content: any) => any; + theme?: Themes; + } +) => JSX.Element; + +interface Props { + placement?: PopperJS.Placement; + content: PopperContent; + className?: string; + children: PopperControllerRenderProp; + theme?: Themes; +} + +interface State { + placement: PopperJS.Placement; + show: boolean; +} + +class PopperController extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + placement: this.props.placement || 'auto', + show: false, + }; + } + + componentWillReceiveProps(nextProps: Props) { + if (nextProps.placement && nextProps.placement !== this.state.placement) { + this.setState((prevState: State) => { + return { + ...prevState, + placement: nextProps.placement || 'auto', + }; + }); + } + } + + showPopper = () => { + this.setState(prevState => ({ + ...prevState, + show: true, + })); + }; + + hidePopper = () => { + this.setState(prevState => ({ + ...prevState, + show: false, + })); + }; + + renderContent(content: PopperContent) { + if (typeof content === 'function') { + // If it's a function we assume it's a React component + const ReactComponent = content; + return ; + } + return content; + } + + render() { + const { children, content, theme } = this.props; + const { show, placement } = this.state; + + return children(this.showPopper, this.hidePopper, { + show, + placement, + content, + renderContent: this.renderContent, + theme, + }); + } +} + +export default PopperController; diff --git a/public/app/core/components/Tooltip/Tooltip.test.tsx b/packages/grafana-ui/src/components/Tooltip/Tooltip.test.tsx similarity index 56% rename from public/app/core/components/Tooltip/Tooltip.test.tsx rename to packages/grafana-ui/src/components/Tooltip/Tooltip.test.tsx index d2c96bb23d2..95d01c7f2fe 100644 --- a/public/app/core/components/Tooltip/Tooltip.test.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Tooltip.test.tsx @@ -1,13 +1,15 @@ import React from 'react'; import renderer from 'react-test-renderer'; -import Tooltip from './Tooltip'; +import { Tooltip } from './Tooltip'; describe('Tooltip', () => { it('renders correctly', () => { const tree = renderer .create( - - Link with tooltip + + + Link with tooltip + ) .toJSON(); diff --git a/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx new file mode 100644 index 00000000000..9cffb151d83 --- /dev/null +++ b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx @@ -0,0 +1,32 @@ +import React, { createRef } from 'react'; +import * as PopperJS from 'popper.js'; +import Popper from './Popper'; +import PopperController, { UsingPopperProps } from './PopperController'; + +export const Tooltip = ({ children, renderContent, ...controllerProps }: UsingPopperProps) => { + const tooltipTriggerRef = createRef(); + + return ( + + {(showPopper, hidePopper, popperProps) => { + return ( + <> + {tooltipTriggerRef.current && ( + + )} + {React.cloneElement(children, { + ref: tooltipTriggerRef, + onMouseEnter: showPopper, + onMouseLeave: hidePopper, + })} + + ); + }} + + ); +}; diff --git a/public/sass/components/_popper.scss b/packages/grafana-ui/src/components/Tooltip/_Tooltip.scss similarity index 83% rename from public/sass/components/_popper.scss rename to packages/grafana-ui/src/components/Tooltip/_Tooltip.scss index d869d52b92f..c8fa099cce6 100644 --- a/public/sass/components/_popper.scss +++ b/packages/grafana-ui/src/components/Tooltip/_Tooltip.scss @@ -1,5 +1,13 @@ $popper-margin-from-ref: 5px; + +@mixin popper-theme($backgroundColor, $arrowColor) { + background: $backgroundColor; + .popper__arrow { + border-color: $arrowColor; + } +} + .popper { position: absolute; z-index: $zindex-tooltip; @@ -8,7 +16,24 @@ $popper-margin-from-ref: 5px; text-align: center; } -.popper .popper__arrow { +.popper__background { + background: $tooltipBackground; + border-radius: $border-radius; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.5); + padding: 10px; + + // Themes + &.popper__background--error { + @include popper-theme($tooltipBackgroundError, $tooltipBackgroundError); + } + + &.popper__background--brand { + @include popper-theme($tooltipBackgroundBrand, $tooltipBackgroundBrand); + @include gradient-vertical($red, $orange); + } +} + +.popper__arrow { width: 0; height: 0; border-style: solid; @@ -16,17 +41,10 @@ $popper-margin-from-ref: 5px; margin: 0px; } -.popper .popper__arrow { +.popper__arrow { border-color: $tooltipBackground; } -.popper__background { - background: $tooltipBackground; - border-radius: $border-radius; - box-shadow: 0 0 2px rgba(0, 0, 0, 0.5); - padding: 10px; -} - // Top .popper[data-placement^='top'] { padding-bottom: $popper-margin-from-ref; diff --git a/packages/grafana-ui/src/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap b/packages/grafana-ui/src/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap new file mode 100644 index 00000000000..761221906d4 --- /dev/null +++ b/packages/grafana-ui/src/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Tooltip renders correctly 1`] = ` + + Link with tooltip + +`; diff --git a/public/app/plugins/panel/gauge/MappingRow.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx similarity index 75% rename from public/app/plugins/panel/gauge/MappingRow.tsx rename to packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx index 35d0b2e638c..9705304d354 100644 --- a/public/app/plugins/panel/gauge/MappingRow.tsx +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx @@ -1,22 +1,23 @@ import React, { PureComponent } from 'react'; -import { Label } from 'app/core/components/Label/Label'; -import { Select } from 'app/core/components/Select/Select'; -import { MappingType, RangeMap, ValueMap } from 'app/types'; -interface Props { - mapping: ValueMap | RangeMap; - updateMapping: (mapping) => void; - removeMapping: () => void; +import { MappingType, ValueMapping } from '../../types/panel'; +import { Label } from '../Label/Label'; +import { Select } from '../Select/Select'; + +export interface Props { + valueMapping: ValueMapping; + updateValueMapping: (valueMapping: ValueMapping) => void; + removeValueMapping: () => void; } interface State { - from: string; + from?: string; id: number; operator: string; text: string; - to: string; + to?: string; type: MappingType; - value: string; + value?: string; } const mappingOptions = [ @@ -25,36 +26,34 @@ const mappingOptions = [ ]; export default class MappingRow extends PureComponent { - constructor(props) { + constructor(props: Props) { super(props); - this.state = { - ...props.mapping, - }; + this.state = { ...props.valueMapping }; } - onMappingValueChange = event => { + onMappingValueChange = (event: React.ChangeEvent) => { this.setState({ value: event.target.value }); }; - onMappingFromChange = event => { + onMappingFromChange = (event: React.ChangeEvent) => { this.setState({ from: event.target.value }); }; - onMappingToChange = event => { + onMappingToChange = (event: React.ChangeEvent) => { this.setState({ to: event.target.value }); }; - onMappingTextChange = event => { + onMappingTextChange = (event: React.ChangeEvent) => { this.setState({ text: event.target.value }); }; - onMappingTypeChange = mappingType => { + onMappingTypeChange = (mappingType: MappingType) => { this.setState({ type: mappingType }); }; updateMapping = () => { - this.props.updateMapping({ ...this.state }); + this.props.updateValueMapping({ ...this.state } as ValueMapping); }; renderRow() { @@ -136,7 +135,7 @@ export default class MappingRow extends PureComponent {
{this.renderRow()}
-
diff --git a/public/app/plugins/panel/gauge/ValueMappings.test.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx similarity index 61% rename from public/app/plugins/panel/gauge/ValueMappings.test.tsx rename to packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx index fd9f56343b1..bbad3e5a7ca 100644 --- a/public/app/plugins/panel/gauge/ValueMappings.test.tsx +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx @@ -1,26 +1,23 @@ import React from 'react'; import { shallow } from 'enzyme'; -import ValueMappings from './ValueMappings'; -import { defaultProps, OptionModuleProps } from './module'; -import { MappingType } from 'app/types'; + +import { ValueMappingsEditor, Props } from './ValueMappingsEditor'; +import { MappingType } from '../../types/panel'; const setup = (propOverrides?: object) => { - const props: OptionModuleProps = { + const props: Props = { onChange: jest.fn(), - options: { - ...defaultProps.options, - mappings: [ - { id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' }, - { id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' }, - ], - }, + valueMappings: [ + { id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' }, + { id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' }, + ], }; Object.assign(props, propOverrides); - const wrapper = shallow(); + const wrapper = shallow(); - const instance = wrapper.instance() as ValueMappings; + const instance = wrapper.instance() as ValueMappingsEditor; return { instance, @@ -39,18 +36,20 @@ describe('Render', () => { describe('On remove mapping', () => { it('Should remove mapping with id 0', () => { const { instance } = setup(); + instance.onRemoveMapping(1); - expect(instance.state.mappings).toEqual([ + expect(instance.state.valueMappings).toEqual([ { id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' }, ]); }); it('should remove mapping with id 1', () => { const { instance } = setup(); + instance.onRemoveMapping(2); - expect(instance.state.mappings).toEqual([ + expect(instance.state.valueMappings).toEqual([ { id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' }, ]); }); @@ -66,7 +65,7 @@ describe('Next id to add', () => { }); it('should default to 1', () => { - const { instance } = setup({ options: { ...defaultProps.options } }); + const { instance } = setup({ valueMappings: [] }); expect(instance.state.nextIdToAdd).toEqual(1); }); diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx new file mode 100644 index 00000000000..ca0a6e71f4a --- /dev/null +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx @@ -0,0 +1,105 @@ +import React, { PureComponent } from 'react'; + +import MappingRow from './MappingRow'; +import { MappingType, ValueMapping } from '../../types/panel'; +import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup'; + +export interface Props { + valueMappings: ValueMapping[]; + onChange: (valueMappings: ValueMapping[]) => void; +} + +interface State { + valueMappings: ValueMapping[]; + nextIdToAdd: number; +} + +export class ValueMappingsEditor extends PureComponent { + constructor(props: Props) { + super(props); + + const mappings = props.valueMappings; + + this.state = { + valueMappings: mappings, + nextIdToAdd: mappings.length > 0 ? this.getMaxIdFromValueMappings(mappings) : 1, + }; + } + + getMaxIdFromValueMappings(mappings: ValueMapping[]) { + return Math.max.apply(null, mappings.map(mapping => mapping.id).map(m => m)) + 1; + } + + addMapping = () => + this.setState(prevState => ({ + valueMappings: [ + ...prevState.valueMappings, + { + id: prevState.nextIdToAdd, + operator: '', + value: '', + text: '', + type: MappingType.ValueToText, + from: '', + to: '', + }, + ], + nextIdToAdd: prevState.nextIdToAdd + 1, + })); + + onRemoveMapping = (id: number) => { + this.setState( + prevState => ({ + valueMappings: prevState.valueMappings.filter(m => { + return m.id !== id; + }), + }), + () => { + this.props.onChange(this.state.valueMappings); + } + ); + }; + + updateGauge = (mapping: ValueMapping) => { + this.setState( + prevState => ({ + valueMappings: prevState.valueMappings.map(m => { + if (m.id === mapping.id) { + return { ...mapping }; + } + + return m; + }), + }), + () => { + this.props.onChange(this.state.valueMappings); + } + ); + }; + + render() { + const { valueMappings } = this.state; + + return ( + +
+ {valueMappings.length > 0 && + valueMappings.map((valueMapping, index) => ( + this.onRemoveMapping(valueMapping.id)} + /> + ))} +
+
+
+ +
+
Add mapping
+
+
+ ); + } +} diff --git a/public/sass/components/_value-mappings.scss b/packages/grafana-ui/src/components/ValueMappingsEditor/_ValueMappingsEditor.scss similarity index 100% rename from public/sass/components/_value-mappings.scss rename to packages/grafana-ui/src/components/ValueMappingsEditor/_ValueMappingsEditor.scss diff --git a/public/app/plugins/panel/gauge/__snapshots__/ValueMappings.test.tsx.snap b/packages/grafana-ui/src/components/ValueMappingsEditor/__snapshots__/ValueMappingsEditor.test.tsx.snap similarity index 73% rename from public/app/plugins/panel/gauge/__snapshots__/ValueMappings.test.tsx.snap rename to packages/grafana-ui/src/components/ValueMappingsEditor/__snapshots__/ValueMappingsEditor.test.tsx.snap index 8a05cb7e91b..8a465ff88df 100644 --- a/public/app/plugins/panel/gauge/__snapshots__/ValueMappings.test.tsx.snap +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/__snapshots__/ValueMappingsEditor.test.tsx.snap @@ -1,18 +1,15 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Render should render component 1`] = ` -
-
- Value mappings -
-
+ `; diff --git a/packages/grafana-ui/src/components/index.scss b/packages/grafana-ui/src/components/index.scss index d52508c946c..fa8d0135756 100644 --- a/packages/grafana-ui/src/components/index.scss +++ b/packages/grafana-ui/src/components/index.scss @@ -1 +1,9 @@ +@import 'CustomScrollbar/CustomScrollbar'; @import 'DeleteButton/DeleteButton'; +@import 'ThresholdsEditor/ThresholdsEditor'; +@import 'Tooltip/Tooltip'; +@import 'Select/Select'; +@import 'PanelOptionsGroup/PanelOptionsGroup'; +@import 'PanelOptionsGrid/PanelOptionsGrid'; +@import 'ColorPicker/ColorPicker'; +@import 'ValueMappingsEditor/ValueMappingsEditor'; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index b57b9bcfdb7..1b2d2fce972 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -1 +1,22 @@ export { DeleteButton } from './DeleteButton/DeleteButton'; +export { Tooltip } from './Tooltip/Tooltip'; +export { Portal } from './Portal/Portal'; +export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar'; +export { Label } from './Label/Label'; + +// Select +export { Select, AsyncSelect, SelectOptionItem } from './Select/Select'; +export { IndicatorsContainer } from './Select/IndicatorsContainer'; +export { NoOptionsMessage } from './Select/NoOptionsMessage'; +export { default as resetSelectStyles } from './Select/resetSelectStyles'; + +export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder'; +export { ColorPicker } from './ColorPicker/ColorPicker'; +export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover'; +export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker'; +export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor'; +export { GfFormLabel } from './GfFormLabel/GfFormLabel'; +export { Graph } from './Graph/Graph'; +export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup'; +export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid'; +export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor'; diff --git a/packages/grafana-ui/src/forms/index.ts b/packages/grafana-ui/src/forms/index.ts deleted file mode 100644 index bb6998b0025..00000000000 --- a/packages/grafana-ui/src/forms/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { GfFormLabel } from './GfFormLabel/GfFormLabel'; diff --git a/packages/grafana-ui/src/index.scss b/packages/grafana-ui/src/index.scss index 841415620d6..74fdcde3072 100644 --- a/packages/grafana-ui/src/index.scss +++ b/packages/grafana-ui/src/index.scss @@ -1 +1,3 @@ +@import 'vendor/spectrum'; @import 'components/index'; + diff --git a/packages/grafana-ui/src/index.ts b/packages/grafana-ui/src/index.ts index b22152497b9..974d976bbef 100644 --- a/packages/grafana-ui/src/index.ts +++ b/packages/grafana-ui/src/index.ts @@ -1,5 +1,3 @@ export * from './components'; -export * from './visualizations'; export * from './types'; export * from './utils'; -export * from './forms'; diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 44336555a81..7e4012ad529 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -1,6 +1,8 @@ import { TimeSeries, LoadingState } from './series'; import { TimeRange } from './time'; +export type InterpolateFunction = (value: string, format?: string | Function) => string; + export interface PanelProps { timeSeries: TimeSeries[]; timeRange: TimeRange; @@ -9,6 +11,7 @@ export interface PanelProps { renderCounter: number; width: number; height: number; + onInterpolate: InterpolateFunction; } export interface PanelOptionsProps { @@ -29,3 +32,37 @@ export interface PanelMenuItem { shortcut?: string; subMenu?: PanelMenuItem[]; } + +export interface Threshold { + index: number; + value: number; + color?: string; +} + +export enum BasicGaugeColor { + Green = '#299c46', + Red = '#d44a3a', +} + +export enum MappingType { + ValueToText = 1, + RangeToText = 2, +} + +interface BaseMap { + id: number; + operator: string; + text: string; + type: MappingType; +} + +export type ValueMapping = ValueMap | RangeMap; + +export interface ValueMap extends BaseMap { + value: string; +} + +export interface RangeMap extends BaseMap { + from: string; + to: string; +} diff --git a/packages/grafana-ui/src/utils/colors.ts b/packages/grafana-ui/src/utils/colors.ts new file mode 100644 index 00000000000..263d128aec4 --- /dev/null +++ b/packages/grafana-ui/src/utils/colors.ts @@ -0,0 +1,93 @@ +import _ from 'lodash'; +import tinycolor from 'tinycolor2'; + +export const PALETTE_ROWS = 4; +export const PALETTE_COLUMNS = 14; +export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)'; +export const OK_COLOR = 'rgba(11, 237, 50, 1)'; +export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)'; +export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)'; +export const PENDING_COLOR = 'rgba(247, 149, 32, 1)'; +export const REGION_FILL_ALPHA = 0.09; + +export const colors = [ + '#7EB26D', // 0: pale green + '#EAB839', // 1: mustard + '#6ED0E0', // 2: light blue + '#EF843C', // 3: orange + '#E24D42', // 4: red + '#1F78C1', // 5: ocean + '#BA43A9', // 6: purple + '#705DA0', // 7: violet + '#508642', // 8: dark green + '#CCA300', // 9: dark sand + '#447EBC', + '#C15C17', + '#890F02', + '#0A437C', + '#6D1F62', + '#584477', + '#B7DBAB', + '#F4D598', + '#70DBED', + '#F9BA8F', + '#F29191', + '#82B5D8', + '#E5A8E2', + '#AEA2E0', + '#629E51', + '#E5AC0E', + '#64B0C8', + '#E0752D', + '#BF1B00', + '#0A50A1', + '#962D82', + '#614D93', + '#9AC48A', + '#F2C96D', + '#65C5DB', + '#F9934E', + '#EA6460', + '#5195CE', + '#D683CE', + '#806EB7', + '#3F6833', + '#967302', + '#2F575E', + '#99440A', + '#58140C', + '#052B51', + '#511749', + '#3F2B5B', + '#E0F9D7', + '#FCEACA', + '#CFFAFF', + '#F9E2D2', + '#FCE2DE', + '#BADFF4', + '#F9D9F9', + '#DEDAF7', +]; + +function sortColorsByHue(hexColors: string[]) { + const hslColors = _.map(hexColors, hexToHsl); + + const sortedHSLColors = _.sortBy(hslColors, ['h']); + const chunkedHSLColors = _.chunk(sortedHSLColors, PALETTE_ROWS); + const sortedChunkedHSLColors = _.map(chunkedHSLColors, chunk => { + return _.sortBy(chunk, 'l'); + }); + const flattenedZippedSortedChunkedHSLColors = _.flattenDeep(_.zip(...sortedChunkedHSLColors)); + + return _.map(flattenedZippedSortedChunkedHSLColors, hslToHex); +} + +function hexToHsl(color: string) { + return tinycolor(color).toHsl(); +} + +function hslToHex(color: any) { + return tinycolor(color).toHexString(); +} + +export let sortedColors = sortColorsByHue(colors); diff --git a/packages/grafana-ui/src/utils/index.ts b/packages/grafana-ui/src/utils/index.ts index 4d9b9a4b948..aeb65032067 100644 --- a/packages/grafana-ui/src/utils/index.ts +++ b/packages/grafana-ui/src/utils/index.ts @@ -1 +1,3 @@ export * from './processTimeSeries'; +export * from './valueFormats/valueFormats'; +export * from './colors'; diff --git a/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.test.ts b/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.test.ts new file mode 100644 index 00000000000..44332a51307 --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.test.ts @@ -0,0 +1,40 @@ +import { toHex, toHex0x } from './arithmeticFormatters'; + +describe('hex', () => { + it('positive integer', () => { + const str = toHex(100, 0); + expect(str).toBe('64'); + }); + it('negative integer', () => { + const str = toHex(-100, 0); + expect(str).toBe('-64'); + }); + it('positive float', () => { + const str = toHex(50.52, 1); + expect(str).toBe('32.8'); + }); + it('negative float', () => { + const str = toHex(-50.333, 2); + expect(str).toBe('-32.547AE147AE14'); + }); +}); + +describe('hex 0x', () => { + it('positive integeter', () => { + const str = toHex0x(7999, 0); + expect(str).toBe('0x1F3F'); + }); + it('negative integer', () => { + const str = toHex0x(-584, 0); + expect(str).toBe('-0x248'); + }); + + it('positive float', () => { + const str = toHex0x(74.443, 3); + expect(str).toBe('0x4A.716872B020C4'); + }); + it('negative float', () => { + const str = toHex0x(-65.458, 1); + expect(str).toBe('-0x41.8'); + }); +}); diff --git a/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.ts b/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.ts new file mode 100644 index 00000000000..fa9daf0fb97 --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.ts @@ -0,0 +1,42 @@ +import { toFixed } from './valueFormats'; + +export function toPercent(size: number, decimals: number) { + if (size === null) { + return ''; + } + return toFixed(size, decimals) + '%'; +} + +export function toPercentUnit(size: number, decimals: number) { + if (size === null) { + return ''; + } + return toFixed(100 * size, decimals) + '%'; +} + +export function toHex0x(value: number, decimals: number) { + if (value == null) { + return ''; + } + const hexString = toHex(value, decimals); + if (hexString.substring(0, 1) === '-') { + return '-0x' + hexString.substring(1); + } + return '0x' + hexString; +} + +export function toHex(value: number, decimals: number) { + if (value == null) { + return ''; + } + return parseFloat(toFixed(value, decimals)) + .toString(16) + .toUpperCase(); +} + +export function sci(value: number, decimals: number) { + if (value == null) { + return ''; + } + return value.toExponential(decimals); +} diff --git a/packages/grafana-ui/src/utils/valueFormats/categories.ts b/packages/grafana-ui/src/utils/valueFormats/categories.ts new file mode 100644 index 00000000000..d7410c22276 --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/categories.ts @@ -0,0 +1,322 @@ +import { locale, scaledUnits, simpleCountUnit, toFixed, toFixedUnit, ValueFormatCategory } from './valueFormats'; +import { + dateTimeAsIso, + dateTimeAsUS, + dateTimeFromNow, + toClockMilliseconds, + toClockSeconds, + toDays, + toDurationInHoursMinutesSeconds, + toDurationInMilliseconds, + toDurationInSeconds, + toHours, + toMicroSeconds, + toMilliSeconds, + toMinutes, + toNanoSeconds, + toSeconds, + toTimeTicks, +} from './dateTimeFormatters'; +import { toHex, sci, toHex0x, toPercent, toPercentUnit } from './arithmeticFormatters'; +import { binarySIPrefix, currency, decimalSIPrefix } from './symbolFormatters'; + +export const getCategories = (): ValueFormatCategory[] => [ + { + name: 'Misc', + formats: [ + { name: 'none', id: 'none', fn: toFixed }, + { + name: 'short', + id: 'short', + fn: scaledUnits(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Quadr', ' Quint', ' Sext', ' Sept']), + }, + { name: 'percent (0-100)', id: 'percent', fn: toPercent }, + { name: 'percent (0.0-1.0)', id: 'percentunit', fn: toPercentUnit }, + { name: 'Humidity (%H)', id: 'humidity', fn: toFixedUnit('%H') }, + { name: 'decibel', id: 'dB', fn: toFixedUnit('dB') }, + { name: 'hexadecimal (0x)', id: 'hex0x', fn: toHex0x }, + { name: 'hexadecimal', id: 'hex', fn: toHex }, + { name: 'scientific notation', id: 'sci', fn: sci }, + { name: 'locale format', id: 'locale', fn: locale }, + ], + }, + { + name: 'Acceleration', + formats: [ + { name: 'Meters/sec²', id: 'accMS2', fn: toFixedUnit('m/sec²') }, + { name: 'Feet/sec²', id: 'accFS2', fn: toFixedUnit('f/sec²') }, + { name: 'G unit', id: 'accG', fn: toFixedUnit('g') }, + ], + }, + { + name: 'Angle', + formats: [ + { name: 'Degrees (°)', id: 'degree', fn: toFixedUnit('°') }, + { name: 'Radians', id: 'radian', fn: toFixedUnit('rad') }, + { name: 'Gradian', id: 'grad', fn: toFixedUnit('grad') }, + ], + }, + { + name: 'Area', + formats: [ + { name: 'Square Meters (m²)', id: 'areaM2', fn: toFixedUnit('m²') }, + { name: 'Square Feet (ft²)', id: 'areaF2', fn: toFixedUnit('ft²') }, + { name: 'Square Miles (mi²)', id: 'areaMI2', fn: toFixedUnit('mi²') }, + ], + }, + { + name: 'Computation', + formats: [ + { name: 'FLOP/s', id: 'flops', fn: decimalSIPrefix('FLOP/s') }, + { name: 'MFLOP/s', id: 'mflops', fn: decimalSIPrefix('FLOP/s', 2) }, + { name: 'GFLOP/s', id: 'gflops', fn: decimalSIPrefix('FLOP/s', 3) }, + { name: 'TFLOP/s', id: 'tflops', fn: decimalSIPrefix('FLOP/s', 4) }, + { name: 'PFLOP/s', id: 'pflops', fn: decimalSIPrefix('FLOP/s', 5) }, + { name: 'EFLOP/s', id: 'eflops', fn: decimalSIPrefix('FLOP/s', 6) }, + ], + }, + { + name: 'Concentration', + formats: [ + { name: 'parts-per-million (ppm)', id: 'ppm', fn: toFixedUnit('ppm') }, + { name: 'parts-per-billion (ppb)', id: 'conppb', fn: toFixedUnit('ppb') }, + { name: 'nanogram per cubic meter (ng/m³)', id: 'conngm3', fn: toFixedUnit('ng/m³') }, + { name: 'nanogram per normal cubic meter (ng/Nm³)', id: 'conngNm3', fn: toFixedUnit('ng/Nm³') }, + { name: 'microgram per cubic meter (μg/m³)', id: 'conμgm3', fn: toFixedUnit('μg/m³') }, + { name: 'microgram per normal cubic meter (μg/Nm³)', id: 'conμgNm3', fn: toFixedUnit('μg/Nm³') }, + { name: 'milligram per cubic meter (mg/m³)', id: 'conmgm3', fn: toFixedUnit('mg/m³') }, + { name: 'milligram per normal cubic meter (mg/Nm³)', id: 'conmgNm3', fn: toFixedUnit('mg/Nm³') }, + { name: 'gram per cubic meter (g/m³)', id: 'congm3', fn: toFixedUnit('g/m³') }, + { name: 'gram per normal cubic meter (g/Nm³)', id: 'congNm3', fn: toFixedUnit('g/Nm³') }, + { name: 'milligrams per decilitre (mg/dL)', id: 'conmgdL', fn: toFixedUnit('mg/dL') }, + { name: 'millimoles per litre (mmol/L)', id: 'conmmolL', fn: toFixedUnit('mmol/L') }, + ], + }, + { + name: 'Currency', + formats: [ + { name: 'Dollars ($)', id: 'currencyUSD', fn: currency('$') }, + { name: 'Pounds (£)', id: 'currencyGBP', fn: currency('£') }, + { name: 'Euro (€)', id: 'currencyEUR', fn: currency('€') }, + { name: 'Yen (¥)', id: 'currencyJPY', fn: currency('¥') }, + { name: 'Rubles (₽)', id: 'currencyRUB', fn: currency('₽') }, + { name: 'Hryvnias (₴)', id: 'currencyUAH', fn: currency('₴') }, + { name: 'Real (R$)', id: 'currencyBRL', fn: currency('R$') }, + { name: 'Danish Krone (kr)', id: 'currencyDKK', fn: currency('kr') }, + { name: 'Icelandic Króna (kr)', id: 'currencyISK', fn: currency('kr') }, + { name: 'Norwegian Krone (kr)', id: 'currencyNOK', fn: currency('kr') }, + { name: 'Swedish Krona (kr)', id: 'currencySEK', fn: currency('kr') }, + { name: 'Czech koruna (czk)', id: 'currencyCZK', fn: currency('czk') }, + { name: 'Swiss franc (CHF)', id: 'currencyCHF', fn: currency('CHF') }, + { name: 'Polish Złoty (PLN)', id: 'currencyPLN', fn: currency('PLN') }, + { name: 'Bitcoin (฿)', id: 'currencyBTC', fn: currency('฿') }, + ], + }, + { + name: 'Data (IEC)', + formats: [ + { name: 'bits', id: 'bits', fn: binarySIPrefix('b') }, + { name: 'bytes', id: 'bytes', fn: binarySIPrefix('B') }, + { name: 'kibibytes', id: 'kbytes', fn: binarySIPrefix('B', 1) }, + { name: 'mebibytes', id: 'mbytes', fn: binarySIPrefix('B', 2) }, + { name: 'gibibytes', id: 'gbytes', fn: binarySIPrefix('B', 3) }, + ], + }, + { + name: 'Data (Metric)', + formats: [ + { name: 'bits', id: 'decbits', fn: decimalSIPrefix('d') }, + { name: 'bytes', id: 'decbytes', fn: decimalSIPrefix('B') }, + { name: 'kilobytes', id: 'deckbytes', fn: decimalSIPrefix('B', 1) }, + { name: 'megabytes', id: 'decmbytes', fn: decimalSIPrefix('B', 2) }, + { name: 'gigabytes', id: 'decgbytes', fn: decimalSIPrefix('B', 3) }, + ], + }, + { + name: 'Data Rate', + formats: [ + { name: 'packets/sec', id: 'pps', fn: decimalSIPrefix('pps') }, + { name: 'bits/sec', id: 'bps', fn: decimalSIPrefix('bps') }, + { name: 'bytes/sec', id: 'Bps', fn: decimalSIPrefix('B/s') }, + { name: 'kilobytes/sec', id: 'KBs', fn: decimalSIPrefix('Bs', 1) }, + { name: 'kilobits/sec', id: 'Kbits', fn: decimalSIPrefix('bps', 1) }, + { name: 'megabytes/sec', id: 'MBs', fn: decimalSIPrefix('Bs', 2) }, + { name: 'megabits/sec', id: 'Mbits', fn: decimalSIPrefix('bps', 2) }, + { name: 'gigabytes/sec', id: 'GBs', fn: decimalSIPrefix('Bs', 3) }, + { name: 'gigabits/sec', id: 'Gbits', fn: decimalSIPrefix('bps', 3) }, + ], + }, + { + name: 'Date & Time', + formats: [ + { name: 'YYYY-MM-DD HH:mm:ss', id: 'dateTimeAsIso', fn: dateTimeAsIso }, + { name: 'DD/MM/YYYY h:mm:ss a', id: 'dateTimeAsUS', fn: dateTimeAsUS }, + { name: 'From Now', id: 'dateTimeFromNow', fn: dateTimeFromNow }, + ], + }, + { + name: 'Energy', + formats: [ + { name: 'Watt (W)', id: 'watt', fn: decimalSIPrefix('W') }, + { name: 'Kilowatt (kW)', id: 'kwatt', fn: decimalSIPrefix('W', 1) }, + { name: 'Milliwatt (mW)', id: 'mwatt', fn: decimalSIPrefix('W', -1) }, + { name: 'Watt per square meter (W/m²)', id: 'Wm2', fn: toFixedUnit('W/m²') }, + { name: 'Volt-ampere (VA)', id: 'voltamp', fn: decimalSIPrefix('VA') }, + { name: 'Kilovolt-ampere (kVA)', id: 'kvoltamp', fn: decimalSIPrefix('VA', 1) }, + { name: 'Volt-ampere reactive (var)', id: 'voltampreact', fn: decimalSIPrefix('var') }, + { name: 'Kilovolt-ampere reactive (kvar)', id: 'kvoltampreact', fn: decimalSIPrefix('var', 1) }, + { name: 'Watt-hour (Wh)', id: 'watth', fn: decimalSIPrefix('Wh') }, + { name: 'Kilowatt-hour (kWh)', id: 'kwatth', fn: decimalSIPrefix('Wh', 1) }, + { name: 'Kilowatt-min (kWm)', id: 'kwattm', fn: decimalSIPrefix('W/Min', 1) }, + { name: 'Joule (J)', id: 'joule', fn: decimalSIPrefix('J') }, + { name: 'Electron volt (eV)', id: 'ev', fn: decimalSIPrefix('eV') }, + { name: 'Ampere (A)', id: 'amp', fn: decimalSIPrefix('A') }, + { name: 'Kiloampere (kA)', id: 'kamp', fn: decimalSIPrefix('A', 1) }, + { name: 'Milliampere (mA)', id: 'mamp', fn: decimalSIPrefix('A', -1) }, + { name: 'Volt (V)', id: 'volt', fn: decimalSIPrefix('V') }, + { name: 'Kilovolt (kV)', id: 'kvolt', fn: decimalSIPrefix('V', 1) }, + { name: 'Millivolt (mV)', id: 'mvolt', fn: decimalSIPrefix('V', -1) }, + { name: 'Decibel-milliwatt (dBm)', id: 'dBm', fn: decimalSIPrefix('dBm') }, + { name: 'Ohm (Ω)', id: 'ohm', fn: decimalSIPrefix('Ω') }, + { name: 'Lumens (Lm)', id: 'lumens', fn: decimalSIPrefix('Lm') }, + ], + }, + { + name: 'Flow', + formats: [ + { name: 'Gallons/min (gpm)', id: 'flowgpm', fn: toFixedUnit('gpm') }, + { name: 'Cubic meters/sec (cms)', id: 'flowcms', fn: toFixedUnit('cms') }, + { name: 'Cubic feet/sec (cfs)', id: 'flowcfs', fn: toFixedUnit('cfs') }, + { name: 'Cubic feet/min (cfm)', id: 'flowcfm', fn: toFixedUnit('cfm') }, + { name: 'Litre/hour', id: 'litreh', fn: toFixedUnit('l/h') }, + { name: 'Litre/min (l/min)', id: 'flowlpm', fn: toFixedUnit('l/min') }, + { name: 'milliLitre/min (mL/min)', id: 'flowmlpm', fn: toFixedUnit('mL/min') }, + ], + }, + { + name: 'Force', + formats: [ + { name: 'Newton-meters (Nm)', id: 'forceNm', fn: decimalSIPrefix('Nm') }, + { name: 'Kilonewton-meters (kNm)', id: 'forcekNm', fn: decimalSIPrefix('Nm', 1) }, + { name: 'Newtons (N)', id: 'forceN', fn: decimalSIPrefix('N') }, + { name: 'Kilonewtons (kN)', id: 'forcekN', fn: decimalSIPrefix('N', 1) }, + ], + }, + { + name: 'Hash Rate', + formats: [ + { name: 'hashes/sec', id: 'Hs', fn: decimalSIPrefix('H/s') }, + { name: 'kilohashes/sec', id: 'KHs', fn: decimalSIPrefix('H/s', 1) }, + { name: 'megahashes/sec', id: 'MHs', fn: decimalSIPrefix('H/s', 2) }, + { name: 'gigahashes/sec', id: 'GHs', fn: decimalSIPrefix('H/s', 3) }, + { name: 'terahashes/sec', id: 'THs', fn: decimalSIPrefix('H/s', 4) }, + { name: 'petahashes/sec', id: 'PHs', fn: decimalSIPrefix('H/s', 5) }, + { name: 'exahashes/sec', id: 'EHs', fn: decimalSIPrefix('H/s', 6) }, + ], + }, + { + name: 'Mass', + formats: [ + { name: 'milligram (mg)', id: 'massmg', fn: decimalSIPrefix('g', -1) }, + { name: 'gram (g)', id: 'massg', fn: decimalSIPrefix('g') }, + { name: 'kilogram (kg)', id: 'masskg', fn: decimalSIPrefix('g', 1) }, + { name: 'metric ton (t)', id: 'masst', fn: toFixedUnit('t') }, + ], + }, + { + name: 'length', + formats: [ + { name: 'millimetre (mm)', id: 'lengthmm', fn: decimalSIPrefix('m', -1) }, + { name: 'feet (ft)', id: 'lengthft', fn: toFixedUnit('ft') }, + { name: 'meter (m)', id: 'lengthm', fn: decimalSIPrefix('m') }, + { name: 'kilometer (km)', id: 'lengthkm', fn: decimalSIPrefix('m', 1) }, + { name: 'mile (mi)', id: 'lengthmi', fn: toFixedUnit('mi') }, + ], + }, + { + name: 'Pressure', + formats: [ + { name: 'Millibars', id: 'pressurembar', fn: decimalSIPrefix('bar', -1) }, + { name: 'Bars', id: 'pressurebar', fn: decimalSIPrefix('bar') }, + { name: 'Kilobars', id: 'pressurekbar', fn: decimalSIPrefix('bar', 1) }, + { name: 'Hectopascals', id: 'pressurehpa', fn: toFixedUnit('hPa') }, + { name: 'Kilopascals', id: 'pressurekpa', fn: toFixedUnit('kPa') }, + { name: 'Inches of mercury', id: 'pressurehg', fn: toFixedUnit('"Hg') }, + { name: 'PSI', id: 'pressurepsi', fn: scaledUnits(1000, ['psi', 'ksi', 'Mpsi']) }, + ], + }, + { + name: 'Radiation', + formats: [ + { name: 'Becquerel (Bq)', id: 'radbq', fn: decimalSIPrefix('Bq') }, + { name: 'curie (Ci)', id: 'radci', fn: decimalSIPrefix('Ci') }, + { name: 'Gray (Gy)', id: 'radgy', fn: decimalSIPrefix('Gy') }, + { name: 'rad', id: 'radrad', fn: decimalSIPrefix('rad') }, + { name: 'Sievert (Sv)', id: 'radsv', fn: decimalSIPrefix('Sv') }, + { name: 'rem', id: 'radrem', fn: decimalSIPrefix('rem') }, + { name: 'Exposure (C/kg)', id: 'radexpckg', fn: decimalSIPrefix('C/kg') }, + { name: 'roentgen (R)', id: 'radr', fn: decimalSIPrefix('R') }, + { name: 'Sievert/hour (Sv/h)', id: 'radsvh', fn: decimalSIPrefix('Sv/h') }, + ], + }, + { + name: 'Temperature', + formats: [ + { name: 'Celsius (°C)', id: 'celsius', fn: toFixedUnit('°C') }, + { name: 'Farenheit (°F)', id: 'farenheit', fn: toFixedUnit('°F') }, + { name: 'Kelvin (K)', id: 'kelvin', fn: toFixedUnit('K') }, + ], + }, + { + name: 'Time', + formats: [ + { name: 'Hertz (1/s)', id: 'hertz', fn: decimalSIPrefix('Hz') }, + { name: 'nanoseconds (ns)', id: 'ns', fn: toNanoSeconds }, + { name: 'microseconds (µs)', id: 'µs', fn: toMicroSeconds }, + { name: 'milliseconds (ms)', id: 'ms', fn: toMilliSeconds }, + { name: 'seconds (s)', id: 's', fn: toSeconds }, + { name: 'minutes (m)', id: 'm', fn: toMinutes }, + { name: 'hours (h)', id: 'h', fn: toHours }, + { name: 'days (d)', id: 'd', fn: toDays }, + { name: 'duration (ms)', id: 'dtdurationms', fn: toDurationInMilliseconds }, + { name: 'duration (s)', id: 'dtdurations', fn: toDurationInSeconds }, + { name: 'duration (hh:mm:ss)', id: 'dthms', fn: toDurationInHoursMinutesSeconds }, + { name: 'Timeticks (s/100)', id: 'timeticks', fn: toTimeTicks }, + { name: 'clock (ms)', id: 'clockms', fn: toClockMilliseconds }, + { name: 'clock (s)', id: 'clocks', fn: toClockSeconds }, + ], + }, + { + name: 'Throughput', + formats: [ + { name: 'ops/sec (ops)', id: 'ops', fn: simpleCountUnit('ops') }, + { name: 'requests/sec (rps)', id: 'reqps', fn: simpleCountUnit('reqps') }, + { name: 'reads/sec (rps)', id: 'rps', fn: simpleCountUnit('rps') }, + { name: 'writes/sec (wps)', id: 'wps', fn: simpleCountUnit('wps') }, + { name: 'I/O ops/sec (iops)', id: 'iops', fn: simpleCountUnit('iops') }, + { name: 'ops/min (opm)', id: 'opm', fn: simpleCountUnit('opm') }, + { name: 'reads/min (rpm)', id: 'rpm', fn: simpleCountUnit('rpm') }, + { name: 'writes/min (wpm)', id: 'wpm', fn: simpleCountUnit('wpm') }, + ], + }, + { + name: 'Velocity', + formats: [ + { name: 'metres/second (m/s)', id: 'velocityms', fn: toFixedUnit('m/s') }, + { name: 'kilometers/hour (km/h)', id: 'velocitykmh', fn: toFixedUnit('km/h') }, + { name: 'miles/hour (mph)', id: 'velocitymph', fn: toFixedUnit('mph') }, + { name: 'knot (kn)', id: 'velocityknot', fn: toFixedUnit('kn') }, + ] + }, + { + name: 'Volume', + formats: [ + { name: 'millilitre (mL)', id: 'mlitre', fn: decimalSIPrefix('L', -1) }, + { name: 'litre (L)', id: 'litre', fn: decimalSIPrefix('L') }, + { name: 'cubic metre', id: 'm3', fn: toFixedUnit('m³') }, + { name: 'Normal cubic metre', id: 'Nm3', fn: toFixedUnit('Nm³') }, + { name: 'cubic decimetre', id: 'dm3', fn: toFixedUnit('dm³') }, + { name: 'gallons', id: 'gallons', fn: toFixedUnit('gal') }, + ], + } +]; diff --git a/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.test.ts b/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.test.ts new file mode 100644 index 00000000000..cf69a1d433a --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.test.ts @@ -0,0 +1,231 @@ +import moment from 'moment'; +import { + dateTimeAsIso, + dateTimeAsUS, + dateTimeFromNow, + Interval, + toClock, + toDuration, + toDurationInMilliseconds, + toDurationInSeconds, +} from './dateTimeFormatters'; + +describe('date time formats', () => { + const epoch = 1505634997920; + const utcTime = moment.utc(epoch); + const browserTime = moment(epoch); + + it('should format as iso date', () => { + const expected = browserTime.format('YYYY-MM-DD HH:mm:ss'); + const actual = dateTimeAsIso(epoch, 0, 0, false); + expect(actual).toBe(expected); + }); + + it('should format as iso date (in UTC)', () => { + const expected = utcTime.format('YYYY-MM-DD HH:mm:ss'); + const actual = dateTimeAsIso(epoch, 0, 0, true); + expect(actual).toBe(expected); + }); + + it('should format as iso date and skip date when today', () => { + const now = moment(); + const expected = now.format('HH:mm:ss'); + const actual = dateTimeAsIso(now.valueOf(), 0, 0, false); + expect(actual).toBe(expected); + }); + + it('should format as iso date (in UTC) and skip date when today', () => { + const now = moment.utc(); + const expected = now.format('HH:mm:ss'); + const actual = dateTimeAsIso(now.valueOf(), 0, 0, true); + expect(actual).toBe(expected); + }); + + it('should format as US date', () => { + const expected = browserTime.format('MM/DD/YYYY h:mm:ss a'); + const actual = dateTimeAsUS(epoch, 0, 0, false); + expect(actual).toBe(expected); + }); + + it('should format as US date (in UTC)', () => { + const expected = utcTime.format('MM/DD/YYYY h:mm:ss a'); + const actual = dateTimeAsUS(epoch, 0, 0, true); + expect(actual).toBe(expected); + }); + + it('should format as US date and skip date when today', () => { + const now = moment(); + const expected = now.format('h:mm:ss a'); + const actual = dateTimeAsUS(now.valueOf(), 0, 0, false); + expect(actual).toBe(expected); + }); + + it('should format as US date (in UTC) and skip date when today', () => { + const now = moment.utc(); + const expected = now.format('h:mm:ss a'); + const actual = dateTimeAsUS(now.valueOf(), 0, 0, true); + expect(actual).toBe(expected); + }); + + it('should format as from now with days', () => { + const daysAgo = moment().add(-7, 'd'); + const expected = '7 days ago'; + const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, false); + expect(actual).toBe(expected); + }); + + it('should format as from now with days (in UTC)', () => { + const daysAgo = moment.utc().add(-7, 'd'); + const expected = '7 days ago'; + const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, true); + expect(actual).toBe(expected); + }); + + it('should format as from now with minutes', () => { + const daysAgo = moment().add(-2, 'm'); + const expected = '2 minutes ago'; + const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, false); + expect(actual).toBe(expected); + }); + + it('should format as from now with minutes (in UTC)', () => { + const daysAgo = moment.utc().add(-2, 'm'); + const expected = '2 minutes ago'; + const actual = dateTimeFromNow(daysAgo.valueOf(), 0, 0, true); + expect(actual).toBe(expected); + }); +}); + +describe('duration', () => { + it('0 milliseconds', () => { + const str = toDurationInMilliseconds(0, 0); + expect(str).toBe('0 milliseconds'); + }); + it('1 millisecond', () => { + const str = toDurationInMilliseconds(1, 0); + expect(str).toBe('1 millisecond'); + }); + it('-1 millisecond', () => { + const str = toDurationInMilliseconds(-1, 0); + expect(str).toBe('1 millisecond ago'); + }); + it('seconds', () => { + const str = toDurationInSeconds(1, 0); + expect(str).toBe('1 second'); + }); + it('minutes', () => { + const str = toDuration(1, 0, Interval.Minute); + expect(str).toBe('1 minute'); + }); + it('hours', () => { + const str = toDuration(1, 0, Interval.Hour); + expect(str).toBe('1 hour'); + }); + it('days', () => { + const str = toDuration(1, 0, Interval.Day); + expect(str).toBe('1 day'); + }); + it('weeks', () => { + const str = toDuration(1, 0, Interval.Week); + expect(str).toBe('1 week'); + }); + it('months', () => { + const str = toDuration(1, 0, Interval.Month); + expect(str).toBe('1 month'); + }); + it('years', () => { + const str = toDuration(1, 0, Interval.Year); + expect(str).toBe('1 year'); + }); + it('decimal days', () => { + const str = toDuration(1.5, 2, Interval.Day); + expect(str).toBe('1 day, 12 hours, 0 minutes'); + }); + it('decimal months', () => { + const str = toDuration(1.5, 3, Interval.Month); + expect(str).toBe('1 month, 2 weeks, 1 day, 0 hours'); + }); + it('no decimals', () => { + const str = toDuration(38898367008, 0, Interval.Millisecond); + expect(str).toBe('1 year'); + }); + it('1 decimal', () => { + const str = toDuration(38898367008, 1, Interval.Millisecond); + expect(str).toBe('1 year, 2 months'); + }); + it('too many decimals', () => { + const str = toDuration(38898367008, 20, Interval.Millisecond); + expect(str).toBe('1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds, 8 milliseconds'); + }); + it('floating point error', () => { + const str = toDuration(36993906007, 8, Interval.Millisecond); + expect(str).toBe('1 year, 2 months, 0 weeks, 3 days, 4 hours, 5 minutes, 6 seconds, 7 milliseconds'); + }); +}); + +describe('clock', () => { + it('size less than 1 second', () => { + const str = toClock(999, 0); + expect(str).toBe('999ms'); + }); + describe('size less than 1 minute', () => { + it('default', () => { + const str = toClock(59999); + expect(str).toBe('59s:999ms'); + }); + it('decimals equals 0', () => { + const str = toClock(59999, 0); + expect(str).toBe('59s'); + }); + }); + describe('size less than 1 hour', () => { + it('default', () => { + const str = toClock(3599999); + expect(str).toBe('59m:59s:999ms'); + }); + it('decimals equals 0', () => { + const str = toClock(3599999, 0); + expect(str).toBe('59m'); + }); + it('decimals equals 1', () => { + const str = toClock(3599999, 1); + expect(str).toBe('59m:59s'); + }); + }); + describe('size greater than or equal 1 hour', () => { + it('default', () => { + const str = toClock(7199999); + expect(str).toBe('01h:59m:59s:999ms'); + }); + it('decimals equals 0', () => { + const str = toClock(7199999, 0); + expect(str).toBe('01h'); + }); + it('decimals equals 1', () => { + const str = toClock(7199999, 1); + expect(str).toBe('01h:59m'); + }); + it('decimals equals 2', () => { + const str = toClock(7199999, 2); + expect(str).toBe('01h:59m:59s'); + }); + }); + describe('size greater than or equal 1 day', () => { + it('default', () => { + const str = toClock(89999999); + expect(str).toBe('24h:59m:59s:999ms'); + }); + it('decimals equals 0', () => { + const str = toClock(89999999, 0); + expect(str).toBe('24h'); + }); + it('decimals equals 1', () => { + const str = toClock(89999999, 1); + expect(str).toBe('24h:59m'); + }); + it('decimals equals 2', () => { + const str = toClock(89999999, 2); + expect(str).toBe('24h:59m:59s'); + }); + }); +}); diff --git a/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts b/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts new file mode 100644 index 00000000000..1e07857eb66 --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts @@ -0,0 +1,312 @@ +import { toFixed, toFixedScaled } from './valueFormats'; +import moment from 'moment'; + +interface IntervalsInSeconds { + [interval: string]: number; +} + +export enum Interval { + Year = 'year', + Month = 'month', + Week = 'week', + Day = 'day', + Hour = 'hour', + Minute = 'minute', + Second = 'second', + Millisecond = 'millisecond', +} + +const INTERVALS_IN_SECONDS: IntervalsInSeconds = { + [Interval.Year]: 31536000, + [Interval.Month]: 2592000, + [Interval.Week]: 604800, + [Interval.Day]: 86400, + [Interval.Hour]: 3600, + [Interval.Minute]: 60, + [Interval.Second]: 1, + [Interval.Millisecond]: 0.001, +}; + +export function toNanoSeconds(size: number, decimals: number, scaledDecimals: number) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 1000) { + return toFixed(size, decimals) + ' ns'; + } else if (Math.abs(size) < 1000000) { + return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' µs'); + } else if (Math.abs(size) < 1000000000) { + return toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' ms'); + } else if (Math.abs(size) < 60000000000) { + return toFixedScaled(size / 1000000000, decimals, scaledDecimals, 9, ' s'); + } else { + return toFixedScaled(size / 60000000000, decimals, scaledDecimals, 12, ' min'); + } +} + +export function toMicroSeconds(size: number, decimals: number, scaledDecimals: number) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 1000) { + return toFixed(size, decimals) + ' µs'; + } else if (Math.abs(size) < 1000000) { + return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' ms'); + } else { + return toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' s'); + } +} + +export function toMilliSeconds(size: number, decimals: number, scaledDecimals: number) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 1000) { + return toFixed(size, decimals) + ' ms'; + } else if (Math.abs(size) < 60000) { + // Less than 1 min + return toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' s'); + } else if (Math.abs(size) < 3600000) { + // Less than 1 hour, divide in minutes + return toFixedScaled(size / 60000, decimals, scaledDecimals, 5, ' min'); + } else if (Math.abs(size) < 86400000) { + // Less than one day, divide in hours + return toFixedScaled(size / 3600000, decimals, scaledDecimals, 7, ' hour'); + } else if (Math.abs(size) < 31536000000) { + // Less than one year, divide in days + return toFixedScaled(size / 86400000, decimals, scaledDecimals, 8, ' day'); + } + + return toFixedScaled(size / 31536000000, decimals, scaledDecimals, 10, ' year'); +} + +export function toSeconds(size: number, decimals: number, scaledDecimals: number) { + if (size === null) { + return ''; + } + + // Less than 1 µs, divide in ns + if (Math.abs(size) < 0.000001) { + return toFixedScaled(size * 1e9, decimals, scaledDecimals - decimals, -9, ' ns'); + } + // Less than 1 ms, divide in µs + if (Math.abs(size) < 0.001) { + return toFixedScaled(size * 1e6, decimals, scaledDecimals - decimals, -6, ' µs'); + } + // Less than 1 second, divide in ms + if (Math.abs(size) < 1) { + return toFixedScaled(size * 1e3, decimals, scaledDecimals - decimals, -3, ' ms'); + } + + if (Math.abs(size) < 60) { + return toFixed(size, decimals) + ' s'; + } else if (Math.abs(size) < 3600) { + // Less than 1 hour, divide in minutes + return toFixedScaled(size / 60, decimals, scaledDecimals, 1, ' min'); + } else if (Math.abs(size) < 86400) { + // Less than one day, divide in hours + return toFixedScaled(size / 3600, decimals, scaledDecimals, 4, ' hour'); + } else if (Math.abs(size) < 604800) { + // Less than one week, divide in days + return toFixedScaled(size / 86400, decimals, scaledDecimals, 5, ' day'); + } else if (Math.abs(size) < 31536000) { + // Less than one year, divide in week + return toFixedScaled(size / 604800, decimals, scaledDecimals, 6, ' week'); + } + + return toFixedScaled(size / 3.15569e7, decimals, scaledDecimals, 7, ' year'); +} + +export function toMinutes(size: number, decimals: number, scaledDecimals: number) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 60) { + return toFixed(size, decimals) + ' min'; + } else if (Math.abs(size) < 1440) { + return toFixedScaled(size / 60, decimals, scaledDecimals, 2, ' hour'); + } else if (Math.abs(size) < 10080) { + return toFixedScaled(size / 1440, decimals, scaledDecimals, 3, ' day'); + } else if (Math.abs(size) < 604800) { + return toFixedScaled(size / 10080, decimals, scaledDecimals, 4, ' week'); + } else { + return toFixedScaled(size / 5.25948e5, decimals, scaledDecimals, 5, ' year'); + } +} + +export function toHours(size: number, decimals: number, scaledDecimals: number) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 24) { + return toFixed(size, decimals) + ' hour'; + } else if (Math.abs(size) < 168) { + return toFixedScaled(size / 24, decimals, scaledDecimals, 2, ' day'); + } else if (Math.abs(size) < 8760) { + return toFixedScaled(size / 168, decimals, scaledDecimals, 3, ' week'); + } else { + return toFixedScaled(size / 8760, decimals, scaledDecimals, 4, ' year'); + } +} + +export function toDays(size: number, decimals: number, scaledDecimals: number) { + if (size === null) { + return ''; + } + + if (Math.abs(size) < 7) { + return toFixed(size, decimals) + ' day'; + } else if (Math.abs(size) < 365) { + return toFixedScaled(size / 7, decimals, scaledDecimals, 2, ' week'); + } else { + return toFixedScaled(size / 365, decimals, scaledDecimals, 3, ' year'); + } +} + +export function toDuration(size: number, decimals: number, timeScale: Interval): string { + if (size === null) { + return ''; + } + if (size === 0) { + return '0 ' + timeScale + 's'; + } + if (size < 0) { + return toDuration(-size, decimals, timeScale) + ' ago'; + } + + const units = [ + { long: Interval.Year }, + { long: Interval.Month }, + { long: Interval.Week }, + { long: Interval.Day }, + { long: Interval.Hour }, + { long: Interval.Minute }, + { long: Interval.Second }, + { long: Interval.Millisecond }, + ]; + // convert $size to milliseconds + // intervals_in_seconds uses seconds (duh), convert them to milliseconds here to minimize floating point errors + size *= INTERVALS_IN_SECONDS[timeScale] * 1000; + + const strings = []; + // after first value >= 1 print only $decimals more + let decrementDecimals = false; + for (let i = 0; i < units.length && decimals >= 0; i++) { + const interval = INTERVALS_IN_SECONDS[units[i].long] * 1000; + const value = size / interval; + if (value >= 1 || decrementDecimals) { + decrementDecimals = true; + const floor = Math.floor(value); + const unit = units[i].long + (floor !== 1 ? 's' : ''); + strings.push(floor + ' ' + unit); + size = size % interval; + decimals--; + } + } + + return strings.join(', '); +} + +export function toClock(size: number, decimals?: number) { + if (size === null) { + return ''; + } + + // < 1 second + if (size < 1000) { + return moment.utc(size).format('SSS\\m\\s'); + } + + // < 1 minute + if (size < 60000) { + let format = 'ss\\s:SSS\\m\\s'; + if (decimals === 0) { + format = 'ss\\s'; + } + return moment.utc(size).format(format); + } + + // < 1 hour + if (size < 3600000) { + let format = 'mm\\m:ss\\s:SSS\\m\\s'; + if (decimals === 0) { + format = 'mm\\m'; + } else if (decimals === 1) { + format = 'mm\\m:ss\\s'; + } + return moment.utc(size).format(format); + } + + let format = 'mm\\m:ss\\s:SSS\\m\\s'; + + const hours = `${('0' + Math.floor(moment.duration(size, 'milliseconds').asHours())).slice(-2)}h`; + + if (decimals === 0) { + format = ''; + } else if (decimals === 1) { + format = 'mm\\m'; + } else if (decimals === 2) { + format = 'mm\\m:ss\\s'; + } + + return format ? `${hours}:${moment.utc(size).format(format)}` : hours; +} + +export function toDurationInMilliseconds(size: number, decimals: number) { + return toDuration(size, decimals, Interval.Millisecond); +} + +export function toDurationInSeconds(size: number, decimals: number) { + return toDuration(size, decimals, Interval.Second); +} + +export function toDurationInHoursMinutesSeconds(size: number) { + const strings = []; + const numHours = Math.floor(size / 3600); + const numMinutes = Math.floor((size % 3600) / 60); + const numSeconds = Math.floor((size % 3600) % 60); + numHours > 9 ? strings.push('' + numHours) : strings.push('0' + numHours); + numMinutes > 9 ? strings.push('' + numMinutes) : strings.push('0' + numMinutes); + numSeconds > 9 ? strings.push('' + numSeconds) : strings.push('0' + numSeconds); + return strings.join(':'); +} + +export function toTimeTicks(size: number, decimals: number, scaledDecimals: number) { + return toSeconds(size, decimals, scaledDecimals); +} + +export function toClockMilliseconds(size: number, decimals: number) { + return toClock(size, decimals); +} + +export function toClockSeconds(size: number, decimals: number) { + return toClock(size * 1000, decimals); +} + +export function dateTimeAsIso(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) { + const time = isUtc ? moment.utc(value) : moment(value); + + if (moment().isSame(value, 'day')) { + return time.format('HH:mm:ss'); + } + return time.format('YYYY-MM-DD HH:mm:ss'); +} + +export function dateTimeAsUS(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) { + const time = isUtc ? moment.utc(value) : moment(value); + + if (moment().isSame(value, 'day')) { + return time.format('h:mm:ss a'); + } + return time.format('MM/DD/YYYY h:mm:ss a'); +} + +export function dateTimeFromNow(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) { + const time = isUtc ? moment.utc(value) : moment(value); + return time.fromNow(); +} diff --git a/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.test.ts b/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.test.ts new file mode 100644 index 00000000000..49278711608 --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.test.ts @@ -0,0 +1,7 @@ +import { currency } from './symbolFormatters'; + +describe('Currency', () => { + it('should format as usd', () => { + expect(currency('$')(1532.82, 1, -1)).toEqual('$1.53K'); + }); +}); diff --git a/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.ts b/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.ts new file mode 100644 index 00000000000..66808143daa --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.ts @@ -0,0 +1,30 @@ +import { scaledUnits } from './valueFormats'; + +export function currency(symbol: string) { + const units = ['', 'K', 'M', 'B', 'T']; + const scaler = scaledUnits(1000, units); + return (size: number, decimals: number, scaledDecimals: number) => { + if (size === null) { + return ''; + } + const scaled = scaler(size, decimals, scaledDecimals); + return symbol + scaled; + }; +} + +export function binarySIPrefix(unit: string, offset = 0) { + const prefixes = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'].slice(offset); + const units = prefixes.map(p => { + return ' ' + p + unit; + }); + return scaledUnits(1024, units); +} + +export function decimalSIPrefix(unit: string, offset = 0) { + let prefixes = ['n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; + prefixes = prefixes.slice(3 + (offset || 0)); + const units = prefixes.map(p => { + return ' ' + p + unit; + }); + return scaledUnits(1000, units); +} diff --git a/packages/grafana-ui/src/utils/valueFormats/valueFormats.ts b/packages/grafana-ui/src/utils/valueFormats/valueFormats.ts new file mode 100644 index 00000000000..0a56ce58e5b --- /dev/null +++ b/packages/grafana-ui/src/utils/valueFormats/valueFormats.ts @@ -0,0 +1,166 @@ +import { getCategories } from './categories'; + +type ValueFormatter = (value: number, decimals?: number, scaledDecimals?: number, isUtc?: boolean) => string; + +interface ValueFormat { + name: string; + id: string; + fn: ValueFormatter; +} + +export interface ValueFormatCategory { + name: string; + formats: ValueFormat[]; +} + +interface ValueFormatterIndex { + [id: string]: ValueFormatter; +} + +// Globals & formats cache +let categories: ValueFormatCategory[] = []; +const index: ValueFormatterIndex = {}; +let hasBuiltIndex = false; + +export function toFixed(value: number, decimals?: number): string { + if (value === null) { + return ''; + } + + const factor = decimals ? Math.pow(10, Math.max(0, decimals)) : 1; + const formatted = String(Math.round(value * factor) / factor); + + // if exponent return directly + if (formatted.indexOf('e') !== -1 || value === 0) { + return formatted; + } + + // If tickDecimals was specified, ensure that we have exactly that + // much precision; otherwise default to the value's own precision. + if (decimals != null) { + const decimalPos = formatted.indexOf('.'); + const precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1; + if (precision < decimals) { + return (precision ? formatted : formatted + '.') + String(factor).substr(1, decimals - precision); + } + } + + return formatted; +} + +export function toFixedScaled( + value: number, + decimals: number, + scaledDecimals: number, + additionalDecimals: number, + ext: string +) { + if (scaledDecimals === null) { + return toFixed(value, decimals) + ext; + } else { + return toFixed(value, scaledDecimals + additionalDecimals) + ext; + } +} + +export function toFixedUnit(unit: string) { + return (size: number, decimals: number) => { + if (size === null) { + return ''; + } + return toFixed(size, decimals) + ' ' + unit; + }; +} + +// Formatter which scales the unit string geometrically according to the given +// numeric factor. Repeatedly scales the value down by the factor until it is +// less than the factor in magnitude, or the end of the array is reached. +export function scaledUnits(factor: number, extArray: string[]) { + return (size: number, decimals: number, scaledDecimals: number) => { + if (size === null) { + return ''; + } + + let steps = 0; + const limit = extArray.length; + + while (Math.abs(size) >= factor) { + steps++; + size /= factor; + + if (steps >= limit) { + return 'NA'; + } + } + + if (steps > 0 && scaledDecimals !== null) { + decimals = scaledDecimals + 3 * steps; + } + + return toFixed(size, decimals) + extArray[steps]; + }; +} + +export function locale(value: number, decimals: number) { + if (value == null) { + return ''; + } + return value.toLocaleString(undefined, { maximumFractionDigits: decimals }); +} + +export function simpleCountUnit(symbol: string) { + const units = ['', 'K', 'M', 'B', 'T']; + const scaler = scaledUnits(1000, units); + return (size: number, decimals: number, scaledDecimals: number) => { + if (size === null) { + return ''; + } + const scaled = scaler(size, decimals, scaledDecimals); + return scaled + ' ' + symbol; + }; +} + +function buildFormats() { + categories = getCategories(); + + for (const cat of categories) { + for (const format of cat.formats) { + index[format.id] = format.fn; + } + } + + hasBuiltIndex = true; +} + +export function getValueFormat(id: string): ValueFormatter { + if (!hasBuiltIndex) { + buildFormats(); + } + + return index[id]; +} + +export function getValueFormatterIndex(): ValueFormatterIndex { + if (!hasBuiltIndex) { + buildFormats(); + } + + return index; +} + +export function getValueFormats() { + if (!hasBuiltIndex) { + buildFormats(); + } + + return categories.map(cat => { + return { + text: cat.name, + submenu: cat.formats.map(format => { + return { + text: format.name, + value: format.id, + }; + }), + }; + }); +} diff --git a/public/vendor/css/spectrum.css b/packages/grafana-ui/src/vendor/spectrum.css similarity index 100% rename from public/vendor/css/spectrum.css rename to packages/grafana-ui/src/vendor/spectrum.css diff --git a/public/vendor/spectrum.js b/packages/grafana-ui/src/vendor/spectrum.js similarity index 100% rename from public/vendor/spectrum.js rename to packages/grafana-ui/src/vendor/spectrum.js diff --git a/packages/grafana-ui/src/visualizations/index.ts b/packages/grafana-ui/src/visualizations/index.ts deleted file mode 100644 index 967432d37c9..00000000000 --- a/packages/grafana-ui/src/visualizations/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Graph } from './Graph/Graph'; diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile index 4d4f6539972..d4f2f2aa7a3 100644 --- a/packaging/docker/Dockerfile +++ b/packaging/docker/Dockerfile @@ -1,4 +1,5 @@ -FROM debian:stretch-slim +ARG BASE_IMAGE=debian:stretch-slim +FROM ${BASE_IMAGE} ARG GRAFANA_TGZ="grafana-latest.linux-x64.tar.gz" @@ -10,7 +11,8 @@ COPY ${GRAFANA_TGZ} /tmp/grafana.tar.gz RUN mkdir /tmp/grafana && tar xfvz /tmp/grafana.tar.gz --strip-components=1 -C /tmp/grafana -FROM debian:stretch-slim +ARG BASE_IMAGE=debian:stretch-slim +FROM ${BASE_IMAGE} ARG GF_UID="472" ARG GF_GID="472" diff --git a/packaging/docker/build-deploy.sh b/packaging/docker/build-deploy.sh index ac3226a4a61..22655bead8c 100755 --- a/packaging/docker/build-deploy.sh +++ b/packaging/docker/build-deploy.sh @@ -8,6 +8,5 @@ docker login -u "$DOCKER_USER" -p "$DOCKER_PASS" ./push_to_docker_hub.sh "$_grafana_version" if echo "$_grafana_version" | grep -q "^master-"; then - apk add --no-cache curl ./deploy_to_k8s.sh "grafana/grafana-dev:$_grafana_version" fi diff --git a/packaging/docker/build.sh b/packaging/docker/build.sh index c303c71cd5f..a522363089b 100755 --- a/packaging/docker/build.sh +++ b/packaging/docker/build.sh @@ -1,25 +1,49 @@ #!/bin/sh -_grafana_tag=$1 +_grafana_tag=${1:-} +_docker_repo=${2:-grafana/grafana} # If the tag starts with v, treat this as a official release if echo "$_grafana_tag" | grep -q "^v"; then _grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2) - _docker_repo=${2:-grafana/grafana} else _grafana_version=$_grafana_tag - _docker_repo=${2:-grafana/grafana-dev} fi echo "Building ${_docker_repo}:${_grafana_version}" -docker build \ - --tag "${_docker_repo}:${_grafana_version}" \ - --no-cache=true . +export DOCKER_CLI_EXPERIMENTAL=enabled + +# Build grafana image for a specific arch +docker_build () { + base_image=$1 + grafana_tgz=$2 + tag=$3 + + docker build \ + --build-arg BASE_IMAGE=${base_image} \ + --build-arg GRAFANA_TGZ=${grafana_tgz} \ + --tag "${tag}" \ + --no-cache=true . +} + +# Tag docker images of all architectures +docker_tag_all () { + repo=$1 + tag=$2 + docker tag "${_docker_repo}:${_grafana_version}" "${repo}:${tag}" + docker tag "${_docker_repo}-arm32v7-linux:${_grafana_version}" "${repo}-arm32v7-linux:${tag}" + docker tag "${_docker_repo}-arm64v8-linux:${_grafana_version}" "${repo}-arm64v8-linux:${tag}" +} + +docker_build "debian:stretch-slim" "grafana-latest.linux-x64.tar.gz" "${_docker_repo}:${_grafana_version}" +docker_build "arm32v7/debian:stretch-slim" "grafana-latest.linux-armv7.tar.gz" "${_docker_repo}-arm32v7-linux:${_grafana_version}" +docker_build "arm64v8/debian:stretch-slim" "grafana-latest.linux-arm64.tar.gz" "${_docker_repo}-arm64v8-linux:${_grafana_version}" # Tag as 'latest' for official release; otherwise tag as grafana/grafana:master if echo "$_grafana_tag" | grep -q "^v"; then - docker tag "${_docker_repo}:${_grafana_version}" "${_docker_repo}:latest" + docker_tag_all "${_docker_repo}" "latest" else - docker tag "${_docker_repo}:${_grafana_version}" "grafana/grafana:master" + docker_tag_all "${_docker_repo}" "master" + docker tag "${_docker_repo}:${_grafana_version}" "grafana/grafana-dev:${_grafana_version}" fi diff --git a/packaging/docker/push_to_docker_hub.sh b/packaging/docker/push_to_docker_hub.sh index 526c216f8fa..37b5ae0095c 100755 --- a/packaging/docker/push_to_docker_hub.sh +++ b/packaging/docker/push_to_docker_hub.sh @@ -1,24 +1,46 @@ #!/bin/sh set -e -_grafana_tag=$1 +_grafana_tag=${1:-} +_docker_repo=${2:-grafana/grafana} # If the tag starts with v, treat this as a official release if echo "$_grafana_tag" | grep -q "^v"; then _grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2) - _docker_repo=${2:-grafana/grafana} else _grafana_version=$_grafana_tag - _docker_repo=${2:-grafana/grafana-dev} fi +export DOCKER_CLI_EXPERIMENTAL=enabled + echo "pushing ${_docker_repo}:${_grafana_version}" -docker push "${_docker_repo}:${_grafana_version}" + + +docker_push_all () { + repo=$1 + tag=$2 + + # Push each image individually + docker push "${repo}:${tag}" + docker push "${repo}-arm32v7-linux:${tag}" + docker push "${repo}-arm64v8-linux:${tag}" + + # Create and push a multi-arch manifest + docker manifest create "${repo}:${tag}" \ + "${repo}:${tag}" \ + "${repo}-arm32v7-linux:${tag}" \ + "${repo}-arm64v8-linux:${tag}" + + docker manifest push "${repo}:${tag}" +} if echo "$_grafana_tag" | grep -q "^v" && echo "$_grafana_tag" | grep -vq "beta"; then echo "pushing ${_docker_repo}:latest" - docker push "${_docker_repo}:latest" + docker_push_all "${_docker_repo}" "latest" + docker_push_all "${_docker_repo}" "${_grafana_version}" +elif echo "$_grafana_tag" | grep -q "^v" && echo "$_grafana_tag" | grep -q "beta"; then + docker_push_all "${_docker_repo}" "${_grafana_version}" elif echo "$_grafana_tag" | grep -q "master"; then - echo "pushing grafana/grafana:master" - docker push grafana/grafana:master + docker_push_all "${_docker_repo}" "master" + docker push "grafana/grafana-dev:${_grafana_version}" fi diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 66b3b504946..19fb4efd7e8 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -212,6 +212,10 @@ func GetAlertNotificationByID(c *m.ReqContext) Response { return Error(500, "Failed to get alert notifications", err) } + if query.Result == nil { + return Error(404, "Alert notification not found", nil) + } + return JSON(200, dtos.NewAlertNotification(query.Result)) } diff --git a/pkg/api/alerting_test.go b/pkg/api/alerting_test.go index 331beeef5e4..168193e377f 100644 --- a/pkg/api/alerting_test.go +++ b/pkg/api/alerting_test.go @@ -119,6 +119,12 @@ func TestAlertingApiEndpoint(t *testing.T) { So(getAlertsQuery.Limit, ShouldEqual, 5) So(getAlertsQuery.Query, ShouldEqual, "alertQuery") }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/alert-notifications/1", "/alert-notifications/:notificationId", m.ROLE_ADMIN, func(sc *scenarioContext) { + sc.handlerFunc = GetAlertNotificationByID + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 404) + }) }) } diff --git a/pkg/components/dashdiffs/formatter_json.go b/pkg/components/dashdiffs/formatter_json.go index 488a345d492..09dac1ae5ac 100644 --- a/pkg/components/dashdiffs/formatter_json.go +++ b/pkg/components/dashdiffs/formatter_json.go @@ -206,10 +206,9 @@ func (f *JSONFormatter) processObject(object map[string]interface{}, deltas []di // Added for _, delta := range deltas { - switch delta.(type) { + switch delta := delta.(type) { case *diff.Added: - d := delta.(*diff.Added) - f.printRecursive(d.Position.String(), d.Value, ChangeAdded) + f.printRecursive(delta.Position.String(), delta.Value, ChangeAdded) } } @@ -222,9 +221,8 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi if len(matchedDeltas) > 0 { for _, matchedDelta := range matchedDeltas { - switch matchedDelta.(type) { + switch matchedDelta := matchedDelta.(type) { case *diff.Object: - d := matchedDelta.(*diff.Object) switch value.(type) { case map[string]interface{}: //ok @@ -238,7 +236,7 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi f.print("{") f.closeLine() f.push(positionStr, len(o), false) - f.processObject(o, d.Deltas) + f.processObject(o, matchedDelta.Deltas) f.pop() f.newLine(ChangeNil) f.print("}") @@ -246,7 +244,6 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi f.closeLine() case *diff.Array: - d := matchedDelta.(*diff.Array) switch value.(type) { case []interface{}: //ok @@ -260,7 +257,7 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi f.print("[") f.closeLine() f.push(positionStr, len(a), true) - f.processArray(a, d.Deltas) + f.processArray(a, matchedDelta.Deltas) f.pop() f.newLine(ChangeNil) f.print("]") @@ -268,27 +265,23 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi f.closeLine() case *diff.Added: - d := matchedDelta.(*diff.Added) - f.printRecursive(positionStr, d.Value, ChangeAdded) + f.printRecursive(positionStr, matchedDelta.Value, ChangeAdded) f.size[len(f.size)-1]++ case *diff.Modified: - d := matchedDelta.(*diff.Modified) savedSize := f.size[len(f.size)-1] - f.printRecursive(positionStr, d.OldValue, ChangeOld) + f.printRecursive(positionStr, matchedDelta.OldValue, ChangeOld) f.size[len(f.size)-1] = savedSize - f.printRecursive(positionStr, d.NewValue, ChangeNew) + f.printRecursive(positionStr, matchedDelta.NewValue, ChangeNew) case *diff.TextDiff: savedSize := f.size[len(f.size)-1] - d := matchedDelta.(*diff.TextDiff) - f.printRecursive(positionStr, d.OldValue, ChangeOld) + f.printRecursive(positionStr, matchedDelta.OldValue, ChangeOld) f.size[len(f.size)-1] = savedSize - f.printRecursive(positionStr, d.NewValue, ChangeNew) + f.printRecursive(positionStr, matchedDelta.NewValue, ChangeNew) case *diff.Deleted: - d := matchedDelta.(*diff.Deleted) - f.printRecursive(positionStr, d.Value, ChangeDeleted) + f.printRecursive(positionStr, matchedDelta.Value, ChangeDeleted) default: return errors.New("Unknown Delta type detected") @@ -305,13 +298,13 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi func (f *JSONFormatter) searchDeltas(deltas []diff.Delta, position diff.Position) (results []diff.Delta) { results = make([]diff.Delta, 0) for _, delta := range deltas { - switch delta.(type) { + switch typedDelta := delta.(type) { case diff.PostDelta: - if delta.(diff.PostDelta).PostPosition() == position { + if typedDelta.PostPosition() == position { results = append(results, delta) } case diff.PreDelta: - if delta.(diff.PreDelta).PrePosition() == position { + if typedDelta.PrePosition() == position { results = append(results, delta) } default: @@ -417,20 +410,19 @@ func (f *JSONFormatter) print(a string) { } func (f *JSONFormatter) printRecursive(name string, value interface{}, change ChangeType) { - switch value.(type) { + switch value := value.(type) { case map[string]interface{}: f.newLine(change) f.printKey(name) f.print("{") f.closeLine() - m := value.(map[string]interface{}) - size := len(m) + size := len(value) f.push(name, size, false) - keys := sortKeys(m) + keys := sortKeys(value) for _, key := range keys { - f.printRecursive(key, m[key], change) + f.printRecursive(key, value[key], change) } f.pop() @@ -445,10 +437,9 @@ func (f *JSONFormatter) printRecursive(name string, value interface{}, change Ch f.print("[") f.closeLine() - s := value.([]interface{}) - size := len(s) + size := len(value) f.push("", size, true) - for _, item := range s { + for _, item := range value { f.printRecursive("", item, change) } f.pop() diff --git a/pkg/components/imguploader/imguploader.go b/pkg/components/imguploader/imguploader.go index 93f69cadd46..422a03d3501 100644 --- a/pkg/components/imguploader/imguploader.go +++ b/pkg/components/imguploader/imguploader.go @@ -6,7 +6,6 @@ import ( "regexp" "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/setting" ) @@ -21,6 +20,10 @@ func (NopImageUploader) Upload(ctx context.Context, path string) (string, error) return "", nil } +var ( + logger = log.New("imguploader") +) + func NewImageUploader() (ImageUploader, error) { switch setting.ImageUploadProvider { @@ -94,7 +97,7 @@ func NewImageUploader() (ImageUploader, error) { } if setting.ImageUploadProvider != "" { - log.Error2("The external image storage configuration is invalid", "unsupported provider", setting.ImageUploadProvider) + logger.Error("The external image storage configuration is invalid", "unsupported provider", setting.ImageUploadProvider) } return NopImageUploader{}, nil diff --git a/pkg/log/log.go b/pkg/log/log.go index 8f0522748ef..2e3b6303a6e 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -10,13 +10,11 @@ import ( "path/filepath" "strings" - "gopkg.in/ini.v1" - "github.com/go-stack/stack" + "github.com/grafana/grafana/pkg/util" "github.com/inconshreveable/log15" isatty "github.com/mattn/go-isatty" - - "github.com/grafana/grafana/pkg/util" + "gopkg.in/ini.v1" ) var Root log15.Logger @@ -58,10 +56,6 @@ func Debug(format string, v ...interface{}) { Root.Debug(message) } -func Debug2(message string, v ...interface{}) { - Root.Debug(message, v...) -} - func Info(format string, v ...interface{}) { var message string if len(v) > 0 { @@ -73,10 +67,6 @@ func Info(format string, v ...interface{}) { Root.Info(message) } -func Info2(message string, v ...interface{}) { - Root.Info(message, v...) -} - func Warn(format string, v ...interface{}) { var message string if len(v) > 0 { @@ -88,18 +78,10 @@ func Warn(format string, v ...interface{}) { Root.Warn(message) } -func Warn2(message string, v ...interface{}) { - Root.Warn(message, v...) -} - func Error(skip int, format string, v ...interface{}) { Root.Error(fmt.Sprintf(format, v...)) } -func Error2(message string, v ...interface{}) { - Root.Error(message, v...) -} - func Critical(skip int, format string, v ...interface{}) { Root.Crit(fmt.Sprintf(format, v...)) } diff --git a/pkg/login/ext_user.go b/pkg/login/ext_user.go index 1262c1cc44f..42fb37ff9d0 100644 --- a/pkg/login/ext_user.go +++ b/pkg/login/ext_user.go @@ -11,6 +11,10 @@ func init() { bus.AddHandler("auth", UpsertUser) } +var ( + logger = log.New("login.ext_user") +) + func UpsertUser(cmd *m.UpsertUserCommand) error { extUser := cmd.ExternalUser @@ -135,7 +139,7 @@ func updateUser(user *m.User, extUser *m.ExternalUserInfo) error { return nil } - log.Debug2("Syncing user info", "id", user.Id, "update", updateCmd) + logger.Debug("Syncing user info", "id", user.Id, "update", updateCmd) return bus.Dispatch(updateCmd) } diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 3a8010e797b..0f3f56175fe 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -112,7 +112,7 @@ func NewDashboard(title string) *Dashboard { func NewDashboardFolder(title string) *Dashboard { folder := NewDashboard(title) folder.IsFolder = true - folder.Data.Set("schemaVersion", 16) + folder.Data.Set("schemaVersion", 17) folder.Data.Set("version", 0) folder.IsFolder = true return folder diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index e33e3dc2af3..5b911c5a9ad 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -112,7 +112,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, frequency, err := getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString()) if err != nil { - return nil, ValidationError{Reason: "Could not parse frequency"} + return nil, ValidationError{Reason: err.Error()} } rawFor := jsonAlert.Get("for").MustString() diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index 4a4a989d873..ab43f3bce35 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -130,7 +130,7 @@ func (this *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.Eval defer func() { err := imageFile.Close() if err != nil { - log.Error2("Could not close Telegram inline image.", "err", err) + this.log.Error("Could not close Telegram inline image.", "err", err) } }() diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index d2a505145ac..4423046d600 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -1,16 +1,21 @@ package alerting import ( + "errors" "fmt" "regexp" "strconv" "time" "github.com/grafana/grafana/pkg/components/simplejson" - m "github.com/grafana/grafana/pkg/models" ) +var ( + ErrFrequencyCannotBeZeroOrLess = errors.New(`"evaluate every" cannot be zero or below`) + ErrFrequencyCouldNotBeParsed = errors.New(`"evaluate every" field could not be parsed`) +) + type Rule struct { Id int64 OrgId int64 @@ -76,7 +81,7 @@ func getTimeDurationStringToSeconds(str string) (int64, error) { matches := ValueFormatRegex.FindAllString(str, 1) if len(matches) <= 0 { - return 0, fmt.Errorf("Frequency could not be parsed") + return 0, ErrFrequencyCouldNotBeParsed } value, err := strconv.Atoi(matches[0]) @@ -84,6 +89,10 @@ func getTimeDurationStringToSeconds(str string) (int64, error) { return 0, err } + if value == 0 { + return 0, ErrFrequencyCannotBeZeroOrLess + } + unit := UnitFormatRegex.FindAllString(str, 1)[0] if val, ok := unitMultiplier[unit]; ok { @@ -101,7 +110,6 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { model.PanelId = ruleDef.PanelId model.Name = ruleDef.Name model.Message = ruleDef.Message - model.Frequency = ruleDef.Frequency model.State = ruleDef.State model.LastStateChange = ruleDef.NewStateDate model.For = ruleDef.For @@ -109,6 +117,13 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting")) model.StateChanges = ruleDef.StateChanges + model.Frequency = ruleDef.Frequency + // frequency cannot be zero since that would not execute the alert rule. + // so we fallback to 60 seconds if `Freqency` is missing + if model.Frequency == 0 { + model.Frequency = 60 + } + for _, v := range ruleDef.Settings.Get("notifications").MustArray() { jsonModel := simplejson.NewFromAny(v) id, err := jsonModel.Get("id").Int64() diff --git a/pkg/services/alerting/rule_test.go b/pkg/services/alerting/rule_test.go index 2a9e95e5723..cf25cc118f4 100644 --- a/pkg/services/alerting/rule_test.go +++ b/pkg/services/alerting/rule_test.go @@ -14,6 +14,36 @@ func (f *FakeCondition) Eval(context *EvalContext) (*ConditionResult, error) { return &ConditionResult{}, nil } +func TestAlertRuleFrequencyParsing(t *testing.T) { + tcs := []struct { + input string + err error + result int64 + }{ + {input: "10s", result: 10}, + {input: "10m", result: 600}, + {input: "1h", result: 3600}, + {input: "1o", result: 1}, + {input: "0s", err: ErrFrequencyCannotBeZeroOrLess}, + {input: "0m", err: ErrFrequencyCannotBeZeroOrLess}, + {input: "0h", err: ErrFrequencyCannotBeZeroOrLess}, + {input: "0", err: ErrFrequencyCannotBeZeroOrLess}, + {input: "-1s", err: ErrFrequencyCouldNotBeParsed}, + } + + for _, tc := range tcs { + r, err := getTimeDurationStringToSeconds(tc.input) + if err != tc.err { + t.Errorf("expected error: '%v' got: '%v'", tc.err, err) + return + } + + if r != tc.result { + t.Errorf("expected result: %d got %d", tc.result, r) + } + } +} + func TestAlertRuleModel(t *testing.T) { Convey("Testing alert rule", t, func() { @@ -21,26 +51,6 @@ func TestAlertRuleModel(t *testing.T) { return &FakeCondition{}, nil }) - Convey("Can parse seconds", func() { - seconds, _ := getTimeDurationStringToSeconds("10s") - So(seconds, ShouldEqual, 10) - }) - - Convey("Can parse minutes", func() { - seconds, _ := getTimeDurationStringToSeconds("10m") - So(seconds, ShouldEqual, 600) - }) - - Convey("Can parse hours", func() { - seconds, _ := getTimeDurationStringToSeconds("1h") - So(seconds, ShouldEqual, 3600) - }) - - Convey("defaults to seconds", func() { - seconds, _ := getTimeDurationStringToSeconds("1o") - So(seconds, ShouldEqual, 1) - }) - Convey("should return err for empty string", func() { _, err := getTimeDurationStringToSeconds("") So(err, ShouldNotBeNil) @@ -89,5 +99,35 @@ func TestAlertRuleModel(t *testing.T) { So(len(alertRule.Notifications), ShouldEqual, 2) }) }) + + Convey("can construct alert rule model with invalid frequency", func() { + json := ` + { + "name": "name2", + "description": "desc2", + "noDataMode": "critical", + "enabled": true, + "frequency": "0s", + "conditions": [ { "type": "test", "prop": 123 } ], + "notifications": [] + }` + + alertJSON, jsonErr := simplejson.NewJson([]byte(json)) + So(jsonErr, ShouldBeNil) + + alert := &m.Alert{ + Id: 1, + OrgId: 1, + DashboardId: 1, + PanelId: 1, + Frequency: 0, + + Settings: alertJSON, + } + + alertRule, err := NewRuleFromDBAlert(alert) + So(err, ShouldBeNil) + So(alertRule.Frequency, ShouldEqual, 60) + }) }) } diff --git a/pkg/services/alerting/test_notification.go b/pkg/services/alerting/test_notification.go index b6e59f694c8..5ffc8dc58fc 100644 --- a/pkg/services/alerting/test_notification.go +++ b/pkg/services/alerting/test_notification.go @@ -18,9 +18,12 @@ type NotificationTestCommand struct { Settings *simplejson.Json } +var ( + logger = log.New("alerting.testnotification") +) + func init() { bus.AddHandler("alerting", handleNotificationTestCommand) - } func handleNotificationTestCommand(cmd *NotificationTestCommand) error { @@ -35,7 +38,7 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error { notifiers, err := InitNotifier(model) if err != nil { - log.Error2("Failed to create notifier", "error", err.Error()) + logger.Error("Failed to create notifier", "error", err.Error()) return err } diff --git a/pkg/services/notifications/webhook.go b/pkg/services/notifications/webhook.go index dbe441c915e..2be3b145372 100644 --- a/pkg/services/notifications/webhook.go +++ b/pkg/services/notifications/webhook.go @@ -3,6 +3,7 @@ package notifications import ( "bytes" "context" + "crypto/tls" "fmt" "io" "io/ioutil" @@ -26,6 +27,9 @@ type Webhook struct { } var netTransport = &http.Transport{ + TLSClientConfig: &tls.Config{ + Renegotiation: tls.RenegotiateFreelyAsClient, + }, Proxy: http.ProxyFromEnvironment, Dial: (&net.Dialer{ Timeout: 30 * time.Second, diff --git a/pkg/services/provisioning/dashboards/config_reader.go b/pkg/services/provisioning/dashboards/config_reader.go index bfef06b558e..fa08972961d 100644 --- a/pkg/services/provisioning/dashboards/config_reader.go +++ b/pkg/services/provisioning/dashboards/config_reader.go @@ -1,6 +1,7 @@ package dashboards import ( + "fmt" "io/ioutil" "os" "path/filepath" @@ -69,7 +70,7 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) { parsedDashboards, err := cr.parseConfigs(file) if err != nil { - return nil, err + return nil, fmt.Errorf("could not parse provisioning config file: %s error: %v", file.Name(), err) } if len(parsedDashboards) > 0 { diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index f9590b01bf1..1c1819df8a9 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -118,6 +118,7 @@ func (fr *fileReader) startWalkingDisk() error { return nil } + func (fr *fileReader) deleteDashboardIfFileIsMissing(provisionedDashboardRefs map[string]*models.DashboardProvisioning, filesFoundOnDisk map[string]os.FileInfo) { if fr.Cfg.DisableDeletion { return @@ -180,7 +181,7 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil dash.Dashboard.SetId(provisionedData.DashboardId) } - fr.log.Debug("saving new dashboard", "file", path) + fr.log.Debug("saving new dashboard", "provisoner", fr.Cfg.Name, "file", path, "folderId", dash.Dashboard.FolderId) dp := &models.DashboardProvisioning{ ExternalId: path, Name: fr.Cfg.Name, diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index fe849816553..1a9d2216e7a 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -166,6 +166,36 @@ func TestDashboardFileReader(t *testing.T) { _, err := NewDashboardFileReader(cfg, logger) So(err, ShouldBeNil) }) + + Convey("Two dashboard providers should be able to provisioned the same dashboard without uid", func() { + cfg1 := &DashboardsAsConfig{Name: "1", Type: "file", OrgId: 1, Folder: "f1", Options: map[string]interface{}{"path": containingId}} + cfg2 := &DashboardsAsConfig{Name: "2", Type: "file", OrgId: 1, Folder: "f2", Options: map[string]interface{}{"path": containingId}} + + reader1, err := NewDashboardFileReader(cfg1, logger) + So(err, ShouldBeNil) + + err = reader1.startWalkingDisk() + So(err, ShouldBeNil) + + reader2, err := NewDashboardFileReader(cfg2, logger) + So(err, ShouldBeNil) + + err = reader2.startWalkingDisk() + So(err, ShouldBeNil) + + var folderCount int + var dashCount int + for _, o := range fakeService.inserted { + if o.Dashboard.IsFolder { + folderCount++ + } else { + dashCount++ + } + } + + So(folderCount, ShouldEqual, 2) + So(dashCount, ShouldEqual, 2) + }) }) Convey("Should not create new folder if folder name is missing", func() { @@ -256,7 +286,9 @@ func (ffi FakeFileInfo) Sys() interface{} { } func mockDashboardProvisioningService() *fakeDashboardProvisioningService { - mock := fakeDashboardProvisioningService{} + mock := fakeDashboardProvisioningService{ + provisioned: map[string][]*models.DashboardProvisioning{}, + } dashboards.NewProvisioningService = func() dashboards.DashboardProvisioningService { return &mock } @@ -265,17 +297,26 @@ func mockDashboardProvisioningService() *fakeDashboardProvisioningService { type fakeDashboardProvisioningService struct { inserted []*dashboards.SaveDashboardDTO - provisioned []*models.DashboardProvisioning + provisioned map[string][]*models.DashboardProvisioning getDashboard []*models.Dashboard } func (s *fakeDashboardProvisioningService) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) { - return s.provisioned, nil + if _, ok := s.provisioned[name]; !ok { + s.provisioned[name] = []*models.DashboardProvisioning{} + } + + return s.provisioned[name], nil } func (s *fakeDashboardProvisioningService) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) { s.inserted = append(s.inserted, dto) - s.provisioned = append(s.provisioned, provisioning) + + if _, ok := s.provisioned[provisioning.Name]; !ok { + s.provisioned[provisioning.Name] = []*models.DashboardProvisioning{} + } + + s.provisioned[provisioning.Name] = append(s.provisioned[provisioning.Name], provisioning) return dto.Dashboard, nil } diff --git a/pkg/services/sqlstore/dashboard_provisioning.go b/pkg/services/sqlstore/dashboard_provisioning.go index 33fbb01c5b7..9b180662278 100644 --- a/pkg/services/sqlstore/dashboard_provisioning.go +++ b/pkg/services/sqlstore/dashboard_provisioning.go @@ -51,7 +51,7 @@ func SaveProvisionedDashboard(cmd *models.SaveProvisionedDashboardCommand) error func saveProvionedData(sess *DBSession, cmd *models.DashboardProvisioning, dashboard *models.Dashboard) error { result := &models.DashboardProvisioning{} - exist, err := sess.Where("dashboard_id=?", dashboard.Id).Get(result) + exist, err := sess.Where("dashboard_id=? AND name = ?", dashboard.Id, cmd.Name).Get(result) if err != nil { return err } diff --git a/pkg/services/sqlstore/login_attempt.go b/pkg/services/sqlstore/login_attempt.go index 78da198e8e7..ceff2394dce 100644 --- a/pkg/services/sqlstore/login_attempt.go +++ b/pkg/services/sqlstore/login_attempt.go @@ -78,14 +78,14 @@ func GetUserLoginAttemptCount(query *m.GetUserLoginAttemptCountQuery) error { } func toInt64(i interface{}) int64 { - switch i.(type) { + switch i := i.(type) { case []byte: - n, _ := strconv.ParseInt(string(i.([]byte)), 10, 64) + n, _ := strconv.ParseInt(string(i), 10, 64) return n case int: - return int64(i.(int)) + return int64(i) case int64: - return i.(int64) + return i } return 0 } diff --git a/pkg/tsdb/cloudwatch/credentials.go b/pkg/tsdb/cloudwatch/credentials.go index 165f8fdbe97..fb92c827f7c 100644 --- a/pkg/tsdb/cloudwatch/credentials.go +++ b/pkg/tsdb/cloudwatch/credentials.go @@ -3,7 +3,6 @@ package cloudwatch import ( "fmt" "os" - "strings" "sync" "time" @@ -43,7 +42,7 @@ func GetCredentials(dsInfo *DatasourceInfo) (*credentials.Credentials, error) { secretAccessKey := "" sessionToken := "" var expiration *time.Time = nil - if dsInfo.AuthType == "arn" && strings.Index(dsInfo.AssumeRoleArn, "arn:aws:iam:") == 0 { + if dsInfo.AuthType == "arn" { params := &sts.AssumeRoleInput{ RoleArn: aws.String(dsInfo.AssumeRoleArn), RoleSessionName: aws.String("GrafanaSession"), diff --git a/pkg/tsdb/elasticsearch/client/models.go b/pkg/tsdb/elasticsearch/client/models.go index 5307dc34b27..fcc96aeba93 100644 --- a/pkg/tsdb/elasticsearch/client/models.go +++ b/pkg/tsdb/elasticsearch/client/models.go @@ -292,7 +292,7 @@ func (a *MetricAggregation) MarshalJSON() ([]byte, error) { // PipelineAggregation represents a metric aggregation type PipelineAggregation struct { - BucketPath string + BucketPath interface{} Settings map[string]interface{} } diff --git a/pkg/tsdb/elasticsearch/client/search_request.go b/pkg/tsdb/elasticsearch/client/search_request.go index d89a98cbadb..b8c9232f97b 100644 --- a/pkg/tsdb/elasticsearch/client/search_request.go +++ b/pkg/tsdb/elasticsearch/client/search_request.go @@ -268,7 +268,7 @@ type AggBuilder interface { Filters(key string, fn func(a *FiltersAggregation, b AggBuilder)) AggBuilder GeoHashGrid(key, field string, fn func(a *GeoHashGridAggregation, b AggBuilder)) AggBuilder Metric(key, metricType, field string, fn func(a *MetricAggregation)) AggBuilder - Pipeline(key, pipelineType, bucketPath string, fn func(a *PipelineAggregation)) AggBuilder + Pipeline(key, pipelineType string, bucketPath interface{}, fn func(a *PipelineAggregation)) AggBuilder Build() (AggArray, error) } @@ -438,7 +438,7 @@ func (b *aggBuilderImpl) Metric(key, metricType, field string, fn func(a *Metric return b } -func (b *aggBuilderImpl) Pipeline(key, pipelineType, bucketPath string, fn func(a *PipelineAggregation)) AggBuilder { +func (b *aggBuilderImpl) Pipeline(key, pipelineType string, bucketPath interface{}, fn func(a *PipelineAggregation)) AggBuilder { innerAgg := &PipelineAggregation{ BucketPath: bucketPath, Settings: make(map[string]interface{}), diff --git a/pkg/tsdb/elasticsearch/models.go b/pkg/tsdb/elasticsearch/models.go index 46af5e4b745..d38e21c3ebc 100644 --- a/pkg/tsdb/elasticsearch/models.go +++ b/pkg/tsdb/elasticsearch/models.go @@ -25,13 +25,14 @@ type BucketAgg struct { // MetricAgg represents a metric aggregation of the time series query model of the datasource type MetricAgg struct { - Field string `json:"field"` - Hide bool `json:"hide"` - ID string `json:"id"` - PipelineAggregate string `json:"pipelineAgg"` - Settings *simplejson.Json `json:"settings"` - Meta *simplejson.Json `json:"meta"` - Type string `json:"type"` + Field string `json:"field"` + Hide bool `json:"hide"` + ID string `json:"id"` + PipelineAggregate string `json:"pipelineAgg"` + PipelineVariables map[string]string `json:"pipelineVariables"` + Settings *simplejson.Json `json:"settings"` + Meta *simplejson.Json `json:"meta"` + Type string `json:"type"` } var metricAggType = map[string]string{ @@ -45,6 +46,7 @@ var metricAggType = map[string]string{ "cardinality": "Unique Count", "moving_avg": "Moving Average", "derivative": "Derivative", + "bucket_script": "Bucket Script", "raw_document": "Raw Document", } @@ -60,8 +62,13 @@ var extendedStats = map[string]string{ } var pipelineAggType = map[string]string{ - "moving_avg": "moving_avg", - "derivative": "derivative", + "moving_avg": "moving_avg", + "derivative": "derivative", + "bucket_script": "bucket_script", +} + +var pipelineAggWithMultipleBucketPathsType = map[string]string{ + "bucket_script": "bucket_script", } func isPipelineAgg(metricType string) bool { @@ -71,6 +78,13 @@ func isPipelineAgg(metricType string) bool { return false } +func isPipelineAggWithMultipleBucketPaths(metricType string) bool { + if _, ok := pipelineAggWithMultipleBucketPathsType[metricType]; ok { + return true + } + return false +} + func describeMetric(metricType, field string) string { text := metricAggType[metricType] if metricType == countType { diff --git a/pkg/tsdb/elasticsearch/response_parser.go b/pkg/tsdb/elasticsearch/response_parser.go index b2c724a9b93..6bbaa3df34b 100644 --- a/pkg/tsdb/elasticsearch/response_parser.go +++ b/pkg/tsdb/elasticsearch/response_parser.go @@ -260,6 +260,7 @@ func (rp *responseParser) processMetrics(esAgg *simplejson.Json, target *Query, newSeries.Tags["metric"] = metric.Type newSeries.Tags["field"] = metric.Field + newSeries.Tags["metricId"] = metric.ID for _, v := range esAgg.Get("buckets").MustArray() { bucket := simplejson.NewFromAny(v) key := castToNullFloat(bucket.Get("key")) @@ -459,20 +460,42 @@ func (rp *responseParser) getSeriesName(series *tsdb.TimeSeries, target *Query, } // todo, if field and pipelineAgg if field != "" && isPipelineAgg(metricType) { - found := false - for _, metric := range target.Metrics { - if metric.ID == field { - metricName += " " + describeMetric(metric.Type, field) - found = true + if isPipelineAggWithMultipleBucketPaths(metricType) { + metricID := "" + if v, ok := series.Tags["metricId"]; ok { + metricID = v + } + + for _, metric := range target.Metrics { + if metric.ID == metricID { + metricName = metric.Settings.Get("script").MustString() + for name, pipelineAgg := range metric.PipelineVariables { + for _, m := range target.Metrics { + if m.ID == pipelineAgg { + metricName = strings.Replace(metricName, "params."+name, describeMetric(m.Type, m.Field), -1) + } + } + } + } + } + } else { + found := false + for _, metric := range target.Metrics { + if metric.ID == field { + metricName += " " + describeMetric(metric.Type, field) + found = true + } + } + if !found { + metricName = "Unset" } - } - if !found { - metricName = "Unset" } } else if field != "" { metricName += " " + field } + delete(series.Tags, "metricId") + if len(series.Tags) == 0 { return metricName } diff --git a/pkg/tsdb/elasticsearch/response_parser_test.go b/pkg/tsdb/elasticsearch/response_parser_test.go index b00c14cf946..e9cd8ad0980 100644 --- a/pkg/tsdb/elasticsearch/response_parser_test.go +++ b/pkg/tsdb/elasticsearch/response_parser_test.go @@ -787,6 +787,84 @@ func TestResponseParser(t *testing.T) { So(rows[0][2].(null.Float).Float64, ShouldEqual, 3000) }) + Convey("With bucket_script", func() { + targets := map[string]string{ + "A": `{ + "timeField": "@timestamp", + "metrics": [ + { "id": "1", "type": "sum", "field": "@value" }, + { "id": "3", "type": "max", "field": "@value" }, + { + "id": "4", + "field": "select field", + "pipelineVariables": [{ "name": "var1", "pipelineAgg": "1" }, { "name": "var2", "pipelineAgg": "3" }], + "settings": { "script": "params.var1 * params.var2" }, + "type": "bucket_script" + } + ], + "bucketAggs": [{ "type": "date_histogram", "field": "@timestamp", "id": "2" }] + }`, + } + response := `{ + "responses": [ + { + "aggregations": { + "2": { + "buckets": [ + { + "1": { "value": 2 }, + "3": { "value": 3 }, + "4": { "value": 6 }, + "doc_count": 60, + "key": 1000 + }, + { + "1": { "value": 3 }, + "3": { "value": 4 }, + "4": { "value": 12 }, + "doc_count": 60, + "key": 2000 + } + ] + } + } + } + ] + }` + rp, err := newResponseParserForTest(targets, response) + So(err, ShouldBeNil) + result, err := rp.getTimeSeries() + So(err, ShouldBeNil) + So(result.Results, ShouldHaveLength, 1) + + queryRes := result.Results["A"] + So(queryRes, ShouldNotBeNil) + So(queryRes.Series, ShouldHaveLength, 3) + seriesOne := queryRes.Series[0] + So(seriesOne.Name, ShouldEqual, "Sum @value") + So(seriesOne.Points, ShouldHaveLength, 2) + So(seriesOne.Points[0][0].Float64, ShouldEqual, 2) + So(seriesOne.Points[0][1].Float64, ShouldEqual, 1000) + So(seriesOne.Points[1][0].Float64, ShouldEqual, 3) + So(seriesOne.Points[1][1].Float64, ShouldEqual, 2000) + + seriesTwo := queryRes.Series[1] + So(seriesTwo.Name, ShouldEqual, "Max @value") + So(seriesTwo.Points, ShouldHaveLength, 2) + So(seriesTwo.Points[0][0].Float64, ShouldEqual, 3) + So(seriesTwo.Points[0][1].Float64, ShouldEqual, 1000) + So(seriesTwo.Points[1][0].Float64, ShouldEqual, 4) + So(seriesTwo.Points[1][1].Float64, ShouldEqual, 2000) + + seriesThree := queryRes.Series[2] + So(seriesThree.Name, ShouldEqual, "Sum @value * Max @value") + So(seriesThree.Points, ShouldHaveLength, 2) + So(seriesThree.Points[0][0].Float64, ShouldEqual, 6) + So(seriesThree.Points[0][1].Float64, ShouldEqual, 1000) + So(seriesThree.Points[1][0].Float64, ShouldEqual, 12) + So(seriesThree.Points[1][1].Float64, ShouldEqual, 2000) + }) + // Convey("Raw documents query", func() { // targets := map[string]string{ // "A": `{ diff --git a/pkg/tsdb/elasticsearch/time_series_query.go b/pkg/tsdb/elasticsearch/time_series_query.go index 28a930df3a2..e1cd466275e 100644 --- a/pkg/tsdb/elasticsearch/time_series_query.go +++ b/pkg/tsdb/elasticsearch/time_series_query.go @@ -94,26 +94,56 @@ func (e *timeSeriesQuery) execute() (*tsdb.Response, error) { } if isPipelineAgg(m.Type) { - if _, err := strconv.Atoi(m.PipelineAggregate); err == nil { - var appliedAgg *MetricAgg - for _, pipelineMetric := range q.Metrics { - if pipelineMetric.ID == m.PipelineAggregate { - appliedAgg = pipelineMetric - break - } - } - if appliedAgg != nil { - bucketPath := m.PipelineAggregate - if appliedAgg.Type == countType { - bucketPath = "_count" + if isPipelineAggWithMultipleBucketPaths(m.Type) { + if len(m.PipelineVariables) > 0 { + bucketPaths := map[string]interface{}{} + for name, pipelineAgg := range m.PipelineVariables { + if _, err := strconv.Atoi(pipelineAgg); err == nil { + var appliedAgg *MetricAgg + for _, pipelineMetric := range q.Metrics { + if pipelineMetric.ID == pipelineAgg { + appliedAgg = pipelineMetric + break + } + } + if appliedAgg != nil { + if appliedAgg.Type == countType { + bucketPaths[name] = "_count" + } else { + bucketPaths[name] = pipelineAgg + } + } + } } - aggBuilder.Pipeline(m.ID, m.Type, bucketPath, func(a *es.PipelineAggregation) { + aggBuilder.Pipeline(m.ID, m.Type, bucketPaths, func(a *es.PipelineAggregation) { a.Settings = m.Settings.MustMap() }) + } else { + continue } } else { - continue + if _, err := strconv.Atoi(m.PipelineAggregate); err == nil { + var appliedAgg *MetricAgg + for _, pipelineMetric := range q.Metrics { + if pipelineMetric.ID == m.PipelineAggregate { + appliedAgg = pipelineMetric + break + } + } + if appliedAgg != nil { + bucketPath := m.PipelineAggregate + if appliedAgg.Type == countType { + bucketPath = "_count" + } + + aggBuilder.Pipeline(m.ID, m.Type, bucketPath, func(a *es.PipelineAggregation) { + a.Settings = m.Settings.MustMap() + }) + } + } else { + continue + } } } else { aggBuilder.Metric(m.ID, m.Type, m.Field, func(a *es.MetricAggregation) { @@ -328,12 +358,20 @@ func (p *timeSeriesQueryParser) parseMetrics(model *simplejson.Json) ([]*MetricA metric.PipelineAggregate = metricJSON.Get("pipelineAgg").MustString() metric.Settings = simplejson.NewFromAny(metricJSON.Get("settings").MustMap()) metric.Meta = simplejson.NewFromAny(metricJSON.Get("meta").MustMap()) - metric.Type, err = metricJSON.Get("type").String() if err != nil { return nil, err } + if isPipelineAggWithMultipleBucketPaths(metric.Type) { + metric.PipelineVariables = map[string]string{} + pvArr := metricJSON.Get("pipelineVariables").MustArray() + for _, v := range pvArr { + kv := v.(map[string]interface{}) + metric.PipelineVariables[kv["name"].(string)] = kv["pipelineAgg"].(string) + } + } + result = append(result, metric) } return result, nil diff --git a/pkg/tsdb/elasticsearch/time_series_query_test.go b/pkg/tsdb/elasticsearch/time_series_query_test.go index a4f305242fa..3a558c32782 100644 --- a/pkg/tsdb/elasticsearch/time_series_query_test.go +++ b/pkg/tsdb/elasticsearch/time_series_query_test.go @@ -543,6 +543,77 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { plAgg := derivativeAgg.Aggregation.Aggregation.(*es.PipelineAggregation) So(plAgg.BucketPath, ShouldEqual, "_count") }) + + Convey("With bucket_script", func() { + c := newFakeClient(5) + _, err := executeTsdbQuery(c, `{ + "timeField": "@timestamp", + "bucketAggs": [ + { "type": "date_histogram", "field": "@timestamp", "id": "4" } + ], + "metrics": [ + { "id": "3", "type": "sum", "field": "@value" }, + { "id": "5", "type": "max", "field": "@value" }, + { + "id": "2", + "type": "bucket_script", + "pipelineVariables": [ + { "name": "var1", "pipelineAgg": "3" }, + { "name": "var2", "pipelineAgg": "5" } + ], + "settings": { "script": "params.var1 * params.var2" } + } + ] + }`, from, to, 15*time.Second) + So(err, ShouldBeNil) + sr := c.multisearchRequests[0].Requests[0] + + firstLevel := sr.Aggs[0] + So(firstLevel.Key, ShouldEqual, "4") + So(firstLevel.Aggregation.Type, ShouldEqual, "date_histogram") + + bucketScriptAgg := firstLevel.Aggregation.Aggs[2] + So(bucketScriptAgg.Key, ShouldEqual, "2") + plAgg := bucketScriptAgg.Aggregation.Aggregation.(*es.PipelineAggregation) + So(plAgg.BucketPath.(map[string]interface{}), ShouldResemble, map[string]interface{}{ + "var1": "3", + "var2": "5", + }) + }) + + Convey("With bucket_script doc count", func() { + c := newFakeClient(5) + _, err := executeTsdbQuery(c, `{ + "timeField": "@timestamp", + "bucketAggs": [ + { "type": "date_histogram", "field": "@timestamp", "id": "4" } + ], + "metrics": [ + { "id": "3", "type": "count", "field": "select field" }, + { + "id": "2", + "type": "bucket_script", + "pipelineVariables": [ + { "name": "var1", "pipelineAgg": "3" } + ], + "settings": { "script": "params.var1 * 1000" } + } + ] + }`, from, to, 15*time.Second) + So(err, ShouldBeNil) + sr := c.multisearchRequests[0].Requests[0] + + firstLevel := sr.Aggs[0] + So(firstLevel.Key, ShouldEqual, "4") + So(firstLevel.Aggregation.Type, ShouldEqual, "date_histogram") + + bucketScriptAgg := firstLevel.Aggregation.Aggs[0] + So(bucketScriptAgg.Key, ShouldEqual, "2") + plAgg := bucketScriptAgg.Aggregation.Aggregation.(*es.PipelineAggregation) + So(plAgg.BucketPath.(map[string]interface{}), ShouldResemble, map[string]interface{}{ + "var1": "_count", + }) + }) }) } diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 5609c058a27..65eb68966ff 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -1,11 +1,15 @@ import { react2AngularDirective } from 'app/core/utils/react2angular'; +import { QueryEditor as StackdriverQueryEditor } from 'app/plugins/datasource/stackdriver/components/QueryEditor'; +import { AnnotationQueryEditor as StackdriverAnnotationQueryEditor } from 'app/plugins/datasource/stackdriver/components/AnnotationQueryEditor'; import { PasswordStrength } from './components/PasswordStrength'; import PageHeader from './components/PageHeader/PageHeader'; import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA'; import { SearchResult } from './components/search/SearchResult'; import { TagFilter } from './components/TagFilter/TagFilter'; import { SideMenu } from './components/sidemenu/SideMenu'; +import { MetricSelect } from './components/Select/MetricSelect'; import AppNotificationList from './components/AppNotifications/AppNotificationList'; +import { ColorPicker, SeriesColorPickerPopover } from '@grafana/ui'; export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); @@ -19,4 +23,37 @@ export function registerAngularDirectives() { ['onChange', { watchDepth: 'reference' }], ['tagOptions', { watchDepth: 'reference' }], ]); + react2AngularDirective('colorPicker', ColorPicker, [ + 'color', + ['onChange', { watchDepth: 'reference', wrapApply: true }], + ]); + react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopover, [ + 'series', + 'onColorChange', + 'onToggleAxis', + ]); + react2AngularDirective('metricSelect', MetricSelect, [ + 'options', + 'onChange', + 'value', + 'isSearchable', + 'className', + 'placeholder', + ['variables', { watchDepth: 'reference' }], + ]); + react2AngularDirective('stackdriverQueryEditor', StackdriverQueryEditor, [ + 'target', + 'onQueryChange', + 'onExecuteQuery', + ['events', { watchDepth: 'reference' }], + ['datasource', { watchDepth: 'reference' }], + ['templateSrv', { watchDepth: 'reference' }], + ]); + react2AngularDirective('stackdriverAnnotationQueryEditor', StackdriverAnnotationQueryEditor, [ + 'target', + 'onQueryChange', + 'onExecuteQuery', + ['datasource', { watchDepth: 'reference' }], + ['templateSrv', { watchDepth: 'reference' }], + ]); } diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx index ae0e39cc26d..d63af72ae4d 100644 --- a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx +++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx @@ -24,12 +24,14 @@ class EmptyListCTA extends Component { {buttonTitle} -
- ProTip: {proTip} - - {proTipLinkTitle} - -
+ {proTip && ( +
+ ProTip: {proTip} + + {proTipLinkTitle} + +
+ )}
); } diff --git a/public/app/core/components/ErrorBoundary/ErrorBoundary.tsx b/public/app/core/components/ErrorBoundary/ErrorBoundary.tsx new file mode 100644 index 00000000000..188750b0fef --- /dev/null +++ b/public/app/core/components/ErrorBoundary/ErrorBoundary.tsx @@ -0,0 +1,44 @@ +import { Component } from 'react'; + +interface ErrorInfo { + componentStack: string; +} + +interface RenderProps { + error: Error; + errorInfo: ErrorInfo; +} + +interface Props { + children: (r: RenderProps) => JSX.Element; +} + +interface State { + error: Error; + errorInfo: ErrorInfo; +} + +class ErrorBoundary extends Component { + readonly state: State = { + error: null, + errorInfo: null, + }; + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + this.setState({ + error: error, + errorInfo: errorInfo + }); + } + + render() { + const { children } = this.props; + const { error, errorInfo } = this.state; + return children({ + error, + errorInfo, + }); + } +} + +export default ErrorBoundary; diff --git a/public/app/core/components/PermissionList/AddPermission.tsx b/public/app/core/components/PermissionList/AddPermission.tsx index 749bef680bf..30219371257 100644 --- a/public/app/core/components/PermissionList/AddPermission.tsx +++ b/public/app/core/components/PermissionList/AddPermission.tsx @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import { UserPicker } from 'app/core/components/Select/UserPicker'; import { TeamPicker, Team } from 'app/core/components/Select/TeamPicker'; -import { Select, SelectOptionItem } from 'app/core/components/Select/Select'; +import { Select, SelectOptionItem } from '@grafana/ui'; import { User } from 'app/types'; import { dashboardPermissionLevels, diff --git a/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx b/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx index d3f9ddbb1fb..ebf3cbad1bc 100644 --- a/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx +++ b/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import Select from 'app/core/components/Select/Select'; +import { Select } from '@grafana/ui'; import { dashboardPermissionLevels } from 'app/types/acl'; export interface Props { diff --git a/public/app/core/components/PermissionList/PermissionListItem.tsx b/public/app/core/components/PermissionList/PermissionListItem.tsx index e726667cfbb..c33b564154a 100644 --- a/public/app/core/components/PermissionList/PermissionListItem.tsx +++ b/public/app/core/components/PermissionList/PermissionListItem.tsx @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import { Select } from 'app/core/components/Select/Select'; +import { Select } from '@grafana/ui'; import { dashboardPermissionLevels, DashboardAcl, PermissionLevel } from 'app/types/acl'; import { FolderInfo } from 'app/types'; diff --git a/public/app/core/components/ScrollBar/ScrollBar.tsx b/public/app/core/components/ScrollBar/ScrollBar.tsx deleted file mode 100644 index 24d17f67367..00000000000 --- a/public/app/core/components/ScrollBar/ScrollBar.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import baron from 'baron'; - -export interface Props { - children: any; - className: string; -} - -export default class ScrollBar extends React.Component { - private container: any; - private scrollbar: baron; - - constructor(props) { - super(props); - } - - componentDidMount() { - this.scrollbar = baron({ - root: this.container.parentElement, - scroller: this.container, - bar: '.baron__bar', - barOnCls: '_scrollbar', - scrollingCls: '_scrolling', - track: '.baron__track', - }); - } - - componentDidUpdate() { - this.scrollbar.update(); - } - - componentWillUnmount() { - this.scrollbar.dispose(); - } - - // methods can be invoked by outside - setScrollTop(top) { - if (this.container) { - this.container.scrollTop = top; - this.scrollbar.update(); - - return true; - } - return false; - } - - setScrollLeft(left) { - if (this.container) { - this.container.scrollLeft = left; - this.scrollbar.update(); - - return true; - } - return false; - } - - update() { - this.scrollbar.update(); - } - - handleRef = ref => { - this.container = ref; - }; - - render() { - return ( -
-
- {this.props.children} -
- -
-
-
-
- ); - } -} diff --git a/public/app/core/components/Select/DataSourcePicker.tsx b/public/app/core/components/Select/DataSourcePicker.tsx index 1a9081038c0..372c4cd4013 100644 --- a/public/app/core/components/Select/DataSourcePicker.tsx +++ b/public/app/core/components/Select/DataSourcePicker.tsx @@ -3,7 +3,7 @@ import React, { PureComponent } from 'react'; import _ from 'lodash'; // Components -import Select from './Select'; +import { Select } from '@grafana/ui'; // Types import { DataSourceSelectItem } from 'app/types'; diff --git a/public/app/core/components/Select/MetricSelect.tsx b/public/app/core/components/Select/MetricSelect.tsx new file mode 100644 index 00000000000..c9247198052 --- /dev/null +++ b/public/app/core/components/Select/MetricSelect.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import _ from 'lodash'; + +import { Select } from '@grafana/ui'; +import { SelectOptionItem } from '@grafana/ui'; +import { Variable } from 'app/types/templates'; + +export interface Props { + onChange: (value: string) => void; + options: SelectOptionItem[]; + isSearchable: boolean; + value: string; + placeholder?: string; + className?: string; + variables?: Variable[]; +} + +interface State { + options: any[]; +} + +export class MetricSelect extends React.Component { + static defaultProps = { + variables: [], + options: [], + isSearchable: true, + }; + + constructor(props) { + super(props); + this.state = { options: [] }; + } + + componentDidMount() { + this.setState({ options: this.buildOptions(this.props) }); + } + + componentWillReceiveProps(nextProps: Props) { + if (nextProps.options.length > 0 || nextProps.variables.length) { + this.setState({ options: this.buildOptions(nextProps) }); + } + } + + shouldComponentUpdate(nextProps: Props) { + const nextOptions = this.buildOptions(nextProps); + return nextProps.value !== this.props.value || !_.isEqual(nextOptions, this.state.options); + } + + buildOptions({ variables = [], options }) { + return variables.length > 0 ? [this.getVariablesGroup(), ...options] : options; + } + + getVariablesGroup() { + return { + label: 'Template Variables', + options: this.props.variables.map(v => ({ + label: `$${v.name}`, + value: `$${v.name}`, + })), + }; + } + + getSelectedOption() { + const { options } = this.state; + const allOptions = options.every(o => o.options) ? _.flatten(options.map(o => o.options)) : options; + return allOptions.find(option => option.value === this.props.value); + } + + render() { + const { placeholder, className, isSearchable, onChange } = this.props; + const { options } = this.state; + const selectedOption = this.getSelectedOption(); + + return ( + +
+
+ Evaluate every + +
+
+ + + + If an alert rule has a configured For and the query violates the configured + threshold it + will first go from OK to Pending. + Going from OK to Pending Grafana will not send any notifications. Once the alert + rule + has + been firing for more than For duration, it will change to Alerting and send alert + notifications. + +
+
+
-
-
-
- {{ctrl.error}} -
+
+

Conditions

+
+
+ + WHEN +
+
+ + + OF +
+
+ + +
+
+ + + + +
+
+ +
+
-
-
Alert Config
-
- Name - -
-
-
- Evaluate every - -
-
- - - - If an alert rule has a configured For and the query violates the configured threshold it will first go from OK to Pending. - Going from OK to Pending Grafana will not send any notifications. Once the alert rule has been firing for more than For duration, it will change to Alerting and send alert notifications. - -
-
-
+
+ +
+
-
-
Conditions
-
-
- - WHEN -
-
- - - OF -
-
- - -
-
- - - - -
-
- -
-
+
+

No Data & Error Handling

+
+
+ If no data or all values are null +
+
+ SET STATE TO +
+ +
+
+
-
- -
-
+
+
+ If execution error or timeout +
+
+ SET STATE TO +
+ +
+
+
+
+
+
-
-
- If no data or all values are null - SET STATE TO -
- -
-
- -
- If execution error or timeout - SET STATE TO -
- -
-
- -
- -
-
- -
- Evaluating rule -
- -
- -
-
- -
-
Notifications
-
-
- Send to - -  {{nc.name}}  - - - -
-
-
- Message - -
-
- -
- -
- State history (last 50 state changes) -
- -
-
- No state changes recorded -
- -
    -
  1. -
    - -
    -
    -
    -
    - {{al.stateModel.text}} -
    -
    - {{al.info}} -
    -
    - {{al.time}} -
    -
  2. -
-
-
-
-
- -
-
-
Panel has no alert rule defined
- -
-
+
+
Notifications
+
+
+
+ Send to +
+
+ +  {{nc.name}}  + + +
+
+ +
+
+
+ Message + +
+
+
diff --git a/public/app/features/all.ts b/public/app/features/all.ts index b1283fdfd3e..1ba6a85899c 100644 --- a/public/app/features/all.ts +++ b/public/app/features/all.ts @@ -11,3 +11,4 @@ import './alerting/NotificationsListCtrl'; import './manage-dashboards'; import './teams/CreateTeamCtrl'; import './profile/all'; +import './datasources/settings/HttpSettingsCtrl'; diff --git a/public/app/features/annotations/event_manager.ts b/public/app/features/annotations/event_manager.ts index db748e639a1..6966d3cdc82 100644 --- a/public/app/features/annotations/event_manager.ts +++ b/public/app/features/annotations/event_manager.ts @@ -1,8 +1,6 @@ import _ from 'lodash'; import moment from 'moment'; import tinycolor from 'tinycolor2'; -import { MetricsPanelCtrl } from 'app/plugins/sdk'; -import { AnnotationEvent } from './event'; import { OK_COLOR, ALERTING_COLOR, @@ -10,7 +8,10 @@ import { PENDING_COLOR, DEFAULT_ANNOTATION_COLOR, REGION_FILL_ALPHA, -} from 'app/core/utils/colors'; +} from '@grafana/ui'; + +import { MetricsPanelCtrl } from 'app/plugins/sdk'; +import { AnnotationEvent } from './event'; export class EventManager { event: AnnotationEvent; diff --git a/public/app/features/dashboard/dashboard_migration.ts b/public/app/features/dashboard/dashboard_migration.ts index abd12ab4b13..2dbeb6c6e80 100644 --- a/public/app/features/dashboard/dashboard_migration.ts +++ b/public/app/features/dashboard/dashboard_migration.ts @@ -9,6 +9,7 @@ import { } from 'app/core/constants'; import { PanelModel } from './panel_model'; import { DashboardModel } from './dashboard_model'; +import getFactors from 'app/core/utils/factors'; export class DashboardMigrator { dashboard: DashboardModel; @@ -21,7 +22,7 @@ export class DashboardMigrator { let i, j, k, n; const oldVersion = this.dashboard.schemaVersion; const panelUpgrades = []; - this.dashboard.schemaVersion = 16; + this.dashboard.schemaVersion = 17; if (oldVersion === this.dashboard.schemaVersion) { return; @@ -368,6 +369,24 @@ export class DashboardMigrator { this.upgradeToGridLayout(old); } + if (oldVersion < 17) { + panelUpgrades.push(panel => { + if (panel.minSpan) { + const max = GRID_COLUMN_COUNT / panel.minSpan; + const factors = getFactors(GRID_COLUMN_COUNT); + // find the best match compared to factors + // (ie. [1,2,3,4,6,12,24] for 24 columns) + panel.maxPerRow = + factors[ + _.findIndex(factors, o => { + return o > max; + }) - 1 + ]; + } + delete panel.minSpan; + }); + } + if (panelUpgrades.length === 0) { return; } diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 6f98bc5a17a..2ae2df0124b 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -1,8 +1,8 @@ import moment from 'moment'; import _ from 'lodash'; +import { DEFAULT_ANNOTATION_COLOR } from '@grafana/ui'; import { GRID_COLUMN_COUNT, REPEAT_DIR_VERTICAL, GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; -import { DEFAULT_ANNOTATION_COLOR } from 'app/core/utils/colors'; import { Emitter } from 'app/core/utils/emitter'; import { contextSrv } from 'app/core/services/context_srv'; import sortByKeys from 'app/core/utils/sort_by_keys'; @@ -442,7 +442,7 @@ export class DashboardModel { } const selectedOptions = this.getSelectedVariableOptions(variable); - const minWidth = panel.minSpan || 6; + const maxPerRow = panel.maxPerRow || 4; let xPos = 0; let yPos = panel.gridPos.y; @@ -462,7 +462,7 @@ export class DashboardModel { } else { // set width based on how many are selected // assumed the repeated panels should take up full row width - copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selectedOptions.length, minWidth); + copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selectedOptions.length, GRID_COLUMN_COUNT / maxPerRow); copy.gridPos.x = xPos; copy.gridPos.y = yPos; diff --git a/public/app/features/dashboard/dashgrid/AlertTab.tsx b/public/app/features/dashboard/dashgrid/AlertTab.tsx deleted file mode 100644 index 7df7864c758..00000000000 --- a/public/app/features/dashboard/dashgrid/AlertTab.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React, { PureComponent } from 'react'; - -import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; -import { EditorTabBody } from './EditorTabBody'; -import 'app/features/alerting/AlertTabCtrl'; - -interface Props { - angularPanel?: AngularComponent; -} - -export class AlertTab extends PureComponent { - element: any; - component: AngularComponent; - - constructor(props) { - super(props); - } - - componentDidMount() { - if (this.shouldLoadAlertTab()) { - this.loadAlertTab(); - } - } - - componentDidUpdate(prevProps: Props) { - if (this.shouldLoadAlertTab()) { - this.loadAlertTab(); - } - } - - shouldLoadAlertTab() { - return this.props.angularPanel && this.element; - } - - componentWillUnmount() { - if (this.component) { - this.component.destroy(); - } - } - - loadAlertTab() { - const { angularPanel } = this.props; - - const scope = angularPanel.getScope(); - - // When full page reloading in edit mode the angular panel has on fully compiled & instantiated yet - if (!scope.$$childHead) { - setTimeout(() => { - this.forceUpdate(); - }); - return; - } - - const panelCtrl = scope.$$childHead.ctrl; - const loader = getAngularLoader(); - const template = ''; - - const scopeProps = { - ctrl: panelCtrl, - }; - - this.component = loader.load(this.element, scopeProps, template); - } - - render() { - return ( - -
(this.element = element)} /> - - ); - } -} diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 9de298e0799..f0e97162d43 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -9,7 +9,7 @@ import { AddPanelPanel } from './AddPanelPanel'; import { getPanelPluginNotFound } from './PanelPluginNotFound'; import { DashboardRow } from './DashboardRow'; import { PanelChrome } from './PanelChrome'; -import { PanelEditor } from './PanelEditor'; +import { PanelEditor } from '../panel_editor/PanelEditor'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index 30a939b50aa..d71a274ab10 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -1,5 +1,9 @@ // Library import React, { Component } from 'react'; +import { Tooltip } from '@grafana/ui'; +import { Themes } from '@grafana/ui/src/components/Tooltip/Popper'; + +import ErrorBoundary from 'app/core/components/ErrorBoundary/ErrorBoundary'; // Services import { getDatasourceSrv, DatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -11,6 +15,8 @@ import kbn from 'app/core/utils/kbn'; import { DataQueryOptions, DataQueryResponse } from 'app/types'; import { TimeRange, TimeSeries, LoadingState } from '@grafana/ui'; +const DEFAULT_PLUGIN_ERROR = 'Error in plugin'; + interface RenderProps { loading: LoadingState; timeSeries: TimeSeries[]; @@ -33,6 +39,7 @@ export interface Props { export interface State { isFirstLoad: boolean; loading: LoadingState; + errorMessage: string; response: DataQueryResponse; } @@ -51,6 +58,7 @@ export class DataPanel extends Component { this.state = { loading: LoadingState.NotStarted, + errorMessage: '', response: { data: [], }, @@ -90,7 +98,7 @@ export class DataPanel extends Component { return; } - this.setState({ loading: LoadingState.Loading }); + this.setState({ loading: LoadingState.Loading, errorMessage: '' }); try { const ds = await this.dataSourceSrv.get(datasource); @@ -128,7 +136,17 @@ export class DataPanel extends Component { }); } catch (err) { console.log('Loading error', err); - this.setState({ loading: LoadingState.Error, isFirstLoad: false }); + this.onError('Request Error'); + } + }; + + onError = (errorMessage: string) => { + if (this.state.loading !== LoadingState.Error || this.state.errorMessage !== errorMessage) { + this.setState({ + loading: LoadingState.Error, + isFirstLoad: false, + errorMessage: errorMessage, + }); } }; @@ -139,7 +157,7 @@ export class DataPanel extends Component { const timeSeries = response.data; if (isFirstLoad && loading === LoadingState.Loading) { - return this.renderLoadingSpinner(); + return this.renderLoadingStates(); } if (!queries.length) { @@ -152,24 +170,44 @@ export class DataPanel extends Component { return ( <> - {this.renderLoadingSpinner()} - {this.props.children({ - timeSeries, - loading, - })} + {this.renderLoadingStates()} + + {({ error, errorInfo }) => { + if (errorInfo) { + this.onError(error.message || DEFAULT_PLUGIN_ERROR); + return null; + } + return ( + <> + {this.props.children({ + timeSeries, + loading, + })} + + ); + }} + ); } - private renderLoadingSpinner(): JSX.Element { - const { loading } = this.state; - + private renderLoadingStates(): JSX.Element { + const { loading, errorMessage } = this.state; if (loading === LoadingState.Loading) { return (
); + } else if (loading === LoadingState.Error) { + return ( + +
+ + +
+
+ ); } return null; diff --git a/public/app/features/dashboard/dashgrid/KeyboardNavigation.tsx b/public/app/features/dashboard/dashgrid/KeyboardNavigation.tsx deleted file mode 100644 index dab8371c925..00000000000 --- a/public/app/features/dashboard/dashgrid/KeyboardNavigation.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { KeyboardEvent, Component } from 'react'; - -interface State { - selected: number; -} - -export interface KeyboardNavigationProps { - onKeyDown: (evt: KeyboardEvent, maxSelectedIndex: number, onEnterAction: () => void) => void; - onMouseEnter: (select: number) => void; - selected: number; -} - -interface Props { - render: (injectProps: any) => void; -} - -class KeyboardNavigation extends Component { - constructor(props) { - super(props); - - this.state = { - selected: 0, - }; - } - - goToNext = (maxSelectedIndex: number) => { - const nextIndex = this.state.selected >= maxSelectedIndex ? 0 : this.state.selected + 1; - this.setState({ - selected: nextIndex, - }); - }; - - goToPrev = (maxSelectedIndex: number) => { - const nextIndex = this.state.selected <= 0 ? maxSelectedIndex : this.state.selected - 1; - this.setState({ - selected: nextIndex, - }); - }; - - onKeyDown = (evt: KeyboardEvent, maxSelectedIndex: number, onEnterAction: any) => { - if (evt.key === 'ArrowDown') { - evt.preventDefault(); - this.goToNext(maxSelectedIndex); - } - if (evt.key === 'ArrowUp') { - evt.preventDefault(); - this.goToPrev(maxSelectedIndex); - } - if (evt.key === 'Enter' && onEnterAction) { - onEnterAction(); - } - }; - - onMouseEnter = (mouseEnterIndex: number) => { - this.setState({ - selected: mouseEnterIndex, - }); - }; - - render() { - const injectProps = { - onKeyDown: this.onKeyDown, - onMouseEnter: this.onMouseEnter, - selected: this.state.selected, - }; - - return <>{this.props.render({ ...injectProps })}; - } -} - -export default KeyboardNavigation; diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 94719dfe6e0..6b4ef48c32e 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -19,6 +19,9 @@ import { DashboardModel } from '../dashboard_model'; import { PanelPlugin } from 'app/types'; import { TimeRange } from '@grafana/ui'; +import variables from 'sass/_variables.scss'; +import templateSrv from 'app/features/templating/template_srv'; + export interface Props { panel: PanelModel; dashboard: DashboardModel; @@ -76,6 +79,10 @@ export class PanelChrome extends PureComponent { }); }; + onInterpolate = (value: string, format?: string) => { + return templateSrv.replace(value, this.props.panel.scopedVars, format); + }; + get isVisible() { return !this.props.dashboard.otherPanelInFullscreen(this.props.panel); } @@ -87,7 +94,6 @@ export class PanelChrome extends PureComponent { const { datasource, targets, transparent } = panel; const PanelComponent = plugin.exports.Panel; const containerClassNames = `panel-container panel-container--absolute ${transparent ? 'panel-transparent' : ''}`; - return ( {({ width, height }) => { @@ -123,9 +129,10 @@ export class PanelChrome extends PureComponent { timeSeries={timeSeries} timeRange={timeRange} options={panel.getOptions(plugin.exports.PanelDefaults)} - width={width} - height={height - PANEL_HEADER_HEIGHT} + width={width - 2 * variables.panelHorizontalPadding} + height={height - PANEL_HEADER_HEIGHT - variables.panelVerticalPadding} renderCounter={renderCounter} + onInterpolate={this.onInterpolate} />
); diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx index 8b7afd7d09e..b5cd9258c08 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -3,6 +3,7 @@ import classNames from 'classnames'; import PanelHeaderCorner from './PanelHeaderCorner'; import { PanelHeaderMenu } from './PanelHeaderMenu'; +import templateSrv from 'app/features/templating/template_srv'; import { DashboardModel } from 'app/features/dashboard/dashboard_model'; import { PanelModel } from 'app/features/dashboard/panel_model'; @@ -45,7 +46,9 @@ export class PanelHeader extends Component { const isFullscreen = false; const isLoading = false; const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); - const { panel, dashboard, timeInfo } = this.props; + const { panel, dashboard, timeInfo, scopedVars } = this.props; + const title = templateSrv.replaceWithText(panel.title, scopedVars); + return ( <> {
- {panel.title} + {title} {this.state.panelMenuOpen && ( diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx index 331e469a60d..6b6f81fc579 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx @@ -1,10 +1,10 @@ import React, { Component } from 'react'; +import Remarkable from 'remarkable'; +import { Tooltip } from '@grafana/ui'; import { PanelModel } from 'app/features/dashboard/panel_model'; -import Tooltip from 'app/core/components/Tooltip/Tooltip'; import templateSrv from 'app/features/templating/template_srv'; import { LinkSrv } from 'app/features/dashboard/panellinks/link_srv'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/time_srv'; -import Remarkable from 'remarkable'; enum InfoModes { Error = 'Error', @@ -78,12 +78,14 @@ export class PanelHeaderCorner extends Component { {infoMode === InfoModes.Info || infoMode === InfoModes.Links ? ( - - +
+ + +
) : null} diff --git a/public/app/features/dashboard/dashgrid/PanelLoader.ts b/public/app/features/dashboard/dashgrid/PanelLoader.ts deleted file mode 100644 index c654b756085..00000000000 --- a/public/app/features/dashboard/dashgrid/PanelLoader.ts +++ /dev/null @@ -1,31 +0,0 @@ -import angular from 'angular'; -import coreModule from 'app/core/core_module'; - -export interface AttachedPanel { - destroy(); -} - -export class PanelLoader { - /** @ngInject */ - constructor(private $compile, private $rootScope) {} - - load(elem, panel, dashboard): AttachedPanel { - const template = ''; - const panelScope = this.$rootScope.$new(); - panelScope.panel = panel; - panelScope.dashboard = dashboard; - - const compiledElem = this.$compile(template)(panelScope); - const rootNode = angular.element(elem); - rootNode.append(compiledElem); - - return { - destroy: () => { - panelScope.$destroy(); - compiledElem.remove(); - }, - }; - } -} - -coreModule.service('panelLoader', PanelLoader); diff --git a/public/app/features/dashboard/dashgrid/DataSourceOption.tsx b/public/app/features/dashboard/panel_editor/DataSourceOption.tsx similarity index 93% rename from public/app/features/dashboard/dashgrid/DataSourceOption.tsx rename to public/app/features/dashboard/panel_editor/DataSourceOption.tsx index 0adfc4abe16..9a3ce527510 100644 --- a/public/app/features/dashboard/dashgrid/DataSourceOption.tsx +++ b/public/app/features/dashboard/panel_editor/DataSourceOption.tsx @@ -1,5 +1,5 @@ import React, { SFC } from 'react'; -import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import { Tooltip } from '@grafana/ui'; interface Props { label: string; diff --git a/public/app/features/dashboard/dashgrid/EditorTabBody.tsx b/public/app/features/dashboard/panel_editor/EditorTabBody.tsx similarity index 79% rename from public/app/features/dashboard/dashgrid/EditorTabBody.tsx rename to public/app/features/dashboard/panel_editor/EditorTabBody.tsx index 7606d327405..dbea7ed59bc 100644 --- a/public/app/features/dashboard/dashgrid/EditorTabBody.tsx +++ b/public/app/features/dashboard/panel_editor/EditorTabBody.tsx @@ -2,29 +2,29 @@ import React, { PureComponent } from 'react'; // Components -import CustomScrollbar from 'app/core/components/CustomScrollbar/CustomScrollbar'; +import { CustomScrollbar, PanelOptionsGroup } from '@grafana/ui'; import { FadeIn } from 'app/core/components/Animations/FadeIn'; -import { PanelOptionSection } from './PanelOptionSection'; interface Props { children: JSX.Element; heading: string; renderToolbar?: () => JSX.Element; - toolbarItems?: EditorToolBarView[]; + toolbarItems?: EditorToolbarView[]; } -export interface EditorToolBarView { +export interface EditorToolbarView { title?: string; heading?: string; - imgSrc?: string; icon?: string; disabled?: boolean; onClick?: () => void; - render: (closeFunction?: any) => JSX.Element | JSX.Element[]; + render?: () => JSX.Element; + action?: () => void; + btnType?: 'danger'; } interface State { - openView?: EditorToolBarView; + openView?: EditorToolbarView; isOpen: boolean; fadeIn: boolean; } @@ -48,10 +48,10 @@ export class EditorTabBody extends PureComponent { this.setState({ fadeIn: true }); } - onToggleToolBarView = (item: EditorToolBarView) => { + onToggleToolBarView = (item: EditorToolbarView) => { this.setState({ openView: item, - isOpen: !this.state.isOpen, + isOpen: this.state.openView !== item || !this.state.isOpen, }); }; @@ -74,12 +74,15 @@ export class EditorTabBody extends PureComponent { return state; } - renderButton(view: EditorToolBarView) { + renderButton(view: EditorToolbarView) { const onClick = () => { if (view.onClick) { view.onClick(); } - this.onToggleToolBarView(view); + + if (view.render) { + this.onToggleToolBarView(view); + } }; return ( @@ -91,11 +94,11 @@ export class EditorTabBody extends PureComponent { ); } - renderOpenView(view: EditorToolBarView) { + renderOpenView(view: EditorToolbarView) { return ( - + {view.render()} - + ); } diff --git a/public/app/features/dashboard/dashgrid/GeneralTab.tsx b/public/app/features/dashboard/panel_editor/GeneralTab.tsx similarity index 100% rename from public/app/features/dashboard/dashgrid/GeneralTab.tsx rename to public/app/features/dashboard/panel_editor/GeneralTab.tsx diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/panel_editor/PanelEditor.tsx similarity index 92% rename from public/app/features/dashboard/dashgrid/PanelEditor.tsx rename to public/app/features/dashboard/panel_editor/PanelEditor.tsx index a746d6c4b91..a09ff66f114 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/panel_editor/PanelEditor.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames'; import { QueriesTab } from './QueriesTab'; import { VisualizationTab } from './VisualizationTab'; import { GeneralTab } from './GeneralTab'; -import { AlertTab } from './AlertTab'; +import { AlertTab } from '../../alerting/AlertTab'; import config from 'app/core/config'; import { store } from 'app/store/store'; @@ -15,7 +15,8 @@ import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; import { PanelPlugin } from 'app/types/plugins'; -import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import { Tooltip } from '@grafana/ui'; +import { Themes } from '@grafana/ui/src/components/Tooltip/Popper'; interface PanelEditorProps { panel: PanelModel; @@ -54,7 +55,7 @@ export class PanelEditor extends PureComponent { case 'queries': return ; case 'alert': - return ; + return ; case 'visualization': return ( onClick(tab)}> - + diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/panel_editor/QueriesTab.tsx similarity index 87% rename from public/app/features/dashboard/dashgrid/QueriesTab.tsx rename to public/app/features/dashboard/panel_editor/QueriesTab.tsx index 9ad0bb3cadd..47c4f358136 100644 --- a/public/app/features/dashboard/dashgrid/QueriesTab.tsx +++ b/public/app/features/dashboard/panel_editor/QueriesTab.tsx @@ -1,26 +1,26 @@ // Libraries -import React, { SFC, PureComponent } from 'react'; +import React, { PureComponent } from 'react'; import _ from 'lodash'; // Components -import './../../panel/metrics_tab'; -import { EditorTabBody } from './EditorTabBody'; +import 'app/features/panel/metrics_tab'; +import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { QueryInspector } from './QueryInspector'; import { QueryOptions } from './QueryOptions'; import { AngularQueryComponentScope } from 'app/features/panel/metrics_tab'; -import { PanelOptionSection } from './PanelOptionSection'; +import { PanelOptionsGroup } from '@grafana/ui'; // Services import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv'; -import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; +import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv'; +import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; import config from 'app/core/config'; // Types import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; -import { DataSourceSelectItem, DataQuery } from 'app/types'; +import { DataQuery, DataSourceSelectItem } from 'app/types'; import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; interface Props { @@ -36,12 +36,6 @@ interface State { isAddingMixed: boolean; } -interface LoadingPlaceholderProps { - text: string; -} - -const LoadingPlaceholder: SFC = ({ text }) =>

{text}

; - export class QueriesTab extends PureComponent { element: HTMLElement; component: AngularComponent; @@ -50,17 +44,21 @@ export class QueriesTab extends PureComponent { constructor(props) { super(props); - const { panel } = props; this.state = { - currentDS: this.datasources.find(datasource => datasource.value === panel.datasource), isLoadingHelp: false, + currentDS: this.findCurrentDataSource(), helpContent: null, isPickerOpen: false, isAddingMixed: false, }; } + findCurrentDataSource(): DataSourceSelectItem { + const { panel } = this.props; + return this.datasources.find(datasource => datasource.value === panel.datasource) || this.datasources[0]; + } + getAngularQueryComponentScope(): AngularQueryComponentScope { const { panel, dashboard } = this.props; @@ -130,7 +128,7 @@ export class QueriesTab extends PureComponent { renderQueryInspector = () => { const { panel } = this.props; - return ; + return ; }; renderHelp = () => { @@ -204,12 +202,12 @@ export class QueriesTab extends PureComponent { const { panel } = this.props; const { currentDS, isAddingMixed } = this.state; - const queryInspector = { + const queryInspector: EditorToolbarView = { title: 'Query Inspector', render: this.renderQueryInspector, }; - const dsHelp = { + const dsHelp: EditorToolbarView = { heading: 'Help', icon: 'fa fa-question', render: this.renderHelp, @@ -218,7 +216,7 @@ export class QueriesTab extends PureComponent { return ( <> - +
(this.element = element)} /> @@ -241,10 +239,10 @@ export class QueriesTab extends PureComponent {
- - + + - +
); diff --git a/public/app/features/dashboard/dashgrid/QueryInspector.tsx b/public/app/features/dashboard/panel_editor/QueryInspector.tsx similarity index 98% rename from public/app/features/dashboard/dashgrid/QueryInspector.tsx rename to public/app/features/dashboard/panel_editor/QueryInspector.tsx index 090bc220bc0..8e490f6b622 100644 --- a/public/app/features/dashboard/dashgrid/QueryInspector.tsx +++ b/public/app/features/dashboard/panel_editor/QueryInspector.tsx @@ -2,6 +2,7 @@ import React, { PureComponent } from 'react'; import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter'; import appEvents from 'app/core/app_events'; import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard'; +import { LoadingPlaceholder } from '@grafana/ui'; interface DsQuery { isLoading: boolean; @@ -10,7 +11,6 @@ interface DsQuery { interface Props { panel: any; - LoadingPlaceholder: any; } interface State { @@ -177,7 +177,6 @@ export class QueryInspector extends PureComponent { render() { const { response, isLoading } = this.state.dsQuery; - const { LoadingPlaceholder } = this.props; const { isMocking } = this.state; const openNodes = this.getNrOfOpenNodes(); diff --git a/public/app/features/dashboard/dashgrid/QueryOptions.tsx b/public/app/features/dashboard/panel_editor/QueryOptions.tsx similarity index 100% rename from public/app/features/dashboard/dashgrid/QueryOptions.tsx rename to public/app/features/dashboard/panel_editor/QueryOptions.tsx diff --git a/public/app/features/dashboard/dashgrid/VisualizationTab.tsx b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx similarity index 92% rename from public/app/features/dashboard/dashgrid/VisualizationTab.tsx rename to public/app/features/dashboard/panel_editor/VisualizationTab.tsx index 42d9bf6a6eb..ad569a9ff90 100644 --- a/public/app/features/dashboard/dashgrid/VisualizationTab.tsx +++ b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx @@ -2,14 +2,13 @@ import React, { PureComponent } from 'react'; // Utils & Services -import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; +import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; // Components -import { EditorTabBody } from './EditorTabBody'; +import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; import { VizTypePicker } from './VizTypePicker'; import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; import { FadeIn } from 'app/core/components/Animations/FadeIn'; -import { PanelOptionSection } from './PanelOptionSection'; // Types import { PanelModel } from '../panel_model'; @@ -62,13 +61,13 @@ export class VisualizationTab extends PureComponent { } return ( - + <> {PanelOptions ? ( ) : (

Visualization has no options

)} -
+ ); } @@ -112,9 +111,9 @@ export class VisualizationTab extends PureComponent { for (let i = 0; i < panelCtrl.editorTabs.length; i++) { template += ` -
` + - (i > 0 ? `
{{ctrl.editorTabs[${i}].title}}
` : '') + - `
+
` + + (i > 0 ? `
{{ctrl.editorTabs[${i}].title}}
` : '') + + `
@@ -206,7 +205,7 @@ export class VisualizationTab extends PureComponent { const { plugin } = this.props; const { isVizPickerOpen, searchQuery } = this.state; - const pluginHelp = { + const pluginHelp: EditorToolbarView = { heading: 'Help', icon: 'fa fa-question', render: this.renderHelp, diff --git a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx b/public/app/features/dashboard/panel_editor/VizTypePicker.tsx similarity index 100% rename from public/app/features/dashboard/dashgrid/VizTypePicker.tsx rename to public/app/features/dashboard/panel_editor/VizTypePicker.tsx diff --git a/public/app/features/dashboard/dashgrid/VizTypePickerPlugin.tsx b/public/app/features/dashboard/panel_editor/VizTypePickerPlugin.tsx similarity index 100% rename from public/app/features/dashboard/dashgrid/VizTypePickerPlugin.tsx rename to public/app/features/dashboard/panel_editor/VizTypePickerPlugin.tsx diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index 2d5a70b47dd..2fec8e379dd 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -77,7 +77,7 @@ export class PanelModel { repeatPanelId?: number; repeatDirection?: string; repeatedByRow?: boolean; - minSpan?: number; + maxPerRow?: number; collapsed?: boolean; panels?: any; soloMode?: boolean; diff --git a/public/app/features/dashboard/permissions/DashboardPermissions.tsx b/public/app/features/dashboard/permissions/DashboardPermissions.tsx index c07bef42930..96d0e23adcd 100644 --- a/public/app/features/dashboard/permissions/DashboardPermissions.tsx +++ b/public/app/features/dashboard/permissions/DashboardPermissions.tsx @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import { Tooltip } from '@grafana/ui'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { StoreState, FolderInfo } from 'app/types'; import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl'; @@ -70,8 +70,10 @@ export class DashboardPermissions extends PureComponent {

Permissions

- - + +
+ +
@@ -959,80 +284,97 @@ export class Explore extends React.PureComponent {
)} - {datasource && !datasourceError ? ( -
- -
- - {showingStartPage && } - {!showingStartPage && ( - <> - {supportsGraph && ( - - - - )} - {supportsTable && ( - - - - )} - {supportsLogs && ( - - - - )} - + {datasourceInstance && + !datasourceError && ( +
+ + + {({ width }) => ( +
+ + {showingStartPage && } + {!showingStartPage && ( + <> + {supportsGraph && } + {supportsTable && } + {supportsLogs && ( + + )} + + )} + +
)} - - -
- ) : null} + + + )} ); } } -export default hot(module)(Explore); +function mapStateToProps(state: StoreState, { exploreId }) { + const explore = state.explore; + const { split } = explore; + const item: ExploreItemState = explore[exploreId]; + const { + StartPage, + datasourceError, + datasourceInstance, + datasourceLoading, + datasourceMissing, + exploreDatasources, + initialDatasource, + initialQueries, + initialized, + queryTransactions, + range, + showingStartPage, + supportsGraph, + supportsLogs, + supportsTable, + } = item; + const loading = queryTransactions.some(qt => !qt.done); + return { + StartPage, + datasourceError, + datasourceInstance, + datasourceLoading, + datasourceMissing, + exploreDatasources, + initialDatasource, + initialQueries, + initialized, + loading, + queryTransactions, + range, + showingStartPage, + split, + supportsGraph, + supportsLogs, + supportsTable, + }; +} + +const mapDispatchToProps = { + changeDatasource, + changeSize, + changeTime, + clearQueries, + initializeExplore, + modifyQueries, + runQueries, + scanStart, + scanStop, + setQueries, + splitClose, + splitOpen, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Explore)); diff --git a/public/app/features/explore/GraphContainer.tsx b/public/app/features/explore/GraphContainer.tsx new file mode 100644 index 00000000000..e2610bcc781 --- /dev/null +++ b/public/app/features/explore/GraphContainer.tsx @@ -0,0 +1,61 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import { RawTimeRange, TimeRange } from '@grafana/ui'; + +import { ExploreId, ExploreItemState } from 'app/types/explore'; +import { StoreState } from 'app/types'; + +import { toggleGraph } from './state/actions'; +import Graph from './Graph'; +import Panel from './Panel'; + +interface GraphContainerProps { + onChangeTime: (range: TimeRange) => void; + exploreId: ExploreId; + graphResult?: any[]; + loading: boolean; + range: RawTimeRange; + showingGraph: boolean; + showingTable: boolean; + split: boolean; + toggleGraph: typeof toggleGraph; +} + +export class GraphContainer extends PureComponent { + onClickGraphButton = () => { + this.props.toggleGraph(this.props.exploreId); + }; + + render() { + const { exploreId, graphResult, loading, onChangeTime, showingGraph, showingTable, range, split } = this.props; + const graphHeight = showingGraph && showingTable ? '200px' : '400px'; + return ( + + + + ); + } +} + +function mapStateToProps(state: StoreState, { exploreId }) { + const explore = state.explore; + const { split } = explore; + const item: ExploreItemState = explore[exploreId]; + const { graphResult, queryTransactions, range, showingGraph, showingTable } = item; + const loading = queryTransactions.some(qt => qt.resultType === 'Graph' && !qt.done); + return { graphResult, loading, range, showingGraph, showingTable, split }; +} + +const mapDispatchToProps = { + toggleGraph, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(GraphContainer)); diff --git a/public/app/features/explore/LogLabels.tsx b/public/app/features/explore/LogLabels.tsx index 8aa1789017e..7675fb13152 100644 --- a/public/app/features/explore/LogLabels.tsx +++ b/public/app/features/explore/LogLabels.tsx @@ -1,4 +1,3 @@ -import _ from 'lodash'; import React, { PureComponent } from 'react'; import classnames from 'classnames'; diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 1a384cf011d..d07b31e2ff1 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -241,9 +241,9 @@ function renderMetaItem(value: any, kind: LogsMetaKind) { interface LogsProps { data: LogsModel; + exploreId: string; highlighterExpressions: string[]; loading: boolean; - position: string; range?: RawTimeRange; scanning?: boolean; scanRange?: RawTimeRange; @@ -348,10 +348,10 @@ export default class Logs extends PureComponent { render() { const { data, + exploreId, highlighterExpressions, loading = false, onClickLabel, - position, range, scanning, scanRange, @@ -400,7 +400,7 @@ export default class Logs extends PureComponent { data={data.series} height="100px" range={range} - id={`explore-logs-graph-${position}`} + id={`explore-logs-graph-${exploreId}`} onChangeTime={this.props.onChangeTime} onToggleSeries={this.onToggleLogLevel} userOptions={graphOptions} diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx new file mode 100644 index 00000000000..e58cd2b5e95 --- /dev/null +++ b/public/app/features/explore/LogsContainer.tsx @@ -0,0 +1,91 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import { RawTimeRange, TimeRange } from '@grafana/ui'; + +import { ExploreId, ExploreItemState } from 'app/types/explore'; +import { LogsModel } from 'app/core/logs_model'; +import { StoreState } from 'app/types'; + +import { toggleLogs } from './state/actions'; +import Logs from './Logs'; +import Panel from './Panel'; + +interface LogsContainerProps { + exploreId: ExploreId; + loading: boolean; + logsHighlighterExpressions?: string[]; + logsResult?: LogsModel; + onChangeTime: (range: TimeRange) => void; + onClickLabel: (key: string, value: string) => void; + onStartScanning: () => void; + onStopScanning: () => void; + range: RawTimeRange; + scanning?: boolean; + scanRange?: RawTimeRange; + showingLogs: boolean; + toggleLogs: typeof toggleLogs; +} + +export class LogsContainer extends PureComponent { + onClickLogsButton = () => { + this.props.toggleLogs(this.props.exploreId); + }; + + render() { + const { + exploreId, + loading, + logsHighlighterExpressions, + logsResult, + onChangeTime, + onClickLabel, + onStartScanning, + onStopScanning, + range, + showingLogs, + scanning, + scanRange, + } = this.props; + return ( + + + + ); + } +} + +function mapStateToProps(state: StoreState, { exploreId }) { + const explore = state.explore; + const item: ExploreItemState = explore[exploreId]; + const { logsHighlighterExpressions, logsResult, queryTransactions, scanning, scanRange, showingLogs, range } = item; + const loading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done); + return { + loading, + logsHighlighterExpressions, + logsResult, + scanning, + scanRange, + showingLogs, + range, + }; +} + +const mapDispatchToProps = { + toggleLogs, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LogsContainer)); diff --git a/public/app/features/explore/QueryEditor.tsx b/public/app/features/explore/QueryEditor.tsx index ce0a8a6e03e..dde674d3fcd 100644 --- a/public/app/features/explore/QueryEditor.tsx +++ b/public/app/features/explore/QueryEditor.tsx @@ -48,7 +48,7 @@ export default class QueryEditor extends PureComponent { getNextQueryLetter: x => '', }, hideEditorRowActions: true, - ...getIntervals(range, datasource, null), // Possible to get resolution? + ...getIntervals(range, (datasource || {}).interval, null), // Possible to get resolution? }, }; diff --git a/public/app/features/explore/QueryRow.tsx b/public/app/features/explore/QueryRow.tsx new file mode 100644 index 00000000000..b5b150b3ba8 --- /dev/null +++ b/public/app/features/explore/QueryRow.tsx @@ -0,0 +1,163 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import { RawTimeRange } from '@grafana/ui'; +import _ from 'lodash'; + +import { QueryTransaction, HistoryItem, QueryHint, ExploreItemState, ExploreId } from 'app/types/explore'; +import { Emitter } from 'app/core/utils/emitter'; +import { DataQuery, StoreState } from 'app/types'; + +// import DefaultQueryField from './QueryField'; +import QueryEditor from './QueryEditor'; +import QueryTransactionStatus from './QueryTransactionStatus'; +import { + addQueryRow, + changeQuery, + highlightLogsExpression, + modifyQueries, + removeQueryRow, + runQueries, +} from './state/actions'; + +function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint { + const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0); + if (transaction) { + return transaction.hints[0]; + } + return undefined; +} + +interface QueryRowProps { + addQueryRow: typeof addQueryRow; + changeQuery: typeof changeQuery; + className?: string; + exploreId: ExploreId; + datasourceInstance: any; + highlightLogsExpression: typeof highlightLogsExpression; + history: HistoryItem[]; + index: number; + initialQuery: DataQuery; + modifyQueries: typeof modifyQueries; + queryTransactions: QueryTransaction[]; + exploreEvents: Emitter; + range: RawTimeRange; + removeQueryRow: typeof removeQueryRow; + runQueries: typeof runQueries; +} + +export class QueryRow extends PureComponent { + onExecuteQuery = () => { + const { exploreId } = this.props; + this.props.runQueries(exploreId); + }; + + onChangeQuery = (query: DataQuery, override?: boolean) => { + const { datasourceInstance, exploreId, index } = this.props; + this.props.changeQuery(exploreId, query, index, override); + if (query && !override && datasourceInstance.getHighlighterExpression && index === 0) { + // Live preview of log search matches. Only use on first row for now + this.updateLogsHighlights(query); + } + }; + + onClickAddButton = () => { + const { exploreId, index } = this.props; + this.props.addQueryRow(exploreId, index); + }; + + onClickClearButton = () => { + this.onChangeQuery(null, true); + }; + + onClickHintFix = action => { + const { datasourceInstance, exploreId, index } = this.props; + if (datasourceInstance && datasourceInstance.modifyQuery) { + const modifier = (queries: DataQuery, action: any) => datasourceInstance.modifyQuery(queries, action); + this.props.modifyQueries(exploreId, action, index, modifier); + } + }; + + onClickRemoveButton = () => { + const { exploreId, index } = this.props; + this.props.removeQueryRow(exploreId, index); + }; + + updateLogsHighlights = _.debounce((value: DataQuery) => { + const { datasourceInstance } = this.props; + if (datasourceInstance.getHighlighterExpression) { + const expressions = [datasourceInstance.getHighlighterExpression(value)]; + this.props.highlightLogsExpression(this.props.exploreId, expressions); + } + }, 500); + + render() { + const { datasourceInstance, history, index, initialQuery, queryTransactions, exploreEvents, range } = this.props; + const transactions = queryTransactions.filter(t => t.rowIndex === index); + const transactionWithError = transactions.find(t => t.error !== undefined); + const hint = getFirstHintFromTransactions(transactions); + const queryError = transactionWithError ? transactionWithError.error : null; + const QueryField = datasourceInstance.pluginExports.ExploreQueryField; + return ( +
+
+ +
+
+ {QueryField ? ( + + ) : ( + + )} +
+
+ + + +
+
+ ); + } +} + +function mapStateToProps(state: StoreState, { exploreId, index }) { + const explore = state.explore; + const item: ExploreItemState = explore[exploreId]; + const { datasourceInstance, history, initialQueries, queryTransactions, range } = item; + const initialQuery = initialQueries[index]; + return { datasourceInstance, history, initialQuery, queryTransactions, range }; +} + +const mapDispatchToProps = { + addQueryRow, + changeQuery, + highlightLogsExpression, + modifyQueries, + removeQueryRow, + runQueries, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(QueryRow)); diff --git a/public/app/features/explore/QueryRows.tsx b/public/app/features/explore/QueryRows.tsx index 4101475092b..01bd409f444 100644 --- a/public/app/features/explore/QueryRows.tsx +++ b/public/app/features/explore/QueryRows.tsx @@ -1,159 +1,25 @@ import React, { PureComponent } from 'react'; -import { QueryTransaction, HistoryItem, QueryHint } from 'app/types/explore'; import { Emitter } from 'app/core/utils/emitter'; +import { DataQuery } from 'app/types'; +import { ExploreId } from 'app/types/explore'; -// import DefaultQueryField from './QueryField'; -import QueryEditor from './QueryEditor'; -import QueryTransactionStatus from './QueryTransactionStatus'; -import { DataSource, DataQuery } from 'app/types'; -import { RawTimeRange } from '@grafana/ui'; +import QueryRow from './QueryRow'; -function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint { - const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0); - if (transaction) { - return transaction.hints[0]; - } - return undefined; -} - -interface QueryRowEventHandlers { - onAddQueryRow: (index: number) => void; - onChangeQuery: (value: DataQuery, index: number, override?: boolean) => void; - onClickHintFix: (action: object, index?: number) => void; - onExecuteQuery: () => void; - onRemoveQueryRow: (index: number) => void; -} - -interface QueryRowCommonProps { +interface QueryRowsProps { className?: string; - datasource: DataSource; - history: HistoryItem[]; - transactions: QueryTransaction[]; exploreEvents: Emitter; - range: RawTimeRange; + exploreId: ExploreId; + initialQueries: DataQuery[]; } - -type QueryRowProps = QueryRowCommonProps & - QueryRowEventHandlers & { - index: number; - initialQuery: DataQuery; - }; - -class QueryRow extends PureComponent { - onExecuteQuery = () => { - const { onExecuteQuery } = this.props; - onExecuteQuery(); - }; - - onChangeQuery = (value: DataQuery, override?: boolean) => { - const { index, onChangeQuery } = this.props; - if (onChangeQuery) { - onChangeQuery(value, index, override); - } - }; - - onClickAddButton = () => { - const { index, onAddQueryRow } = this.props; - if (onAddQueryRow) { - onAddQueryRow(index); - } - }; - - onClickClearButton = () => { - this.onChangeQuery(null, true); - }; - - onClickHintFix = action => { - const { index, onClickHintFix } = this.props; - if (onClickHintFix) { - onClickHintFix(action, index); - } - }; - - onClickRemoveButton = () => { - const { index, onRemoveQueryRow } = this.props; - if (onRemoveQueryRow) { - onRemoveQueryRow(index); - } - }; - - onPressEnter = () => { - const { onExecuteQuery } = this.props; - if (onExecuteQuery) { - onExecuteQuery(); - } - }; - - render() { - const { datasource, history, initialQuery, transactions, exploreEvents, range } = this.props; - const transactionWithError = transactions.find(t => t.error !== undefined); - const hint = getFirstHintFromTransactions(transactions); - const queryError = transactionWithError ? transactionWithError.error : null; - const QueryField = datasource.pluginExports.ExploreQueryField; - return ( -
-
- -
-
- {QueryField ? ( - - ) : ( - - )} -
-
- - - -
-
- ); - } -} - -type QueryRowsProps = QueryRowCommonProps & - QueryRowEventHandlers & { - initialQueries: DataQuery[]; - }; - export default class QueryRows extends PureComponent { render() { - const { className = '', initialQueries, transactions, ...handlers } = this.props; + const { className = '', exploreEvents, exploreId, initialQueries } = this.props; return (
{initialQueries.map((query, index) => ( - t.rowIndex === index)} - {...handlers} - /> + // TODO instead of relying on initialQueries, move to react key list in redux + ))}
); diff --git a/public/app/features/explore/TableContainer.tsx b/public/app/features/explore/TableContainer.tsx new file mode 100644 index 00000000000..1d00a441e14 --- /dev/null +++ b/public/app/features/explore/TableContainer.tsx @@ -0,0 +1,49 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; + +import { ExploreId, ExploreItemState } from 'app/types/explore'; +import { StoreState } from 'app/types'; + +import { toggleGraph } from './state/actions'; +import Table from './Table'; +import Panel from './Panel'; +import TableModel from 'app/core/table_model'; + +interface TableContainerProps { + exploreId: ExploreId; + loading: boolean; + onClickCell: (key: string, value: string) => void; + showingTable: boolean; + tableResult?: TableModel; + toggleGraph: typeof toggleGraph; +} + +export class TableContainer extends PureComponent { + onClickTableButton = () => { + this.props.toggleGraph(this.props.exploreId); + }; + + render() { + const { loading, onClickCell, showingTable, tableResult } = this.props; + return ( + +
+ + ); + } +} + +function mapStateToProps(state: StoreState, { exploreId }) { + const explore = state.explore; + const item: ExploreItemState = explore[exploreId]; + const { queryTransactions, showingTable, tableResult } = item; + const loading = queryTransactions.some(qt => qt.resultType === 'Table' && !qt.done); + return { loading, showingTable, tableResult }; +} + +const mapDispatchToProps = { + toggleGraph, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TableContainer)); diff --git a/public/app/features/explore/Typeahead.tsx b/public/app/features/explore/Typeahead.tsx index 721527fbebe..6d6c60c0553 100644 --- a/public/app/features/explore/Typeahead.tsx +++ b/public/app/features/explore/Typeahead.tsx @@ -55,7 +55,7 @@ class TypeaheadItem extends React.PureComponent { interface TypeaheadGroupProps { items: CompletionItem[]; label: string; - onClickItem: (CompletionItem) => void; + onClickItem: (suggestion: CompletionItem) => void; selected: CompletionItem; prefix?: string; } diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx index de1eee4c662..7ea8f228af8 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/Wrapper.tsx @@ -3,91 +3,56 @@ import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import { updateLocation } from 'app/core/actions'; -import { serializeStateToUrlParam, parseUrlState } from 'app/core/utils/explore'; import { StoreState } from 'app/types'; -import { ExploreState } from 'app/types/explore'; +import { ExploreId, ExploreUrlState } from 'app/types/explore'; +import { parseUrlState } from 'app/core/utils/explore'; +import { initializeExploreSplit } from './state/actions'; import ErrorBoundary from './ErrorBoundary'; import Explore from './Explore'; interface WrapperProps { - backendSrv?: any; - datasourceSrv?: any; + initializeExploreSplit: typeof initializeExploreSplit; + split: boolean; updateLocation: typeof updateLocation; urlStates: { [key: string]: string }; } -interface WrapperState { - split: boolean; - splitState: ExploreState; -} - -const STATE_KEY_LEFT = 'state'; -const STATE_KEY_RIGHT = 'stateRight'; - -export class Wrapper extends Component { - urlStates: { [key: string]: string }; +export class Wrapper extends Component { + initialSplit: boolean; + urlStates: { [key: string]: ExploreUrlState }; constructor(props: WrapperProps) { super(props); - this.urlStates = props.urlStates; - this.state = { - split: Boolean(props.urlStates[STATE_KEY_RIGHT]), - splitState: undefined, - }; + this.urlStates = {}; + const { left, right } = props.urlStates; + if (props.urlStates.left) { + this.urlStates.leftState = parseUrlState(left); + } + if (props.urlStates.right) { + this.urlStates.rightState = parseUrlState(right); + this.initialSplit = true; + } } - onChangeSplit = (split: boolean, splitState: ExploreState) => { - this.setState({ split, splitState }); - // When closing split, remove URL state for split part - if (!split) { - delete this.urlStates[STATE_KEY_RIGHT]; - this.props.updateLocation({ - query: this.urlStates, - }); + componentDidMount() { + if (this.initialSplit) { + this.props.initializeExploreSplit(); } - }; - - onSaveState = (key: string, state: ExploreState) => { - const urlState = serializeStateToUrlParam(state, true); - this.urlStates[key] = urlState; - this.props.updateLocation({ - query: this.urlStates, - }); - }; + } render() { - const { datasourceSrv } = this.props; - // State overrides for props from first Explore - const { split, splitState } = this.state; - const urlStateLeft = parseUrlState(this.urlStates[STATE_KEY_LEFT]); - const urlStateRight = parseUrlState(this.urlStates[STATE_KEY_RIGHT]); + const { split } = this.props; + const { leftState, rightState } = this.urlStates; return (
- + {split && ( - + )}
@@ -95,11 +60,14 @@ export class Wrapper extends Component { } } -const mapStateToProps = (state: StoreState) => ({ - urlStates: state.location.query, -}); +const mapStateToProps = (state: StoreState) => { + const urlStates = state.location.query; + const { split } = state.explore; + return { split, urlStates }; +}; const mapDispatchToProps = { + initializeExploreSplit, updateLocation, }; diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts new file mode 100644 index 00000000000..b267da4f2c1 --- /dev/null +++ b/public/app/features/explore/state/actionTypes.ts @@ -0,0 +1,302 @@ +import { RawTimeRange, TimeRange } from '@grafana/ui'; + +import { Emitter } from 'app/core/core'; +import { + ExploreId, + ExploreItemState, + HistoryItem, + RangeScanner, + ResultType, + QueryTransaction, +} from 'app/types/explore'; +import { DataSourceSelectItem } from 'app/types/datasources'; +import { DataQuery } from 'app/types'; + +export enum ActionTypes { + AddQueryRow = 'explore/ADD_QUERY_ROW', + ChangeDatasource = 'explore/CHANGE_DATASOURCE', + ChangeQuery = 'explore/CHANGE_QUERY', + ChangeSize = 'explore/CHANGE_SIZE', + ChangeTime = 'explore/CHANGE_TIME', + ClearQueries = 'explore/CLEAR_QUERIES', + HighlightLogsExpression = 'explore/HIGHLIGHT_LOGS_EXPRESSION', + InitializeExplore = 'explore/INITIALIZE_EXPLORE', + InitializeExploreSplit = 'explore/INITIALIZE_EXPLORE_SPLIT', + LoadDatasourceFailure = 'explore/LOAD_DATASOURCE_FAILURE', + LoadDatasourceMissing = 'explore/LOAD_DATASOURCE_MISSING', + LoadDatasourcePending = 'explore/LOAD_DATASOURCE_PENDING', + LoadDatasourceSuccess = 'explore/LOAD_DATASOURCE_SUCCESS', + ModifyQueries = 'explore/MODIFY_QUERIES', + QueryTransactionFailure = 'explore/QUERY_TRANSACTION_FAILURE', + QueryTransactionStart = 'explore/QUERY_TRANSACTION_START', + QueryTransactionSuccess = 'explore/QUERY_TRANSACTION_SUCCESS', + RemoveQueryRow = 'explore/REMOVE_QUERY_ROW', + RunQueries = 'explore/RUN_QUERIES', + RunQueriesEmpty = 'explore/RUN_QUERIES_EMPTY', + ScanRange = 'explore/SCAN_RANGE', + ScanStart = 'explore/SCAN_START', + ScanStop = 'explore/SCAN_STOP', + SetQueries = 'explore/SET_QUERIES', + SplitClose = 'explore/SPLIT_CLOSE', + SplitOpen = 'explore/SPLIT_OPEN', + StateSave = 'explore/STATE_SAVE', + ToggleGraph = 'explore/TOGGLE_GRAPH', + ToggleLogs = 'explore/TOGGLE_LOGS', + ToggleTable = 'explore/TOGGLE_TABLE', +} + +export interface AddQueryRowAction { + type: ActionTypes.AddQueryRow; + payload: { + exploreId: ExploreId; + index: number; + query: DataQuery; + }; +} + +export interface ChangeQueryAction { + type: ActionTypes.ChangeQuery; + payload: { + exploreId: ExploreId; + query: DataQuery; + index: number; + override: boolean; + }; +} + +export interface ChangeSizeAction { + type: ActionTypes.ChangeSize; + payload: { + exploreId: ExploreId; + width: number; + height: number; + }; +} + +export interface ChangeTimeAction { + type: ActionTypes.ChangeTime; + payload: { + exploreId: ExploreId; + range: TimeRange; + }; +} + +export interface ClearQueriesAction { + type: ActionTypes.ClearQueries; + payload: { + exploreId: ExploreId; + }; +} + +export interface HighlightLogsExpressionAction { + type: ActionTypes.HighlightLogsExpression; + payload: { + exploreId: ExploreId; + expressions: string[]; + }; +} + +export interface InitializeExploreAction { + type: ActionTypes.InitializeExplore; + payload: { + exploreId: ExploreId; + containerWidth: number; + datasource: string; + eventBridge: Emitter; + exploreDatasources: DataSourceSelectItem[]; + queries: DataQuery[]; + range: RawTimeRange; + }; +} + +export interface InitializeExploreSplitAction { + type: ActionTypes.InitializeExploreSplit; +} + +export interface LoadDatasourceFailureAction { + type: ActionTypes.LoadDatasourceFailure; + payload: { + exploreId: ExploreId; + error: string; + }; +} + +export interface LoadDatasourcePendingAction { + type: ActionTypes.LoadDatasourcePending; + payload: { + exploreId: ExploreId; + datasourceId: number; + }; +} + +export interface LoadDatasourceMissingAction { + type: ActionTypes.LoadDatasourceMissing; + payload: { + exploreId: ExploreId; + }; +} + +export interface LoadDatasourceSuccessAction { + type: ActionTypes.LoadDatasourceSuccess; + payload: { + exploreId: ExploreId; + StartPage?: any; + datasourceInstance: any; + history: HistoryItem[]; + initialDatasource: string; + initialQueries: DataQuery[]; + logsHighlighterExpressions?: any[]; + showingStartPage: boolean; + supportsGraph: boolean; + supportsLogs: boolean; + supportsTable: boolean; + }; +} + +export interface ModifyQueriesAction { + type: ActionTypes.ModifyQueries; + payload: { + exploreId: ExploreId; + modification: any; + index: number; + modifier: (queries: DataQuery[], modification: any) => DataQuery[]; + }; +} + +export interface QueryTransactionFailureAction { + type: ActionTypes.QueryTransactionFailure; + payload: { + exploreId: ExploreId; + queryTransactions: QueryTransaction[]; + }; +} + +export interface QueryTransactionStartAction { + type: ActionTypes.QueryTransactionStart; + payload: { + exploreId: ExploreId; + resultType: ResultType; + rowIndex: number; + transaction: QueryTransaction; + }; +} + +export interface QueryTransactionSuccessAction { + type: ActionTypes.QueryTransactionSuccess; + payload: { + exploreId: ExploreId; + history: HistoryItem[]; + queryTransactions: QueryTransaction[]; + }; +} + +export interface RemoveQueryRowAction { + type: ActionTypes.RemoveQueryRow; + payload: { + exploreId: ExploreId; + index: number; + }; +} + +export interface RunQueriesEmptyAction { + type: ActionTypes.RunQueriesEmpty; + payload: { + exploreId: ExploreId; + }; +} + +export interface ScanStartAction { + type: ActionTypes.ScanStart; + payload: { + exploreId: ExploreId; + scanner: RangeScanner; + }; +} + +export interface ScanRangeAction { + type: ActionTypes.ScanRange; + payload: { + exploreId: ExploreId; + range: RawTimeRange; + }; +} + +export interface ScanStopAction { + type: ActionTypes.ScanStop; + payload: { + exploreId: ExploreId; + }; +} + +export interface SetQueriesAction { + type: ActionTypes.SetQueries; + payload: { + exploreId: ExploreId; + queries: DataQuery[]; + }; +} + +export interface SplitCloseAction { + type: ActionTypes.SplitClose; +} + +export interface SplitOpenAction { + type: ActionTypes.SplitOpen; + payload: { + itemState: ExploreItemState; + }; +} + +export interface StateSaveAction { + type: ActionTypes.StateSave; +} + +export interface ToggleTableAction { + type: ActionTypes.ToggleTable; + payload: { + exploreId: ExploreId; + }; +} + +export interface ToggleGraphAction { + type: ActionTypes.ToggleGraph; + payload: { + exploreId: ExploreId; + }; +} + +export interface ToggleLogsAction { + type: ActionTypes.ToggleLogs; + payload: { + exploreId: ExploreId; + }; +} + +export type Action = + | AddQueryRowAction + | ChangeQueryAction + | ChangeSizeAction + | ChangeTimeAction + | ClearQueriesAction + | HighlightLogsExpressionAction + | InitializeExploreAction + | InitializeExploreSplitAction + | LoadDatasourceFailureAction + | LoadDatasourceMissingAction + | LoadDatasourcePendingAction + | LoadDatasourceSuccessAction + | ModifyQueriesAction + | QueryTransactionFailureAction + | QueryTransactionStartAction + | QueryTransactionSuccessAction + | RemoveQueryRowAction + | RunQueriesEmptyAction + | ScanRangeAction + | ScanStartAction + | ScanStopAction + | SetQueriesAction + | SplitCloseAction + | SplitOpenAction + | ToggleGraphAction + | ToggleLogsAction + | ToggleTableAction; diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts new file mode 100644 index 00000000000..ae0bce6a019 --- /dev/null +++ b/public/app/features/explore/state/actions.ts @@ -0,0 +1,757 @@ +import _ from 'lodash'; +import { ThunkAction } from 'redux-thunk'; +import { RawTimeRange, TimeRange } from '@grafana/ui'; + +import { + LAST_USED_DATASOURCE_KEY, + clearQueryKeys, + ensureQueries, + generateEmptyQuery, + hasNonEmptyQuery, + makeTimeSeriesList, + updateHistory, + buildQueryTransaction, + serializeStateToUrlParam, +} from 'app/core/utils/explore'; + +import { updateLocation } from 'app/core/actions'; +import store from 'app/core/store'; +import { DataSourceSelectItem } from 'app/types/datasources'; +import { DataQuery, StoreState } from 'app/types'; +import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { + ExploreId, + ExploreUrlState, + RangeScanner, + ResultType, + QueryOptions, + QueryTransaction, + QueryHint, + QueryHintGetter, +} from 'app/types/explore'; +import { Emitter } from 'app/core/core'; + +import { + Action as ThunkableAction, + ActionTypes, + AddQueryRowAction, + ChangeSizeAction, + HighlightLogsExpressionAction, + LoadDatasourceFailureAction, + LoadDatasourceMissingAction, + LoadDatasourcePendingAction, + LoadDatasourceSuccessAction, + QueryTransactionStartAction, + ScanStopAction, +} from './actionTypes'; + +type ThunkResult = ThunkAction; + +/** + * Adds a query row after the row with the given index. + */ +export function addQueryRow(exploreId: ExploreId, index: number): AddQueryRowAction { + const query = generateEmptyQuery(index + 1); + return { type: ActionTypes.AddQueryRow, payload: { exploreId, index, query } }; +} + +/** + * Loads a new datasource identified by the given name. + */ +export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult { + return async dispatch => { + const instance = await getDatasourceSrv().get(datasource); + dispatch(loadDatasource(exploreId, instance)); + }; +} + +/** + * Query change handler for the query row with the given index. + * If `override` is reset the query modifications and run the queries. Use this to set queries via a link. + */ +export function changeQuery( + exploreId: ExploreId, + query: DataQuery, + index: number, + override: boolean +): ThunkResult { + return dispatch => { + // Null query means reset + if (query === null) { + query = { ...generateEmptyQuery(index) }; + } + + dispatch({ type: ActionTypes.ChangeQuery, payload: { exploreId, query, index, override } }); + if (override) { + dispatch(runQueries(exploreId)); + } + }; +} + +/** + * Keep track of the Explore container size, in particular the width. + * The width will be used to calculate graph intervals (number of datapoints). + */ +export function changeSize( + exploreId: ExploreId, + { height, width }: { height: number; width: number } +): ChangeSizeAction { + return { type: ActionTypes.ChangeSize, payload: { exploreId, height, width } }; +} + +/** + * Change the time range of Explore. Usually called from the Timepicker or a graph interaction. + */ +export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult { + return dispatch => { + dispatch({ type: ActionTypes.ChangeTime, payload: { exploreId, range } }); + dispatch(runQueries(exploreId)); + }; +} + +/** + * Clear all queries and results. + */ +export function clearQueries(exploreId: ExploreId): ThunkResult { + return dispatch => { + dispatch(scanStop(exploreId)); + dispatch({ type: ActionTypes.ClearQueries, payload: { exploreId } }); + dispatch(stateSave()); + }; +} + +/** + * Highlight expressions in the log results + */ +export function highlightLogsExpression(exploreId: ExploreId, expressions: string[]): HighlightLogsExpressionAction { + return { type: ActionTypes.HighlightLogsExpression, payload: { exploreId, expressions } }; +} + +/** + * Initialize Explore state with state from the URL and the React component. + * Call this only on components for with the Explore state has not been initialized. + */ +export function initializeExplore( + exploreId: ExploreId, + datasource: string, + queries: DataQuery[], + range: RawTimeRange, + containerWidth: number, + eventBridge: Emitter +): ThunkResult { + return async dispatch => { + const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv() + .getExternal() + .map(ds => ({ + value: ds.name, + name: ds.name, + meta: ds.meta, + })); + + dispatch({ + type: ActionTypes.InitializeExplore, + payload: { + exploreId, + containerWidth, + datasource, + eventBridge, + exploreDatasources, + queries, + range, + }, + }); + + if (exploreDatasources.length > 1) { + let instance; + if (datasource) { + instance = await getDatasourceSrv().get(datasource); + } else { + instance = await getDatasourceSrv().get(); + } + dispatch(loadDatasource(exploreId, instance)); + } else { + dispatch(loadDatasourceMissing(exploreId)); + } + }; +} + +/** + * Initialize the wrapper split state + */ +export function initializeExploreSplit() { + return async dispatch => { + dispatch({ type: ActionTypes.InitializeExploreSplit }); + }; +} + +/** + * Display an error that happened during the selection of a datasource + */ +export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({ + type: ActionTypes.LoadDatasourceFailure, + payload: { + exploreId, + error, + }, +}); + +/** + * Display an error when no datasources have been configured + */ +export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissingAction => ({ + type: ActionTypes.LoadDatasourceMissing, + payload: { exploreId }, +}); + +/** + * Start the async process of loading a datasource to display a loading indicator + */ +export const loadDatasourcePending = (exploreId: ExploreId, datasourceId: number): LoadDatasourcePendingAction => ({ + type: ActionTypes.LoadDatasourcePending, + payload: { + exploreId, + datasourceId, + }, +}); + +/** + * Datasource loading was successfully completed. The instance is stored in the state as well in case we need to + * run datasource-specific code. Existing queries are imported to the new datasource if an importer exists, + * e.g., Prometheus -> Loki queries. + */ +export const loadDatasourceSuccess = ( + exploreId: ExploreId, + instance: any, + queries: DataQuery[] +): LoadDatasourceSuccessAction => { + // Capabilities + const supportsGraph = instance.meta.metrics; + const supportsLogs = instance.meta.logs; + const supportsTable = instance.meta.tables; + // Custom components + const StartPage = instance.pluginExports.ExploreStartPage; + + const historyKey = `grafana.explore.history.${instance.meta.id}`; + const history = store.getObject(historyKey, []); + // Save last-used datasource + store.set(LAST_USED_DATASOURCE_KEY, instance.name); + + return { + type: ActionTypes.LoadDatasourceSuccess, + payload: { + exploreId, + StartPage, + datasourceInstance: instance, + history, + initialDatasource: instance.name, + initialQueries: queries, + showingStartPage: Boolean(StartPage), + supportsGraph, + supportsLogs, + supportsTable, + }, + }; +}; + +/** + * Main action to asynchronously load a datasource. Dispatches lots of smaller actions for feedback. + */ +export function loadDatasource(exploreId: ExploreId, instance: any): ThunkResult { + return async (dispatch, getState) => { + const datasourceId = instance.meta.id; + + // Keep ID to track selection + dispatch(loadDatasourcePending(exploreId, datasourceId)); + + let datasourceError = null; + try { + const testResult = await instance.testDatasource(); + datasourceError = testResult.status === 'success' ? null : testResult.message; + } catch (error) { + datasourceError = (error && error.statusText) || 'Network error'; + } + if (datasourceError) { + dispatch(loadDatasourceFailure(exploreId, datasourceError)); + return; + } + + if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) { + // User already changed datasource again, discard results + return; + } + + if (instance.init) { + instance.init(); + } + + // Check if queries can be imported from previously selected datasource + const queries = getState().explore[exploreId].modifiedQueries; + let importedQueries = queries; + const origin = getState().explore[exploreId].datasourceInstance; + if (origin) { + if (origin.meta.id === instance.meta.id) { + // Keep same queries if same type of datasource + importedQueries = [...queries]; + } else if (instance.importQueries) { + // Datasource-specific importers + importedQueries = await instance.importQueries(queries, origin.meta); + } else { + // Default is blank queries + importedQueries = ensureQueries(); + } + } + + if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) { + // User already changed datasource again, discard results + return; + } + + // Reset edit state with new queries + const nextQueries = importedQueries.map((q, i) => ({ + ...importedQueries[i], + ...generateEmptyQuery(i), + })); + + dispatch(loadDatasourceSuccess(exploreId, instance, nextQueries)); + dispatch(runQueries(exploreId)); + }; +} + +/** + * Action to modify a query given a datasource-specific modifier action. + * @param exploreId Explore area + * @param modification Action object with a type, e.g., ADD_FILTER + * @param index Optional query row index. If omitted, the modification is applied to all query rows. + * @param modifier Function that executes the modification, typically `datasourceInstance.modifyQueries`. + */ +export function modifyQueries( + exploreId: ExploreId, + modification: any, + index: number, + modifier: any +): ThunkResult { + return dispatch => { + dispatch({ type: ActionTypes.ModifyQueries, payload: { exploreId, modification, index, modifier } }); + if (!modification.preventSubmit) { + dispatch(runQueries(exploreId)); + } + }; +} + +/** + * Mark a query transaction as failed with an error extracted from the query response. + * The transaction will be marked as `done`. + */ +export function queryTransactionFailure( + exploreId: ExploreId, + transactionId: string, + response: any, + datasourceId: string +): ThunkResult { + return (dispatch, getState) => { + const { datasourceInstance, queryTransactions } = getState().explore[exploreId]; + if (datasourceInstance.meta.id !== datasourceId || response.cancelled) { + // Navigated away, queries did not matter + return; + } + + // Transaction might have been discarded + if (!queryTransactions.find(qt => qt.id === transactionId)) { + return; + } + + console.error(response); + + let error: string; + let errorDetails: string; + if (response.data) { + if (typeof response.data === 'string') { + error = response.data; + } else if (response.data.error) { + error = response.data.error; + if (response.data.response) { + errorDetails = response.data.response; + } + } else { + throw new Error('Could not handle error response'); + } + } else if (response.message) { + error = response.message; + } else if (typeof response === 'string') { + error = response; + } else { + error = 'Unknown error during query transaction. Please check JS console logs.'; + } + + // Mark transactions as complete + const nextQueryTransactions = queryTransactions.map(qt => { + if (qt.id === transactionId) { + return { + ...qt, + error, + errorDetails, + done: true, + }; + } + return qt; + }); + + dispatch({ + type: ActionTypes.QueryTransactionFailure, + payload: { exploreId, queryTransactions: nextQueryTransactions }, + }); + }; +} + +/** + * Start a query transaction for the given result type. + * @param exploreId Explore area + * @param transaction Query options and `done` status. + * @param resultType Associate the transaction with a result viewer, e.g., Graph + * @param rowIndex Index is used to associate latency for this transaction with a query row + */ +export function queryTransactionStart( + exploreId: ExploreId, + transaction: QueryTransaction, + resultType: ResultType, + rowIndex: number +): QueryTransactionStartAction { + return { type: ActionTypes.QueryTransactionStart, payload: { exploreId, resultType, rowIndex, transaction } }; +} + +/** + * Complete a query transaction, mark the transaction as `done` and store query state in URL. + * If the transaction was started by a scanner, it keeps on scanning for more results. + * Side-effect: the query is stored in localStorage. + * @param exploreId Explore area + * @param transactionId ID + * @param result Response from `datasourceInstance.query()` + * @param latency Duration between request and response + * @param queries Queries from all query rows + * @param datasourceId Origin datasource instance, used to discard results if current datasource is different + */ +export function queryTransactionSuccess( + exploreId: ExploreId, + transactionId: string, + result: any, + latency: number, + queries: DataQuery[], + datasourceId: string +): ThunkResult { + return (dispatch, getState) => { + const { datasourceInstance, history, queryTransactions, scanner, scanning } = getState().explore[exploreId]; + + // If datasource already changed, results do not matter + if (datasourceInstance.meta.id !== datasourceId) { + return; + } + + // Transaction might have been discarded + const transaction = queryTransactions.find(qt => qt.id === transactionId); + if (!transaction) { + return; + } + + // Get query hints + let hints: QueryHint[]; + if (datasourceInstance.getQueryHints as QueryHintGetter) { + hints = datasourceInstance.getQueryHints(transaction.query, result); + } + + // Mark transactions as complete and attach result + const nextQueryTransactions = queryTransactions.map(qt => { + if (qt.id === transactionId) { + return { + ...qt, + hints, + latency, + result, + done: true, + }; + } + return qt; + }); + + // Side-effect: Saving history in localstorage + const nextHistory = updateHistory(history, datasourceId, queries); + + dispatch({ + type: ActionTypes.QueryTransactionSuccess, + payload: { + exploreId, + history: nextHistory, + queryTransactions: nextQueryTransactions, + }, + }); + + // Keep scanning for results if this was the last scanning transaction + if (scanning) { + if (_.size(result) === 0) { + const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done); + if (!other) { + const range = scanner(); + dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } }); + } + } else { + // We can stop scanning if we have a result + dispatch(scanStop(exploreId)); + } + } + }; +} + +/** + * Remove query row of the given index, as well as associated query results. + */ +export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult { + return dispatch => { + dispatch({ type: ActionTypes.RemoveQueryRow, payload: { exploreId, index } }); + dispatch(runQueries(exploreId)); + }; +} + +/** + * Main action to run queries and dispatches sub-actions based on which result viewers are active + */ +export function runQueries(exploreId: ExploreId) { + return (dispatch, getState) => { + const { + datasourceInstance, + modifiedQueries, + showingLogs, + showingGraph, + showingTable, + supportsGraph, + supportsLogs, + supportsTable, + } = getState().explore[exploreId]; + + if (!hasNonEmptyQuery(modifiedQueries)) { + dispatch({ type: ActionTypes.RunQueriesEmpty, payload: { exploreId } }); + return; + } + + // Some datasource's query builders allow per-query interval limits, + // but we're using the datasource interval limit for now + const interval = datasourceInstance.interval; + + // Keep table queries first since they need to return quickly + if (showingTable && supportsTable) { + dispatch( + runQueriesForType( + exploreId, + 'Table', + { + interval, + format: 'table', + instant: true, + valueWithRefId: true, + }, + data => data[0] + ) + ); + } + if (showingGraph && supportsGraph) { + dispatch( + runQueriesForType( + exploreId, + 'Graph', + { + interval, + format: 'time_series', + instant: false, + }, + makeTimeSeriesList + ) + ); + } + if (showingLogs && supportsLogs) { + dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' })); + } + dispatch(stateSave()); + }; +} + +/** + * Helper action to build a query transaction object and handing the query to the datasource. + * @param exploreId Explore area + * @param resultType Result viewer that will be associated with this query result + * @param queryOptions Query options as required by the datasource's `query()` function. + * @param resultGetter Optional result extractor, e.g., if the result is a list and you only need the first element. + */ +function runQueriesForType( + exploreId: ExploreId, + resultType: ResultType, + queryOptions: QueryOptions, + resultGetter?: any +) { + return async (dispatch, getState) => { + const { + datasourceInstance, + eventBridge, + modifiedQueries: queries, + queryIntervals, + range, + scanning, + } = getState().explore[exploreId]; + const datasourceId = datasourceInstance.meta.id; + + // Run all queries concurrently + queries.forEach(async (query, rowIndex) => { + const transaction = buildQueryTransaction( + query, + rowIndex, + resultType, + queryOptions, + range, + queryIntervals, + scanning + ); + dispatch(queryTransactionStart(exploreId, transaction, resultType, rowIndex)); + try { + const now = Date.now(); + const res = await datasourceInstance.query(transaction.options); + eventBridge.emit('data-received', res.data || []); + const latency = Date.now() - now; + const results = resultGetter ? resultGetter(res.data) : res.data; + dispatch(queryTransactionSuccess(exploreId, transaction.id, results, latency, queries, datasourceId)); + } catch (response) { + eventBridge.emit('data-error', response); + dispatch(queryTransactionFailure(exploreId, transaction.id, response, datasourceId)); + } + }); + }; +} + +/** + * Start a scan for more results using the given scanner. + * @param exploreId Explore area + * @param scanner Function that a) returns a new time range and b) triggers a query run for the new range + */ +export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult { + return dispatch => { + // Register the scanner + dispatch({ type: ActionTypes.ScanStart, payload: { exploreId, scanner } }); + // Scanning must trigger query run, and return the new range + const range = scanner(); + // Set the new range to be displayed + dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } }); + }; +} + +/** + * Stop any scanning for more results. + */ +export function scanStop(exploreId: ExploreId): ScanStopAction { + return { type: ActionTypes.ScanStop, payload: { exploreId } }; +} + +/** + * Reset queries to the given queries. Any modifications will be discarded. + * Use this action for clicks on query examples. Triggers a query run. + */ +export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult { + return dispatch => { + // Inject react keys into query objects + const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery() })); + dispatch({ + type: ActionTypes.SetQueries, + payload: { + exploreId, + queries, + }, + }); + dispatch(runQueries(exploreId)); + }; +} + +/** + * Close the split view and save URL state. + */ +export function splitClose(): ThunkResult { + return dispatch => { + dispatch({ type: ActionTypes.SplitClose }); + dispatch(stateSave()); + }; +} + +/** + * Open the split view and copy the left state to be the right state. + * The right state is automatically initialized. + * The copy keeps all query modifications but wipes the query results. + */ +export function splitOpen(): ThunkResult { + return (dispatch, getState) => { + // Clone left state to become the right state + const leftState = getState().explore.left; + const itemState = { + ...leftState, + queryTransactions: [], + initialQueries: leftState.modifiedQueries.slice(), + }; + dispatch({ type: ActionTypes.SplitOpen, payload: { itemState } }); + dispatch(stateSave()); + }; +} + +/** + * Saves Explore state to URL using the `left` and `right` parameters. + * If split view is not active, `right` will not be set. + */ +export function stateSave() { + return (dispatch, getState) => { + const { left, right, split } = getState().explore; + const urlStates: { [index: string]: string } = {}; + const leftUrlState: ExploreUrlState = { + datasource: left.datasourceInstance.name, + queries: left.modifiedQueries.map(clearQueryKeys), + range: left.range, + }; + urlStates.left = serializeStateToUrlParam(leftUrlState, true); + if (split) { + const rightUrlState: ExploreUrlState = { + datasource: right.datasourceInstance.name, + queries: right.modifiedQueries.map(clearQueryKeys), + range: right.range, + }; + urlStates.right = serializeStateToUrlParam(rightUrlState, true); + } + dispatch(updateLocation({ query: urlStates })); + }; +} + +/** + * Expand/collapse the graph result viewer. When collapsed, graph queries won't be run. + */ +export function toggleGraph(exploreId: ExploreId): ThunkResult { + return (dispatch, getState) => { + dispatch({ type: ActionTypes.ToggleGraph, payload: { exploreId } }); + if (getState().explore[exploreId].showingGraph) { + dispatch(runQueries(exploreId)); + } + }; +} + +/** + * Expand/collapse the logs result viewer. When collapsed, log queries won't be run. + */ +export function toggleLogs(exploreId: ExploreId): ThunkResult { + return (dispatch, getState) => { + dispatch({ type: ActionTypes.ToggleLogs, payload: { exploreId } }); + if (getState().explore[exploreId].showingLogs) { + dispatch(runQueries(exploreId)); + } + }; +} + +/** + * Expand/collapse the table result viewer. When collapsed, table queries won't be run. + */ +export function toggleTable(exploreId: ExploreId): ThunkResult { + return (dispatch, getState) => { + dispatch({ type: ActionTypes.ToggleTable, payload: { exploreId } }); + if (getState().explore[exploreId].showingTable) { + dispatch(runQueries(exploreId)); + } + }; +} diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts new file mode 100644 index 00000000000..b112a5370e3 --- /dev/null +++ b/public/app/features/explore/state/reducers.ts @@ -0,0 +1,462 @@ +import { + calculateResultsFromQueryTransactions, + generateEmptyQuery, + getIntervals, + ensureQueries, +} from 'app/core/utils/explore'; +import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore'; +import { DataQuery } from 'app/types/series'; + +import { Action, ActionTypes } from './actionTypes'; + +export const DEFAULT_RANGE = { + from: 'now-6h', + to: 'now', +}; + +// Millies step for helper bar charts +const DEFAULT_GRAPH_INTERVAL = 15 * 1000; + +/** + * Returns a fresh Explore area state + */ +const makeExploreItemState = (): ExploreItemState => ({ + StartPage: undefined, + containerWidth: 0, + datasourceInstance: null, + datasourceError: null, + datasourceLoading: null, + datasourceMissing: false, + exploreDatasources: [], + history: [], + initialQueries: [], + initialized: false, + modifiedQueries: [], + queryTransactions: [], + queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL }, + range: DEFAULT_RANGE, + scanning: false, + scanRange: null, + showingGraph: true, + showingLogs: true, + showingTable: true, + supportsGraph: null, + supportsLogs: null, + supportsTable: null, +}); + +/** + * Global Explore state that handles multiple Explore areas and the split state + */ +const initialExploreState: ExploreState = { + split: null, + left: makeExploreItemState(), + right: makeExploreItemState(), +}; + +/** + * Reducer for an Explore area, to be used by the global Explore reducer. + */ +const itemReducer = (state, action: Action): ExploreItemState => { + switch (action.type) { + case ActionTypes.AddQueryRow: { + const { initialQueries, modifiedQueries, queryTransactions } = state; + const { index, query } = action.payload; + + // Add new query row after given index, keep modifications of existing rows + const nextModifiedQueries = [ + ...modifiedQueries.slice(0, index + 1), + { ...query }, + ...initialQueries.slice(index + 1), + ]; + + // Add to initialQueries, which will cause a new row to be rendered + const nextQueries = [...initialQueries.slice(0, index + 1), { ...query }, ...initialQueries.slice(index + 1)]; + + // Ongoing transactions need to update their row indices + const nextQueryTransactions = queryTransactions.map(qt => { + if (qt.rowIndex > index) { + return { + ...qt, + rowIndex: qt.rowIndex + 1, + }; + } + return qt; + }); + + return { + ...state, + initialQueries: nextQueries, + logsHighlighterExpressions: undefined, + modifiedQueries: nextModifiedQueries, + queryTransactions: nextQueryTransactions, + }; + } + + case ActionTypes.ChangeQuery: { + const { initialQueries, queryTransactions } = state; + let { modifiedQueries } = state; + const { query, index, override } = action.payload; + + // Fast path: only change modifiedQueries to not trigger an update + modifiedQueries[index] = query; + if (!override) { + return { + ...state, + modifiedQueries, + }; + } + + // Override path: queries are completely reset + const nextQuery: DataQuery = { + ...query, + ...generateEmptyQuery(index), + }; + const nextQueries = [...initialQueries]; + nextQueries[index] = nextQuery; + modifiedQueries = [...nextQueries]; + + // Discard ongoing transaction related to row query + const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); + + return { + ...state, + initialQueries: nextQueries, + modifiedQueries: nextQueries.slice(), + queryTransactions: nextQueryTransactions, + }; + } + + case ActionTypes.ChangeSize: { + const { range, datasourceInstance } = state; + let interval = '1s'; + if (datasourceInstance && datasourceInstance.interval) { + interval = datasourceInstance.interval; + } + const containerWidth = action.payload.width; + const queryIntervals = getIntervals(range, interval, containerWidth); + return { ...state, containerWidth, queryIntervals }; + } + + case ActionTypes.ChangeTime: { + return { + ...state, + range: action.payload.range, + }; + } + + case ActionTypes.ClearQueries: { + const queries = ensureQueries(); + return { + ...state, + initialQueries: queries.slice(), + modifiedQueries: queries.slice(), + queryTransactions: [], + showingStartPage: Boolean(state.StartPage), + }; + } + + case ActionTypes.HighlightLogsExpression: { + const { expressions } = action.payload; + return { ...state, logsHighlighterExpressions: expressions }; + } + + case ActionTypes.InitializeExplore: { + const { containerWidth, datasource, eventBridge, exploreDatasources, queries, range } = action.payload; + return { + ...state, + containerWidth, + eventBridge, + exploreDatasources, + range, + initialDatasource: datasource, + initialQueries: queries, + initialized: true, + modifiedQueries: queries.slice(), + }; + } + + case ActionTypes.LoadDatasourceFailure: { + return { ...state, datasourceError: action.payload.error, datasourceLoading: false }; + } + + case ActionTypes.LoadDatasourceMissing: { + return { ...state, datasourceMissing: true, datasourceLoading: false }; + } + + case ActionTypes.LoadDatasourcePending: { + return { ...state, datasourceLoading: true, requestedDatasourceId: action.payload.datasourceId }; + } + + case ActionTypes.LoadDatasourceSuccess: { + const { containerWidth, range } = state; + const { + StartPage, + datasourceInstance, + history, + initialDatasource, + initialQueries, + showingStartPage, + supportsGraph, + supportsLogs, + supportsTable, + } = action.payload; + const queryIntervals = getIntervals(range, datasourceInstance.interval, containerWidth); + + return { + ...state, + queryIntervals, + StartPage, + datasourceInstance, + history, + initialDatasource, + initialQueries, + showingStartPage, + supportsGraph, + supportsLogs, + supportsTable, + datasourceLoading: false, + datasourceMissing: false, + logsHighlighterExpressions: undefined, + modifiedQueries: initialQueries.slice(), + queryTransactions: [], + }; + } + + case ActionTypes.ModifyQueries: { + const { initialQueries, modifiedQueries, queryTransactions } = state; + const { modification, index, modifier } = action.payload as any; + let nextQueries: DataQuery[]; + let nextQueryTransactions; + if (index === undefined) { + // Modify all queries + nextQueries = initialQueries.map((query, i) => ({ + ...modifier(modifiedQueries[i], modification), + ...generateEmptyQuery(i), + })); + // Discard all ongoing transactions + nextQueryTransactions = []; + } else { + // Modify query only at index + nextQueries = initialQueries.map((query, i) => { + // Synchronize all queries with local query cache to ensure consistency + // TODO still needed? + return i === index + ? { + ...modifier(modifiedQueries[i], modification), + ...generateEmptyQuery(i), + } + : query; + }); + nextQueryTransactions = queryTransactions + // Consume the hint corresponding to the action + .map(qt => { + if (qt.hints != null && qt.rowIndex === index) { + qt.hints = qt.hints.filter(hint => hint.fix.action !== modification); + } + return qt; + }) + // Preserve previous row query transaction to keep results visible if next query is incomplete + .filter(qt => modification.preventSubmit || qt.rowIndex !== index); + } + return { + ...state, + initialQueries: nextQueries, + modifiedQueries: nextQueries.slice(), + queryTransactions: nextQueryTransactions, + }; + } + + case ActionTypes.QueryTransactionFailure: { + const { queryTransactions } = action.payload; + return { + ...state, + queryTransactions, + showingStartPage: false, + }; + } + + case ActionTypes.QueryTransactionStart: { + const { datasourceInstance, queryIntervals, queryTransactions } = state; + const { resultType, rowIndex, transaction } = action.payload; + // Discarding existing transactions of same type + const remainingTransactions = queryTransactions.filter( + qt => !(qt.resultType === resultType && qt.rowIndex === rowIndex) + ); + + // Append new transaction + const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction]; + + const results = calculateResultsFromQueryTransactions( + nextQueryTransactions, + datasourceInstance, + queryIntervals.intervalMs + ); + + return { + ...state, + ...results, + queryTransactions: nextQueryTransactions, + showingStartPage: false, + }; + } + + case ActionTypes.QueryTransactionSuccess: { + const { datasourceInstance, queryIntervals } = state; + const { history, queryTransactions } = action.payload; + const results = calculateResultsFromQueryTransactions( + queryTransactions, + datasourceInstance, + queryIntervals.intervalMs + ); + + return { + ...state, + ...results, + history, + queryTransactions, + showingStartPage: false, + }; + } + + case ActionTypes.RemoveQueryRow: { + const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state; + let { modifiedQueries } = state; + const { index } = action.payload; + + modifiedQueries = [...modifiedQueries.slice(0, index), ...modifiedQueries.slice(index + 1)]; + + if (initialQueries.length <= 1) { + return state; + } + + const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)]; + + // Discard transactions related to row query + const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); + const results = calculateResultsFromQueryTransactions( + nextQueryTransactions, + datasourceInstance, + queryIntervals.intervalMs + ); + + return { + ...state, + ...results, + initialQueries: nextQueries, + logsHighlighterExpressions: undefined, + modifiedQueries: nextQueries.slice(), + queryTransactions: nextQueryTransactions, + }; + } + + case ActionTypes.RunQueriesEmpty: { + return { ...state, queryTransactions: [] }; + } + + case ActionTypes.ScanRange: { + return { ...state, scanRange: action.payload.range }; + } + + case ActionTypes.ScanStart: { + return { ...state, scanning: true }; + } + + case ActionTypes.ScanStop: { + const { queryTransactions } = state; + const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done); + return { ...state, queryTransactions: nextQueryTransactions, scanning: false, scanRange: undefined }; + } + + case ActionTypes.SetQueries: { + const { queries } = action.payload; + return { ...state, initialQueries: queries.slice(), modifiedQueries: queries.slice() }; + } + + case ActionTypes.ToggleGraph: { + const showingGraph = !state.showingGraph; + let nextQueryTransactions = state.queryTransactions; + if (!showingGraph) { + // Discard transactions related to Graph query + nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph'); + } + return { ...state, queryTransactions: nextQueryTransactions, showingGraph }; + } + + case ActionTypes.ToggleLogs: { + const showingLogs = !state.showingLogs; + let nextQueryTransactions = state.queryTransactions; + if (!showingLogs) { + // Discard transactions related to Logs query + nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs'); + } + return { ...state, queryTransactions: nextQueryTransactions, showingLogs }; + } + + case ActionTypes.ToggleTable: { + const showingTable = !state.showingTable; + if (showingTable) { + return { ...state, showingTable, queryTransactions: state.queryTransactions }; + } + + // Toggle off needs discarding of table queries and results + const nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Table'); + const results = calculateResultsFromQueryTransactions( + nextQueryTransactions, + state.datasourceInstance, + state.queryIntervals.intervalMs + ); + + return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable }; + } + } + + return state; +}; + +/** + * Global Explore reducer that handles multiple Explore areas (left and right). + * Actions that have an `exploreId` get routed to the ExploreItemReducer. + */ +export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => { + switch (action.type) { + case ActionTypes.SplitClose: { + return { + ...state, + split: false, + }; + } + + case ActionTypes.SplitOpen: { + return { + ...state, + split: true, + right: action.payload.itemState, + }; + } + + case ActionTypes.InitializeExploreSplit: { + return { + ...state, + split: true, + }; + } + } + + if (action.payload) { + const { exploreId } = action.payload as any; + if (exploreId !== undefined) { + const exploreItemState = state[exploreId]; + return { + ...state, + [exploreId]: itemReducer(exploreItemState, action), + }; + } + } + + return state; +}; + +export default { + explore: exploreReducer, +}; diff --git a/public/app/features/folders/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx index 176e270038b..62a479b9124 100644 --- a/public/app/features/folders/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import { Tooltip } from '@grafana/ui'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { getNavModel } from 'app/core/selectors/navModel'; import { NavModel, StoreState, FolderState } from 'app/types'; @@ -84,8 +84,10 @@ export class FolderPermissions extends PureComponent {

Folder Permissions

- - + +
+ +