Browse Source

Merge pull request #6 from grafana/master

Updating master
Pavel 7 years ago
parent
commit
8011a6f45b
100 changed files with 8798 additions and 364 deletions
  1. 49 29
      .circleci/config.yml
  2. 20 1
      CHANGELOG.md
  3. 3 1
      README.md
  4. 2 0
      build.go
  5. 1 1
      devenv/dashboards.yaml
  6. 1674 0
      devenv/dev-dashboards-without-uid/panel_tests_graph.json
  7. 510 0
      devenv/dev-dashboards-without-uid/panel_tests_graph_time_regions.json
  8. 3342 0
      devenv/dev-dashboards-without-uid/panel_tests_polystat.json
  9. 545 7
      devenv/dev-dashboards/datasource_tests_elasticsearch_compare.json
  10. 1 0
      devenv/docker/blocks/influxdb/influxdb.conf
  11. 6 1
      docs/sources/features/explore/index.md
  12. 3 3
      docs/sources/http_api/admin.md
  13. 1 1
      docs/sources/http_api/folder_permissions.md
  14. 8 11
      docs/sources/installation/debian.md
  15. 15 7
      docs/sources/installation/rpm.md
  16. 1 1
      docs/sources/reference/dashboard.md
  17. 4 2
      docs/sources/reference/templating.md
  18. 2 2
      latest.json
  19. 2 3
      package.json
  20. 14 3
      packages/grafana-ui/package.json
  21. 1 1
      packages/grafana-ui/src/components/ColorPicker/ColorPalette.test.tsx
  22. 3 3
      packages/grafana-ui/src/components/ColorPicker/ColorPalette.tsx
  23. 4 9
      packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx
  24. 12 21
      packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx
  25. 2 1
      packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.tsx
  26. 6 10
      packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx
  27. 5 5
      packages/grafana-ui/src/components/ColorPicker/SpectrumPicker.tsx
  28. 0 0
      packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss
  29. 0 0
      packages/grafana-ui/src/components/ColorPicker/__snapshots__/ColorPalette.test.tsx.snap
  30. 0 0
      packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.test.tsx
  31. 8 5
      packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx
  32. 40 0
      packages/grafana-ui/src/components/CustomScrollbar/_CustomScrollbar.scss
  33. 4 8
      packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap
  34. 0 0
      packages/grafana-ui/src/components/GfFormLabel/GfFormLabel.tsx
  35. 1 0
      packages/grafana-ui/src/components/Graph/Graph.tsx
  36. 5 3
      packages/grafana-ui/src/components/Label/Label.tsx
  37. 11 0
      packages/grafana-ui/src/components/LoadingPlaceholder/LoadingPlaceholder.tsx
  38. 15 0
      packages/grafana-ui/src/components/PanelOptionsGrid/PanelOptionsGrid.tsx
  39. 10 0
      packages/grafana-ui/src/components/PanelOptionsGrid/_PanelOptionsGrid.scss
  40. 4 4
      packages/grafana-ui/src/components/PanelOptionsGroup/PanelOptionsGroup.tsx
  41. 27 0
      packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss
  42. 3 6
      packages/grafana-ui/src/components/Portal/Portal.tsx
  43. 4 1
      packages/grafana-ui/src/components/Select/IndicatorsContainer.tsx
  44. 4 0
      packages/grafana-ui/src/components/Select/NoOptionsMessage.tsx
  45. 15 10
      packages/grafana-ui/src/components/Select/Select.tsx
  46. 23 12
      packages/grafana-ui/src/components/Select/SelectOption.test.tsx
  47. 6 3
      packages/grafana-ui/src/components/Select/SelectOption.tsx
  48. 11 5
      packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx
  49. 1 0
      packages/grafana-ui/src/components/Select/_Select.scss
  50. 7 2
      packages/grafana-ui/src/components/Select/__snapshots__/SelectOption.test.tsx.snap
  51. 27 0
      packages/grafana-ui/src/components/Select/resetSelectStyles.ts
  52. 173 0
      packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx
  53. 206 0
      packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx
  54. 105 0
      packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss
  55. 21 16
      packages/grafana-ui/src/components/Tooltip/Popper.tsx
  56. 99 0
      packages/grafana-ui/src/components/Tooltip/PopperController.tsx
  57. 5 3
      packages/grafana-ui/src/components/Tooltip/Tooltip.test.tsx
  58. 32 0
      packages/grafana-ui/src/components/Tooltip/Tooltip.tsx
  59. 27 9
      packages/grafana-ui/src/components/Tooltip/_Tooltip.scss
  60. 12 0
      packages/grafana-ui/src/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap
  61. 21 22
      packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx
  62. 15 16
      packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx
  63. 105 0
      packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.tsx
  64. 0 0
      packages/grafana-ui/src/components/ValueMappingsEditor/_ValueMappingsEditor.scss
  65. 9 14
      packages/grafana-ui/src/components/ValueMappingsEditor/__snapshots__/ValueMappingsEditor.test.tsx.snap
  66. 8 0
      packages/grafana-ui/src/components/index.scss
  67. 21 0
      packages/grafana-ui/src/components/index.ts
  68. 0 1
      packages/grafana-ui/src/forms/index.ts
  69. 2 0
      packages/grafana-ui/src/index.scss
  70. 0 2
      packages/grafana-ui/src/index.ts
  71. 37 0
      packages/grafana-ui/src/types/panel.ts
  72. 93 0
      packages/grafana-ui/src/utils/colors.ts
  73. 2 0
      packages/grafana-ui/src/utils/index.ts
  74. 40 0
      packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.test.ts
  75. 42 0
      packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.ts
  76. 322 0
      packages/grafana-ui/src/utils/valueFormats/categories.ts
  77. 231 0
      packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.test.ts
  78. 312 0
      packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts
  79. 7 0
      packages/grafana-ui/src/utils/valueFormats/symbolFormatters.test.ts
  80. 30 0
      packages/grafana-ui/src/utils/valueFormats/symbolFormatters.ts
  81. 166 0
      packages/grafana-ui/src/utils/valueFormats/valueFormats.ts
  82. 0 0
      packages/grafana-ui/src/vendor/spectrum.css
  83. 0 0
      packages/grafana-ui/src/vendor/spectrum.js
  84. 0 1
      packages/grafana-ui/src/visualizations/index.ts
  85. 4 2
      packaging/docker/Dockerfile
  86. 0 1
      packaging/docker/build-deploy.sh
  87. 32 8
      packaging/docker/build.sh
  88. 29 7
      packaging/docker/push_to_docker_hub.sh
  89. 4 0
      pkg/api/alerting.go
  90. 6 0
      pkg/api/alerting_test.go
  91. 20 29
      pkg/components/dashdiffs/formatter_json.go
  92. 5 2
      pkg/components/imguploader/imguploader.go
  93. 2 20
      pkg/log/log.go
  94. 5 1
      pkg/login/ext_user.go
  95. 1 1
      pkg/models/dashboards.go
  96. 1 1
      pkg/services/alerting/extractor.go
  97. 1 1
      pkg/services/alerting/notifiers/telegram.go
  98. 18 3
      pkg/services/alerting/rule.go
  99. 60 20
      pkg/services/alerting/rule_test.go
  100. 5 2
      pkg/services/alerting/test_notification.go

+ 49 - 29
.circleci/config.yml

@@ -127,7 +127,7 @@ jobs:
 
 
   build-all:
   build-all:
     docker:
     docker:
-     - image: grafana/build-container:1.2.1
+     - image: grafana/build-container:1.2.2
     working_directory: /go/src/github.com/grafana/grafana
     working_directory: /go/src/github.com/grafana/grafana
     steps:
     steps:
       - checkout
       - checkout
@@ -200,51 +200,51 @@ jobs:
             - dist/grafana*
             - dist/grafana*
 
 
   grafana-docker-master:
   grafana-docker-master:
-    docker:
-      - image: docker:stable-git
+    machine:
+      image: circleci/classic:201808-01
     steps:
     steps:
       - checkout
       - checkout
       - attach_workspace:
       - attach_workspace:
           at: .
           at: .
-      - setup_remote_docker
       - run: docker info
       - 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: 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: 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"
       - run: cd packaging/docker && ./build-enterprise.sh "master"
 
 
 
 
   grafana-docker-pr:
   grafana-docker-pr:
-    docker:
-      - image: docker:stable-git
+    machine:
+      image: circleci/classic:201808-01
     steps:
     steps:
       - checkout
       - checkout
       - attach_workspace:
       - attach_workspace:
           at: .
           at: .
-      - setup_remote_docker
       - run: docker info
       - 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}"
       - run: cd packaging/docker && ./build.sh "${CIRCLE_SHA1}"
 
 
   grafana-docker-release:
   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:
   build-enterprise:
     docker:
     docker:
-     - image: grafana/build-container:1.2.1
+     - image: grafana/build-container:1.2.2
     working_directory: /go/src/github.com/grafana/grafana
     working_directory: /go/src/github.com/grafana/grafana
     steps:
     steps:
       - checkout
       - checkout
@@ -276,7 +276,7 @@ jobs:
 
 
   build-all-enterprise:
   build-all-enterprise:
     docker:
     docker:
-    - image: grafana/build-container:1.2.1
+    - image: grafana/build-container:1.2.2
     working_directory: /go/src/github.com/grafana/grafana
     working_directory: /go/src/github.com/grafana/grafana
     steps:
     steps:
     - checkout
     - checkout
@@ -323,7 +323,7 @@ jobs:
 
 
   deploy-enterprise-master:
   deploy-enterprise-master:
     docker:
     docker:
-      - image: grafana/grafana-ci-deploy:1.0.0
+      - image: grafana/grafana-ci-deploy:1.1.0
     steps:
     steps:
       - attach_workspace:
       - attach_workspace:
           at: .
           at: .
@@ -346,7 +346,7 @@ jobs:
 
 
   deploy-enterprise-release:
   deploy-enterprise-release:
     docker:
     docker:
-    - image: grafana/grafana-ci-deploy:1.0.0
+    - image: grafana/grafana-ci-deploy:1.1.0
     steps:
     steps:
       - attach_workspace:
       - attach_workspace:
          at: .
          at: .
@@ -365,10 +365,20 @@ jobs:
       - run:
       - run:
           name: Deploy to Grafana.com
           name: Deploy to Grafana.com
           command: './scripts/build/publish.sh --enterprise'
           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:
   deploy-master:
     docker:
     docker:
-      - image: grafana/grafana-ci-deploy:1.0.0
+      - image: grafana/grafana-ci-deploy:1.1.0
     steps:
     steps:
       - attach_workspace:
       - attach_workspace:
           at: .
           at: .
@@ -398,8 +408,9 @@ jobs:
 
 
   deploy-release:
   deploy-release:
     docker:
     docker:
-      - image: grafana/grafana-ci-deploy:1.0.0
+      - image: grafana/grafana-ci-deploy:1.1.0
     steps:
     steps:
+      - checkout
       - attach_workspace:
       - attach_workspace:
           at: .
           at: .
       - run:
       - run:
@@ -417,6 +428,15 @@ jobs:
       - run:
       - run:
           name: Deploy to Grafana.com
           name: Deploy to Grafana.com
           command: './scripts/build/publish.sh'
           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:
 workflows:
   version: 2
   version: 2

+ 20 - 1
CHANGELOG.md

@@ -2,6 +2,7 @@
 
 
 ### New Features
 ### New Features
 * **Alerting**: Adds support for Google Hangouts Chat notifications [#11221](https://github.com/grafana/grafana/issues/11221), thx [@PatrickSchuster](https://github.com/PatrickSchuster)
 * **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)
 * **Snapshots**: Enable deletion of public snapshot [#14109](https://github.com/grafana/grafana/issues/14109)
 
 
 ### Minor
 ### 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)
 * **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)
 * **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)
 * **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)
 * **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)
 * **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)
 * **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)
 * **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)
 * **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
 ### Bug fixes
 * **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)
 * **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)
 # 5.4.2 (2018-12-13)
 
 

+ 3 - 1
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.
 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
 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
 ## Plugin development
 
 

+ 2 - 0
build.go

@@ -164,6 +164,8 @@ func makeLatestDistCopies() {
 		"_amd64.deb":          "dist/grafana_latest_amd64.deb",
 		"_amd64.deb":          "dist/grafana_latest_amd64.deb",
 		".x86_64.rpm":         "dist/grafana-latest-1.x86_64.rpm",
 		".x86_64.rpm":         "dist/grafana-latest-1.x86_64.rpm",
 		".linux-amd64.tar.gz": "dist/grafana-latest.linux-x64.tar.gz",
 		".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 {
 	for _, file := range files {

+ 1 - 1
devenv/dashboards.yaml

@@ -4,6 +4,6 @@ providers:
  - name: 'gdev dashboards'
  - name: 'gdev dashboards'
    folder: 'gdev dashboards'
    folder: 'gdev dashboards'
    type: file
    type: file
+   updateIntervalSeconds: 15
    options:
    options:
      path: devenv/dev-dashboards
      path: devenv/dev-dashboards
-

+ 1674 - 0
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
+}

+ 510 - 0
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
+}

+ 3342 - 0
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
+}

+ 545 - 7
devenv/dev-dashboards/datasource_tests_elasticsearch_compare.json

@@ -17,7 +17,7 @@
   "editable": true,
   "editable": true,
   "gnetId": null,
   "gnetId": null,
   "graphTooltip": 0,
   "graphTooltip": 0,
-  "iteration": 1542304484522,
+  "iteration": 1545263815779,
   "links": [
   "links": [
     {
     {
       "icon": "external link",
       "icon": "external link",
@@ -66,6 +66,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -168,6 +169,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -270,6 +272,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -372,6 +375,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -474,6 +478,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -576,6 +581,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -2249,6 +2255,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -2366,6 +2373,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -2483,6 +2491,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -2600,6 +2609,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -2717,6 +2727,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -2834,6 +2845,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -2951,6 +2963,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -3068,6 +3081,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -3185,6 +3199,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -3302,6 +3317,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -3419,6 +3435,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -3536,6 +3553,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -3667,6 +3685,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -3780,6 +3799,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -3893,6 +3913,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -4006,6 +4027,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -4119,6 +4141,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -4232,6 +4255,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -4345,6 +4369,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -4458,6 +4483,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -4571,6 +4597,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -4684,6 +4711,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -4797,6 +4825,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -4910,6 +4939,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -5008,6 +5038,512 @@
         "x": 0,
         "x": 0,
         "y": 4
         "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,
       "id": 54,
       "panels": [
       "panels": [
         {
         {
@@ -5042,6 +5578,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -5193,6 +5730,7 @@
           "linewidth": 1,
           "linewidth": 1,
           "links": [],
           "links": [],
           "nullPointMode": "null",
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "percentage": false,
           "pointradius": 5,
           "pointradius": 5,
           "points": false,
           "points": false,
@@ -5328,8 +5866,8 @@
     "list": [
     "list": [
       {
       {
         "current": {
         "current": {
-          "text": "gdev-elasticsearch-v2-metrics",
-          "value": "gdev-elasticsearch-v2-metrics"
+          "text": "gdev-elasticsearch-v5-metrics",
+          "value": "gdev-elasticsearch-v5-metrics"
         },
         },
         "hide": 0,
         "hide": 0,
         "label": "Version One",
         "label": "Version One",
@@ -5343,8 +5881,8 @@
       },
       },
       {
       {
         "current": {
         "current": {
-          "text": "gdev-elasticsearch-v5-metrics",
-          "value": "gdev-elasticsearch-v5-metrics"
+          "text": "gdev-elasticsearch-v6-metrics",
+          "value": "gdev-elasticsearch-v6-metrics"
         },
         },
         "hide": 0,
         "hide": 0,
         "label": "Version Two",
         "label": "Version Two",
@@ -5359,7 +5897,7 @@
     ]
     ]
   },
   },
   "time": {
   "time": {
-    "from": "now-3h",
+    "from": "now-1h",
     "to": "now"
     "to": "now"
   },
   },
   "timepicker": {
   "timepicker": {
@@ -5390,5 +5928,5 @@
   "timezone": "",
   "timezone": "",
   "title": "Datasource tests - Elasticsearch comparison",
   "title": "Datasource tests - Elasticsearch comparison",
   "uid": "fuFWehBmk",
   "uid": "fuFWehBmk",
-  "version": 10
+  "version": 4
 }
 }

+ 1 - 0
devenv/docker/blocks/influxdb/influxdb.conf

@@ -69,6 +69,7 @@ reporting-disabled = false
 
 
   unix-socket-enabled = false # enable http service over unix domain socket
   unix-socket-enabled = false # enable http service over unix domain socket
   # bind-socket = "/var/run/influxdb.sock"
   # bind-socket = "/var/run/influxdb.sock"
+  flux-enabled = true
 
 
 [subscriber]
 [subscriber]
   enabled = true
   enabled = true

+ 6 - 1
docs/sources/features/explore/index.md

@@ -1,5 +1,6 @@
 +++
 +++
 title = "Explore"
 title = "Explore"
+keywords = ["explore", "loki", "logs"]
 type = "docs"
 type = "docs"
 [menu.docs]
 [menu.docs]
 name = "Explore"
 name = "Explore"
@@ -8,7 +9,11 @@ parent = "features"
 weight = 5
 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.
 One of the major new features of Grafana 6.0 is the new query-focused Explore workflow for troubleshooting and/or for data exploration.
 
 

+ 3 - 3
docs/sources/http_api/admin.md

@@ -285,7 +285,7 @@ Content-Type: application/json
 HTTP/1.1 200
 HTTP/1.1 200
 Content-Type: application/json
 Content-Type: application/json
 
 
-{message: "User permissions updated"}
+{"message": "User permissions updated"}
 ```
 ```
 
 
 ## Delete global User
 ## Delete global User
@@ -308,7 +308,7 @@ Content-Type: application/json
 HTTP/1.1 200
 HTTP/1.1 200
 Content-Type: application/json
 Content-Type: application/json
 
 
-{message: "User deleted"}
+{"message": "User deleted"}
 ```
 ```
 
 
 ## Pause all alerts
 ## Pause all alerts
@@ -339,5 +339,5 @@ JSON Body schema:
 HTTP/1.1 200
 HTTP/1.1 200
 Content-Type: application/json
 Content-Type: application/json
 
 
-{state: "new state", message: "alerts pause/un paused", "alertsAffected": 100}
+{"state": "new state", "message": "alerts pause/un paused", "alertsAffected": 100}
 ```
 ```

+ 1 - 1
docs/sources/http_api/folder_permissions.md

@@ -105,7 +105,7 @@ POST /api/folders/nErXDvCkzz/permissions
 Accept: application/json
 Accept: application/json
 Content-Type: application/json
 Content-Type: application/json
 Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
-
+{
   "items": [
   "items": [
     {
     {
       "role": "Viewer",
       "role": "Viewer",

+ 8 - 11
docs/sources/installation/debian.md

@@ -34,32 +34,29 @@ sudo dpkg -i grafana_<version>_amd64.deb
 Example:
 Example:
 
 
 ```bash
 ```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 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
 ## 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
 ```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
 ```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
 ```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
 Update your Apt repositories and install Grafana

+ 15 - 7
docs/sources/installation/rpm.md

@@ -32,7 +32,7 @@ $ sudo yum install <rpm package url>
 Example:
 Example:
 
 
 ```bash
 ```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
 Or install manually using `rpm`. First execute
@@ -44,7 +44,7 @@ $ wget <rpm package url>
 Example:
 Example:
 
 
 ```bash
 ```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:
 ### On CentOS / Fedora / Redhat:
@@ -67,19 +67,27 @@ Add the following to a new file at `/etc/yum.repos.d/grafana.repo`
 ```bash
 ```bash
 [grafana]
 [grafana]
 name=grafana
 name=grafana
-baseurl=https://packagecloud.io/grafana/stable/el/7/$basearch
+baseurl=https://packages.grafana.com/oss/rpm
 repo_gpgcheck=1
 repo_gpgcheck=1
 enabled=1
 enabled=1
 gpgcheck=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
 sslverify=1
 sslcacert=/etc/pki/tls/certs/ca-bundle.crt
 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
 ```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.
 Then install Grafana via the `yum` command.
@@ -91,7 +99,7 @@ $ sudo yum install grafana
 ### RPM GPG Key
 ### RPM GPG Key
 
 
 The RPMs are signed, you can verify the signature with this [public GPG
 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
 ## Package details
 
 

+ 1 - 1
docs/sources/reference/dashboard.md

@@ -51,7 +51,7 @@ When a user creates a new dashboard, a new dashboard JSON object is initialized
     "list": []
     "list": []
   },
   },
   "refresh": "5s",
   "refresh": "5s",
-  "schemaVersion": 16,
+  "schemaVersion": 17,
   "version": 0,
   "version": 0,
   "links": []
   "links": []
 }
 }

+ 4 - 2
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
 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
 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.
 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.
 You can do this by either changing the variable value (that is the basis for the repeat) or reload the dashboard.

+ 2 - 2
latest.json

@@ -1,4 +1,4 @@
 {
 {
-  "stable": "5.4.2",
-  "testing": "5.4.2"
+  "stable": "5.4.3",
+  "testing": "5.4.3"
 }
 }

+ 2 - 3
package.json

@@ -24,7 +24,6 @@
     "@types/jquery": "^1.10.35",
     "@types/jquery": "^1.10.35",
     "@types/node": "^8.0.31",
     "@types/node": "^8.0.31",
     "@types/react": "^16.7.6",
     "@types/react": "^16.7.6",
-    "@types/react-custom-scrollbars": "^4.0.5",
     "@types/react-dom": "^16.0.9",
     "@types/react-dom": "^16.0.9",
     "@types/react-select": "^2.0.4",
     "@types/react-select": "^2.0.4",
     "angular-mocks": "1.6.6",
     "angular-mocks": "1.6.6",
@@ -65,6 +64,7 @@
     "html-webpack-plugin": "^3.2.0",
     "html-webpack-plugin": "^3.2.0",
     "husky": "^0.14.3",
     "husky": "^0.14.3",
     "jest": "^23.6.0",
     "jest": "^23.6.0",
+    "jest-date-mock": "^1.0.6",
     "lint-staged": "^6.0.0",
     "lint-staged": "^6.0.0",
     "load-grunt-tasks": "3.5.2",
     "load-grunt-tasks": "3.5.2",
     "mini-css-extract-plugin": "^0.4.0",
     "mini-css-extract-plugin": "^0.4.0",
@@ -72,8 +72,8 @@
     "ng-annotate-loader": "^0.6.1",
     "ng-annotate-loader": "^0.6.1",
     "ng-annotate-webpack-plugin": "^0.3.0",
     "ng-annotate-webpack-plugin": "^0.3.0",
     "ngtemplate-loader": "^2.0.1",
     "ngtemplate-loader": "^2.0.1",
-    "npm": "^5.4.2",
     "node-sass": "^4.11.0",
     "node-sass": "^4.11.0",
+    "npm": "^5.4.2",
     "optimize-css-assets-webpack-plugin": "^4.0.2",
     "optimize-css-assets-webpack-plugin": "^4.0.2",
     "phantomjs-prebuilt": "^2.1.15",
     "phantomjs-prebuilt": "^2.1.15",
     "postcss-browser-reporter": "^0.5.0",
     "postcss-browser-reporter": "^0.5.0",
@@ -167,7 +167,6 @@
     "prop-types": "^15.6.2",
     "prop-types": "^15.6.2",
     "rc-cascader": "^0.14.0",
     "rc-cascader": "^0.14.0",
     "react": "^16.6.3",
     "react": "^16.6.3",
-    "react-custom-scrollbars": "^4.2.1",
     "react-dom": "^16.6.3",
     "react-dom": "^16.6.3",
     "react-grid-layout": "0.16.6",
     "react-grid-layout": "0.16.6",
     "react-highlight-words": "0.11.0",
     "react-highlight-words": "0.11.0",

+ 14 - 3
packages/grafana-ui/package.json

@@ -11,23 +11,34 @@
   "license": "ISC",
   "license": "ISC",
   "dependencies": {
   "dependencies": {
     "@torkelo/react-select": "2.1.1",
     "@torkelo/react-select": "2.1.1",
+    "@types/react-test-renderer": "^16.0.3",
+    "@types/react-transition-group": "^2.0.15",
     "classnames": "^2.2.5",
     "classnames": "^2.2.5",
     "jquery": "^3.2.1",
     "jquery": "^3.2.1",
     "lodash": "^4.17.10",
     "lodash": "^4.17.10",
     "moment": "^2.22.2",
     "moment": "^2.22.2",
     "react": "^16.6.3",
     "react": "^16.6.3",
+    "react-custom-scrollbars": "^4.2.1",
     "react-dom": "^16.6.3",
     "react-dom": "^16.6.3",
     "react-highlight-words": "0.11.0",
     "react-highlight-words": "0.11.0",
     "react-popper": "^1.3.0",
     "react-popper": "^1.3.0",
     "react-transition-group": "^2.2.1",
     "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": {
   "devDependencies": {
+    "@types/classnames": "^2.2.6",
     "@types/jest": "^23.3.2",
     "@types/jest": "^23.3.2",
+    "@types/jquery": "^1.10.35",
     "@types/lodash": "^4.14.119",
     "@types/lodash": "^4.14.119",
     "@types/react": "^16.7.6",
     "@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"
     "typescript": "^3.2.2"
   }
   }
 }
 }

+ 1 - 1
public/app/core/specs/ColorPalette.test.tsx → packages/grafana-ui/src/components/ColorPicker/ColorPalette.test.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 import React from 'react';
 import renderer from 'react-test-renderer';
 import renderer from 'react-test-renderer';
-import { ColorPalette } from '../components/colorpicker/ColorPalette';
+import { ColorPalette } from './ColorPalette';
 
 
 describe('CollorPalette', () => {
 describe('CollorPalette', () => {
   it('renders correctly', () => {
   it('renders correctly', () => {

+ 3 - 3
public/app/core/components/colorpicker/ColorPalette.tsx → packages/grafana-ui/src/components/ColorPicker/ColorPalette.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
 import React from 'react';
-import { sortedColors } from 'app/core/utils/colors';
+import { sortedColors } from '../../utils';
 
 
 export interface Props {
 export interface Props {
   color: string;
   color: string;
@@ -9,13 +9,13 @@ export interface Props {
 export class ColorPalette extends React.Component<Props, any> {
 export class ColorPalette extends React.Component<Props, any> {
   paletteColors: string[];
   paletteColors: string[];
 
 
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
     super(props);
     this.paletteColors = sortedColors;
     this.paletteColors = sortedColors;
     this.onColorSelect = this.onColorSelect.bind(this);
     this.onColorSelect = this.onColorSelect.bind(this);
   }
   }
 
 
-  onColorSelect(color) {
+  onColorSelect(color: string) {
     return () => {
     return () => {
       this.props.onColorSelect(color);
       this.props.onColorSelect(color);
     };
     };

+ 4 - 9
public/app/core/components/colorpicker/ColorPicker.tsx → packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx

@@ -2,7 +2,6 @@ import React from 'react';
 import ReactDOM from 'react-dom';
 import ReactDOM from 'react-dom';
 import Drop from 'tether-drop';
 import Drop from 'tether-drop';
 import { ColorPickerPopover } from './ColorPickerPopover';
 import { ColorPickerPopover } from './ColorPickerPopover';
-import { react2AngularDirective } from 'app/core/utils/react2angular';
 
 
 export interface Props {
 export interface Props {
   color: string;
   color: string;
@@ -10,7 +9,7 @@ export interface Props {
 }
 }
 
 
 export class ColorPicker extends React.Component<Props, any> {
 export class ColorPicker extends React.Component<Props, any> {
-  pickerElem: HTMLElement;
+  pickerElem: HTMLElement | null;
   colorPickerDrop: any;
   colorPickerDrop: any;
 
 
   openColorPicker = () => {
   openColorPicker = () => {
@@ -20,7 +19,7 @@ export class ColorPicker extends React.Component<Props, any> {
     ReactDOM.render(dropContent, dropContentElem);
     ReactDOM.render(dropContent, dropContentElem);
 
 
     const drop = new Drop({
     const drop = new Drop({
-      target: this.pickerElem,
+      target: this.pickerElem as Element,
       content: dropContentElem,
       content: dropContentElem,
       position: 'top center',
       position: 'top center',
       classes: 'drop-popover',
       classes: 'drop-popover',
@@ -28,6 +27,7 @@ export class ColorPicker extends React.Component<Props, any> {
       hoverCloseDelay: 200,
       hoverCloseDelay: 200,
       tetherOptions: {
       tetherOptions: {
         constraints: [{ to: 'scrollParent', attachment: 'none both' }],
         constraints: [{ to: 'scrollParent', attachment: 'none both' }],
+        attachment: 'bottom center',
       },
       },
     });
     });
 
 
@@ -45,7 +45,7 @@ export class ColorPicker extends React.Component<Props, any> {
     }, 100);
     }, 100);
   };
   };
 
 
-  onColorSelect = color => {
+  onColorSelect = (color: string) => {
     this.props.onChange(color);
     this.props.onChange(color);
   };
   };
 
 
@@ -59,8 +59,3 @@ export class ColorPicker extends React.Component<Props, any> {
     );
     );
   }
   }
 }
 }
-
-react2AngularDirective('colorPicker', ColorPicker, [
-  'color',
-  ['onChange', { watchDepth: 'reference', wrapApply: true }],
-]);

+ 12 - 21
public/app/core/components/colorpicker/ColorPickerPopover.tsx → packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx

@@ -14,7 +14,7 @@ export interface Props {
 export class ColorPickerPopover extends React.Component<Props, any> {
 export class ColorPickerPopover extends React.Component<Props, any> {
   pickerNavElem: any;
   pickerNavElem: any;
 
 
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
     super(props);
     this.state = {
     this.state = {
       tab: 'palette',
       tab: 'palette',
@@ -23,60 +23,51 @@ export class ColorPickerPopover extends React.Component<Props, any> {
     };
     };
   }
   }
 
 
-  setPickerNavElem(elem) {
+  setPickerNavElem(elem: any) {
     this.pickerNavElem = $(elem);
     this.pickerNavElem = $(elem);
   }
   }
 
 
-  setColor(color) {
+  setColor(color: string) {
     const newColor = tinycolor(color);
     const newColor = tinycolor(color);
     if (newColor.isValid()) {
     if (newColor.isValid()) {
-      this.setState({
-        color: newColor.toString(),
-        colorString: newColor.toString(),
-      });
+      this.setState({ color: newColor.toString(), colorString: newColor.toString() });
       this.props.onColorSelect(color);
       this.props.onColorSelect(color);
     }
     }
   }
   }
 
 
-  sampleColorSelected(color) {
+  sampleColorSelected(color: string) {
     this.setColor(color);
     this.setColor(color);
   }
   }
 
 
-  spectrumColorSelected(color) {
+  spectrumColorSelected(color: any) {
     const rgbColor = color.toRgbString();
     const rgbColor = color.toRgbString();
     this.setColor(rgbColor);
     this.setColor(rgbColor);
   }
   }
 
 
-  onColorStringChange(e) {
+  onColorStringChange(e: any) {
     const colorString = e.target.value;
     const colorString = e.target.value;
-    this.setState({
-      colorString: colorString,
-    });
+    this.setState({ colorString: colorString });
 
 
     const newColor = tinycolor(colorString);
     const newColor = tinycolor(colorString);
     if (newColor.isValid()) {
     if (newColor.isValid()) {
       // Update only color state
       // Update only color state
       const newColorString = newColor.toString();
       const newColorString = newColor.toString();
-      this.setState({
-        color: newColorString,
-      });
+      this.setState({ color: newColorString });
       this.props.onColorSelect(newColorString);
       this.props.onColorSelect(newColorString);
     }
     }
   }
   }
 
 
-  onColorStringBlur(e) {
+  onColorStringBlur(e: any) {
     const colorString = e.target.value;
     const colorString = e.target.value;
     this.setColor(colorString);
     this.setColor(colorString);
   }
   }
 
 
   componentDidMount() {
   componentDidMount() {
     this.pickerNavElem.find('li:first').addClass('active');
     this.pickerNavElem.find('li:first').addClass('active');
-    this.pickerNavElem.on('show', e => {
+    this.pickerNavElem.on('show', (e: any) => {
       // use href attr (#name => name)
       // use href attr (#name => name)
       const tab = e.target.hash.slice(1);
       const tab = e.target.hash.slice(1);
-      this.setState({
-        tab: tab,
-      });
+      this.setState({ tab: tab });
     });
     });
   }
   }
 
 

+ 2 - 1
public/app/core/components/colorpicker/SeriesColorPicker.tsx → packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.tsx

@@ -21,7 +21,7 @@ export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
     onToggleAxis: () => {},
     onToggleAxis: () => {},
   };
   };
 
 
-  constructor(props) {
+  constructor(props: SeriesColorPickerProps) {
     super(props);
     super(props);
   }
   }
 
 
@@ -51,6 +51,7 @@ export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
       remove: true,
       remove: true,
       tetherOptions: {
       tetherOptions: {
         constraints: [{ to: 'scrollParent', attachment: 'none both' }],
         constraints: [{ to: 'scrollParent', attachment: 'none both' }],
+        attachment: 'bottom center',
       },
       },
     });
     });
 
 

+ 6 - 10
public/app/core/components/colorpicker/SeriesColorPickerPopover.tsx → packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx

@@ -1,6 +1,5 @@
 import React from 'react';
 import React from 'react';
 import { ColorPickerPopover } from './ColorPickerPopover';
 import { ColorPickerPopover } from './ColorPickerPopover';
-import { react2AngularDirective } from 'app/core/utils/react2angular';
 
 
 export interface SeriesColorPickerPopoverProps {
 export interface SeriesColorPickerPopoverProps {
   color: string;
   color: string;
@@ -22,7 +21,7 @@ export class SeriesColorPickerPopover extends React.PureComponent<SeriesColorPic
 
 
 interface AxisSelectorProps {
 interface AxisSelectorProps {
   yaxis: number;
   yaxis: number;
-  onToggleAxis: () => void;
+  onToggleAxis?: () => void;
 }
 }
 
 
 interface AxisSelectorState {
 interface AxisSelectorState {
@@ -30,7 +29,7 @@ interface AxisSelectorState {
 }
 }
 
 
 export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSelectorState> {
 export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSelectorState> {
-  constructor(props) {
+  constructor(props: AxisSelectorProps) {
     super(props);
     super(props);
     this.state = {
     this.state = {
       yaxis: this.props.yaxis,
       yaxis: this.props.yaxis,
@@ -42,7 +41,10 @@ export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSel
     this.setState({
     this.setState({
       yaxis: this.state.yaxis === 2 ? 1 : 2,
       yaxis: this.state.yaxis === 2 ? 1 : 2,
     });
     });
-    this.props.onToggleAxis();
+
+    if (this.props.onToggleAxis) {
+      this.props.onToggleAxis();
+    }
   }
   }
 
 
   render() {
   render() {
@@ -62,9 +64,3 @@ export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSel
     );
     );
   }
   }
 }
 }
-
-react2AngularDirective('seriesColorPickerPopover', SeriesColorPickerPopover, [
-  'series',
-  'onColorChange',
-  'onToggleAxis',
-]);

+ 5 - 5
public/app/core/components/colorpicker/SpectrumPicker.tsx → packages/grafana-ui/src/components/ColorPicker/SpectrumPicker.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import React from 'react';
 import _ from 'lodash';
 import _ from 'lodash';
 import $ from 'jquery';
 import $ from 'jquery';
-import 'vendor/spectrum';
+import '../../vendor/spectrum';
 
 
 export interface Props {
 export interface Props {
   color: string;
   color: string;
@@ -13,17 +13,17 @@ export class SpectrumPicker extends React.Component<Props, any> {
   elem: any;
   elem: any;
   isMoving: boolean;
   isMoving: boolean;
 
 
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
     super(props);
     this.onSpectrumMove = this.onSpectrumMove.bind(this);
     this.onSpectrumMove = this.onSpectrumMove.bind(this);
     this.setComponentElem = this.setComponentElem.bind(this);
     this.setComponentElem = this.setComponentElem.bind(this);
   }
   }
 
 
-  setComponentElem(elem) {
+  setComponentElem(elem: any) {
     this.elem = $(elem);
     this.elem = $(elem);
   }
   }
 
 
-  onSpectrumMove(color) {
+  onSpectrumMove(color: any) {
     this.isMoving = true;
     this.isMoving = true;
     this.props.onColorSelect(color);
     this.props.onColorSelect(color);
   }
   }
@@ -46,7 +46,7 @@ export class SpectrumPicker extends React.Component<Props, any> {
     this.elem.spectrum('set', this.props.color);
     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
     // 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
     // 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
     // isMoving flag for tracking moving state. Flag should be cleared in componentDidUpdate() which

+ 0 - 0
public/sass/components/_color_picker.scss → packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss


+ 0 - 0
public/app/core/specs/__snapshots__/ColorPalette.test.tsx.snap → packages/grafana-ui/src/components/ColorPicker/__snapshots__/ColorPalette.test.tsx.snap


+ 0 - 0
public/app/core/components/CustomScrollbar/CustomScrollbar.test.tsx → packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.test.tsx


+ 8 - 5
public/app/core/components/CustomScrollbar/CustomScrollbar.tsx → packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx

@@ -6,36 +6,39 @@ interface Props {
   autoHide?: boolean;
   autoHide?: boolean;
   autoHideTimeout?: number;
   autoHideTimeout?: number;
   autoHideDuration?: number;
   autoHideDuration?: number;
+  autoMaxHeight?: string;
   hideTracksWhenNotNeeded?: boolean;
   hideTracksWhenNotNeeded?: boolean;
 }
 }
 
 
 /**
 /**
  * Wraps component into <Scrollbars> component from `react-custom-scrollbars`
  * Wraps component into <Scrollbars> component from `react-custom-scrollbars`
  */
  */
-class CustomScrollbar extends PureComponent<Props> {
+export class CustomScrollbar extends PureComponent<Props> {
   static defaultProps: Partial<Props> = {
   static defaultProps: Partial<Props> = {
     customClassName: 'custom-scrollbars',
     customClassName: 'custom-scrollbars',
     autoHide: true,
     autoHide: true,
     autoHideTimeout: 200,
     autoHideTimeout: 200,
     autoHideDuration: 200,
     autoHideDuration: 200,
+    autoMaxHeight: '100%',
     hideTracksWhenNotNeeded: false,
     hideTracksWhenNotNeeded: false,
   };
   };
 
 
   render() {
   render() {
-    const { customClassName, children, ...scrollProps } = this.props;
+    const { customClassName, children, autoMaxHeight } = this.props;
 
 
     return (
     return (
       <Scrollbars
       <Scrollbars
         className={customClassName}
         className={customClassName}
         autoHeight={true}
         autoHeight={true}
-        autoHeightMin={'inherit'}
-        autoHeightMax={'inherit'}
+        // These autoHeightMin & autoHeightMax options affect firefox and chrome differently.
+        // Before these where set to inhert but that caused problems with cut of legends in firefox
+        autoHeightMin={'0'}
+        autoHeightMax={autoMaxHeight}
         renderTrackHorizontal={props => <div {...props} className="track-horizontal" />}
         renderTrackHorizontal={props => <div {...props} className="track-horizontal" />}
         renderTrackVertical={props => <div {...props} className="track-vertical" />}
         renderTrackVertical={props => <div {...props} className="track-vertical" />}
         renderThumbHorizontal={props => <div {...props} className="thumb-horizontal" />}
         renderThumbHorizontal={props => <div {...props} className="thumb-horizontal" />}
         renderThumbVertical={props => <div {...props} className="thumb-vertical" />}
         renderThumbVertical={props => <div {...props} className="thumb-vertical" />}
         renderView={props => <div {...props} className="view" />}
         renderView={props => <div {...props} className="view" />}
-        {...scrollProps}
       >
       >
         {children}
         {children}
       </Scrollbars>
       </Scrollbars>

+ 40 - 0
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;
+  }
+}

+ 4 - 8
public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap → packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap

@@ -6,8 +6,8 @@ exports[`CustomScrollbar renders correctly 1`] = `
   style={
   style={
     Object {
     Object {
       "height": "auto",
       "height": "auto",
-      "maxHeight": "inherit",
-      "minHeight": "inherit",
+      "maxHeight": "100%",
+      "minHeight": "0",
       "overflow": "hidden",
       "overflow": "hidden",
       "position": "relative",
       "position": "relative",
       "width": "100%",
       "width": "100%",
@@ -23,8 +23,8 @@ exports[`CustomScrollbar renders correctly 1`] = `
         "left": undefined,
         "left": undefined,
         "marginBottom": 0,
         "marginBottom": 0,
         "marginRight": 0,
         "marginRight": 0,
-        "maxHeight": "calc(inherit + 0px)",
-        "minHeight": "calc(inherit + 0px)",
+        "maxHeight": "calc(100% + 0px)",
+        "minHeight": "calc(0 + 0px)",
         "overflow": "scroll",
         "overflow": "scroll",
         "position": "relative",
         "position": "relative",
         "right": undefined,
         "right": undefined,
@@ -42,9 +42,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
       Object {
       Object {
         "display": "none",
         "display": "none",
         "height": 6,
         "height": 6,
-        "opacity": 0,
         "position": "absolute",
         "position": "absolute",
-        "transition": "opacity 200ms",
       }
       }
     }
     }
   >
   >
@@ -64,9 +62,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
     style={
     style={
       Object {
       Object {
         "display": "none",
         "display": "none",
-        "opacity": 0,
         "position": "absolute",
         "position": "absolute",
-        "transition": "opacity 200ms",
         "width": 6,
         "width": 6,
       }
       }
     }
     }

+ 0 - 0
packages/grafana-ui/src/forms/GfFormLabel/GfFormLabel.tsx → packages/grafana-ui/src/components/GfFormLabel/GfFormLabel.tsx


+ 1 - 0
packages/grafana-ui/src/visualizations/Graph/Graph.tsx → packages/grafana-ui/src/components/Graph/Graph.tsx

@@ -98,6 +98,7 @@ export class Graph extends PureComponent<GraphProps> {
       $.plot(this.element, timeSeries, flotOptions);
       $.plot(this.element, timeSeries, flotOptions);
     } catch (err) {
     } catch (err) {
       console.log('Graph rendering error', err, flotOptions, timeSeries);
       console.log('Graph rendering error', err, flotOptions, timeSeries);
+      throw new Error('Error rendering panel');
     }
     }
   }
   }
 
 

+ 5 - 3
public/app/core/components/Label/Label.tsx → packages/grafana-ui/src/components/Label/Label.tsx

@@ -1,5 +1,5 @@
 import React, { SFC, ReactNode } from 'react';
 import React, { SFC, ReactNode } from 'react';
-import Tooltip from '../Tooltip/Tooltip';
+import { Tooltip } from '../Tooltip/Tooltip';
 
 
 interface Props {
 interface Props {
   tooltip?: string;
   tooltip?: string;
@@ -14,8 +14,10 @@ export const Label: SFC<Props> = props => {
     <span className={`gf-form-label width-${props.width ? props.width : '10'}`}>
     <span className={`gf-form-label width-${props.width ? props.width : '10'}`}>
       <span>{props.children}</span>
       <span>{props.children}</span>
       {props.tooltip && (
       {props.tooltip && (
-        <Tooltip className="gf-form-help-icon--right-normal" placement="auto" content={props.tooltip}>
-          <i className="gicon gicon-question gicon--has-hover" />
+        <Tooltip placement="auto" content={props.tooltip}>
+          <div className="gf-form-help-icon--right-normal">
+            <i className="gicon gicon-question gicon--has-hover" />
+          </div>
         </Tooltip>
         </Tooltip>
       )}
       )}
     </span>
     </span>

+ 11 - 0
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<LoadingPlaceholderProps> = ({ text }) => (
+  <div className="gf-form-group">
+    {text} <i className="fa fa-spinner fa-spin" />
+  </div>
+);

+ 15 - 0
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<Props> = ({ children }) => {
+
+  return (
+    <div className="panel-options-grid">
+      {children}
+    </div>
+  );
+};

+ 10 - 0
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);
+  }
+}

+ 4 - 4
public/app/features/dashboard/dashgrid/PanelOptionSection.tsx → packages/grafana-ui/src/components/PanelOptionsGroup/PanelOptionsGroup.tsx

@@ -7,11 +7,11 @@ interface Props {
   children: JSX.Element | JSX.Element[];
   children: JSX.Element | JSX.Element[];
 }
 }
 
 
-export const PanelOptionSection: SFC<Props> = props => {
+export const PanelOptionsGroup: SFC<Props> = props => {
   return (
   return (
-    <div className="panel-option-section">
+    <div className="panel-options-group">
       {props.title && (
       {props.title && (
-        <div className="panel-option-section__header">
+        <div className="panel-options-group__header">
           {props.title}
           {props.title}
           {props.onClose && (
           {props.onClose && (
             <button className="btn btn-link" onClick={props.onClose}>
             <button className="btn btn-link" onClick={props.onClose}>
@@ -20,7 +20,7 @@ export const PanelOptionSection: SFC<Props> = props => {
           )}
           )}
         </div>
         </div>
       )}
       )}
-      <div className="panel-option-section__body">{props.children}</div>
+      <div className="panel-options-group__body">{props.children}</div>
     </div>
     </div>
   );
   );
 };
 };

+ 27 - 0
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;
+  }
+}

+ 3 - 6
public/app/core/components/Portal/Portal.tsx → packages/grafana-ui/src/components/Portal/Portal.tsx

@@ -6,16 +6,13 @@ interface Props {
   root?: HTMLElement;
   root?: HTMLElement;
 }
 }
 
 
-export default class BodyPortal extends PureComponent<Props> {
+export class Portal extends PureComponent<Props> {
   node: HTMLElement = document.createElement('div');
   node: HTMLElement = document.createElement('div');
   portalRoot: HTMLElement;
   portalRoot: HTMLElement;
 
 
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
     super(props);
-    const {
-      className,
-      root = document.body
-    } = this.props;
+    const { className, root = document.body } = this.props;
 
 
     if (className) {
     if (className) {
       this.node.classList.add(className);
       this.node.classList.add(className);

+ 4 - 1
public/app/core/components/Select/IndicatorsContainer.tsx → packages/grafana-ui/src/components/Select/IndicatorsContainer.tsx

@@ -1,7 +1,10 @@
 import React from 'react';
 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 { components } from '@torkelo/react-select';
 
 
-export const IndicatorsContainer = props => {
+export const IndicatorsContainer = (props: any) => {
   const isOpen = props.selectProps.menuIsOpen;
   const isOpen = props.selectProps.menuIsOpen;
   return (
   return (
     <components.IndicatorsContainer {...props}>
     <components.IndicatorsContainer {...props}>

+ 4 - 0
public/app/core/components/Select/NoOptionsMessage.tsx → packages/grafana-ui/src/components/Select/NoOptionsMessage.tsx

@@ -1,5 +1,9 @@
 import React from 'react';
 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 { components } from '@torkelo/react-select';
+// @ts-ignore
 import { OptionProps } from '@torkelo/react-select/lib/components/Option';
 import { OptionProps } from '@torkelo/react-select/lib/components/Option';
 
 
 export interface Props {
 export interface Props {

+ 15 - 10
public/app/core/components/Select/Select.tsx → packages/grafana-ui/src/components/Select/Select.tsx

@@ -1,17 +1,22 @@
 // Libraries
 // Libraries
 import classNames from 'classnames';
 import classNames from 'classnames';
 import React, { PureComponent } from 'react';
 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';
 import { default as ReactSelect } from '@torkelo/react-select';
+// @ts-ignore
 import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async';
 import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async';
+// @ts-ignore
 import { components } from '@torkelo/react-select';
 import { components } from '@torkelo/react-select';
 
 
 // Components
 // Components
-import { Option, SingleValue } from './PickerOption';
-import OptionGroup from './OptionGroup';
+import { SelectOption, SingleValue } from './SelectOption';
+import SelectOptionGroup from './SelectOptionGroup';
 import IndicatorsContainer from './IndicatorsContainer';
 import IndicatorsContainer from './IndicatorsContainer';
 import NoOptionsMessage from './NoOptionsMessage';
 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 {
 export interface SelectOptionItem {
   label?: string;
   label?: string;
@@ -53,10 +58,10 @@ interface AsyncProps {
   loadingMessage?: () => string;
   loadingMessage?: () => string;
 }
 }
 
 
-export const MenuList = props => {
+export const MenuList = (props: any) => {
   return (
   return (
     <components.MenuList {...props}>
     <components.MenuList {...props}>
-      <CustomScrollbar autoHide={false}>{props.children}</CustomScrollbar>
+      <CustomScrollbar autoHide={false} autoMaxHeight="inherit">{props.children}</CustomScrollbar>
     </components.MenuList>
     </components.MenuList>
   );
   );
 };
 };
@@ -112,11 +117,11 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
         classNamePrefix="gf-form-select-box"
         classNamePrefix="gf-form-select-box"
         className={selectClassNames}
         className={selectClassNames}
         components={{
         components={{
-          Option,
+          Option: SelectOption,
           SingleValue,
           SingleValue,
           IndicatorsContainer,
           IndicatorsContainer,
           MenuList,
           MenuList,
-          Group: OptionGroup,
+          Group: SelectOptionGroup,
         }}
         }}
         defaultValue={defaultValue}
         defaultValue={defaultValue}
         value={value}
         value={value}
@@ -127,7 +132,7 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
         onChange={onChange}
         onChange={onChange}
         options={options}
         options={options}
         placeholder={placeholder || 'Choose'}
         placeholder={placeholder || 'Choose'}
-        styles={ResetStyles}
+        styles={resetSelectStyles()}
         isDisabled={isDisabled}
         isDisabled={isDisabled}
         isLoading={isLoading}
         isLoading={isLoading}
         isClearable={isClearable}
         isClearable={isClearable}
@@ -212,7 +217,7 @@ export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> {
         isLoading={isLoading}
         isLoading={isLoading}
         defaultOptions={defaultOptions}
         defaultOptions={defaultOptions}
         placeholder={placeholder || 'Choose'}
         placeholder={placeholder || 'Choose'}
-        styles={ResetStyles}
+        styles={resetSelectStyles()}
         loadingMessage={loadingMessage}
         loadingMessage={loadingMessage}
         noOptionsMessage={noOptionsMessage}
         noOptionsMessage={noOptionsMessage}
         isDisabled={isDisabled}
         isDisabled={isDisabled}

+ 23 - 12
public/app/core/components/Select/PickerOption.test.tsx → packages/grafana-ui/src/components/Select/SelectOption.test.tsx

@@ -1,11 +1,12 @@
 import React from 'react';
 import React from 'react';
 import renderer from 'react-test-renderer';
 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<any> = {
+  data: jest.fn(),
   cx: jest.fn(),
   cx: jest.fn(),
   clearValue: jest.fn(),
   clearValue: jest.fn(),
-  onSelect: jest.fn(),
   getStyles: jest.fn(),
   getStyles: jest.fn(),
   getValue: jest.fn(),
   getValue: jest.fn(),
   hasValue: true,
   hasValue: true,
@@ -18,21 +19,31 @@ const model = {
   isFocused: false,
   isFocused: false,
   isSelected: false,
   isSelected: false,
   innerRef: null,
   innerRef: null,
-  innerProps: null,
+  innerProps: {
+    id: '',
+    key: '',
+    onClick: jest.fn(),
+    onMouseOver: jest.fn(),
+    tabIndex: 1,
+  },
   label: 'Option label',
   label: 'Option label',
-  type: null,
+  type: 'option',
   children: 'Model title',
   children: 'Model title',
-  data: {
-    title: 'Model title',
-    imgUrl: 'url/to/avatar',
-    label: 'User picker label',
-  },
   className: 'class-for-user-picker',
   className: 'class-for-user-picker',
 };
 };
 
 
-describe('PickerOption', () => {
+describe('SelectOption', () => {
   it('renders correctly', () => {
   it('renders correctly', () => {
-    const tree = renderer.create(<PickerOption {...model} />).toJSON();
+    const tree = renderer
+      .create(
+        <SelectOption
+          {...model}
+          data={{
+            imgUrl: 'url/to/avatar',
+          }}
+        />
+      )
+      .toJSON();
     expect(tree).toMatchSnapshot();
     expect(tree).toMatchSnapshot();
   });
   });
 });
 });

+ 6 - 3
public/app/core/components/Select/PickerOption.tsx → packages/grafana-ui/src/components/Select/SelectOption.tsx

@@ -1,4 +1,7 @@
 import React from 'react';
 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 { components } from '@torkelo/react-select';
 import { OptionProps } from 'react-select/lib/components/Option';
 import { OptionProps } from 'react-select/lib/components/Option';
 
 
@@ -10,7 +13,7 @@ interface ExtendedOptionProps extends OptionProps<any> {
   };
   };
 }
 }
 
 
-export const Option = (props: ExtendedOptionProps) => {
+export const SelectOption = (props: ExtendedOptionProps) => {
   const { children, isSelected, data } = props;
   const { children, isSelected, data } = props;
 
 
   return (
   return (
@@ -28,7 +31,7 @@ export const Option = (props: ExtendedOptionProps) => {
 };
 };
 
 
 // was not able to type this without typescript error
 // was not able to type this without typescript error
-export const SingleValue = props => {
+export const SingleValue = (props: any) => {
   const { children, data } = props;
   const { children, data } = props;
 
 
   return (
   return (
@@ -41,4 +44,4 @@ export const SingleValue = props => {
   );
   );
 };
 };
 
 
-export default Option;
+export default SelectOption;

+ 11 - 5
public/app/core/components/Select/OptionGroup.tsx → 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';
 import { GroupProps } from 'react-select/lib/components/Group';
 
 
 interface ExtendedGroupProps extends GroupProps<any> {
 interface ExtendedGroupProps extends GroupProps<any> {
-  data: any;
+  data: {
+    label: string;
+    expanded: boolean;
+    options: any[];
+  };
 }
 }
 
 
 interface State {
 interface State {
   expanded: boolean;
   expanded: boolean;
 }
 }
 
 
-export default class OptionGroup extends PureComponent<ExtendedGroupProps, State> {
+export default class SelectOptionGroup extends PureComponent<ExtendedGroupProps, State> {
   state = {
   state = {
     expanded: false,
     expanded: false,
   };
   };
 
 
   componentDidMount() {
   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)) {
       if (value && this.props.options.some(option => option.value === value)) {
         this.setState({ expanded: true });
         this.setState({ expanded: true });
@@ -24,7 +30,7 @@ export default class OptionGroup extends PureComponent<ExtendedGroupProps, State
     }
     }
   }
   }
 
 
-  componentDidUpdate(nextProps) {
+  componentDidUpdate(nextProps: ExtendedGroupProps) {
     if (nextProps.selectProps.inputValue !== '') {
     if (nextProps.selectProps.inputValue !== '') {
       this.setState({ expanded: true });
       this.setState({ expanded: true });
     }
     }

+ 1 - 0
public/sass/components/_form_select_box.scss → packages/grafana-ui/src/components/Select/_Select.scss

@@ -63,6 +63,7 @@ $select-input-bg-disabled: $input-bg-disabled;
 .gf-form-select-box__menu-list {
 .gf-form-select-box__menu-list {
   overflow-y: auto;
   overflow-y: auto;
   max-height: 300px;
   max-height: 300px;
+  max-width: 600px;
 }
 }
 
 
 .tag-filter .gf-form-select-box__menu {
 .tag-filter .gf-form-select-box__menu {

+ 7 - 2
public/app/core/components/Select/__snapshots__/PickerOption.test.tsx.snap → packages/grafana-ui/src/components/Select/__snapshots__/SelectOption.test.tsx.snap

@@ -1,7 +1,12 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 
-exports[`PickerOption renders correctly 1`] = `
-<div>
+exports[`SelectOption renders correctly 1`] = `
+<div
+  id=""
+  onClick={[MockFunction]}
+  onMouseOver={[MockFunction]}
+  tabIndex={1}
+>
   <div
   <div
     className="gf-form-select-box__desc-option"
     className="gf-form-select-box__desc-option"
   >
   >

+ 27 - 0
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: () => ({}),
+  };
+}

+ 173 - 0
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(<ThresholdsEditor {...props} />).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' },
+    ]);
+  });
+});

+ 206 - 0
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<Props, State> {
+  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 (
+      <div className="thresholds-row-input-inner">
+        <span className="thresholds-row-input-inner-arrow" />
+        <div className="thresholds-row-input-inner-color">
+          {threshold.color && (
+            <div className="thresholds-row-input-inner-color-colorpicker">
+              <ColorPicker color={threshold.color} onChange={color => this.onChangeThresholdColor(threshold, color)} />
+            </div>
+          )}
+        </div>
+        <div className="thresholds-row-input-inner-value">
+          <input
+            type="text"
+            onChange={event => this.onChangeThresholdValue(event, threshold)}
+            value={value}
+            onBlur={this.onBlur}
+            readOnly={threshold.index === 0}
+          />
+        </div>
+        {threshold.index > 0 && (
+          <div className="thresholds-row-input-inner-remove" onClick={() => this.onRemoveThreshold(threshold)}>
+            <i className="fa fa-times" />
+          </div>
+        )}
+      </div>
+    );
+  };
+
+  render() {
+    const { thresholds } = this.state;
+
+    return (
+      <PanelOptionsGroup title="Thresholds">
+        <div className="thresholds">
+          {thresholds.map((threshold, index) => {
+            return (
+              <div className="thresholds-row" key={`${threshold.index}-${index}`}>
+                <div className="thresholds-row-add-button" onClick={() => this.onAddThreshold(threshold.index + 1)}>
+                  <i className="fa fa-plus" />
+                </div>
+                <div className="thresholds-row-color-indicator" style={{ backgroundColor: threshold.color }} />
+                <div className="thresholds-row-input">{this.renderInput(threshold)}</div>
+              </div>
+            );
+          })}
+        </div>
+      </PanelOptionsGroup>
+    );
+  }
+}

+ 105 - 0
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;
+}

+ 21 - 16
public/app/core/components/Tooltip/Popper.tsx → packages/grafana-ui/src/components/Tooltip/Popper.tsx

@@ -1,49 +1,54 @@
 import React, { PureComponent } from 'react';
 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';
 import Transition from 'react-transition-group/Transition';
 
 
+export enum Themes {
+  Default = 'popper__background--default',
+  Error = 'popper__background--error',
+  Brand = 'popper__background--brand',
+}
+
 const defaultTransitionStyles = {
 const defaultTransitionStyles = {
   transition: 'opacity 200ms linear',
   transition: 'opacity 200ms linear',
   opacity: 0,
   opacity: 0,
 };
 };
 
 
-const transitionStyles = {
+const transitionStyles: {[key: string]: object} = {
   exited: { opacity: 0 },
   exited: { opacity: 0 },
   entering: { opacity: 0 },
   entering: { opacity: 0 },
   entered: { opacity: 1 },
   entered: { opacity: 1 },
   exiting: { opacity: 0 },
   exiting: { opacity: 0 },
 };
 };
 
 
-interface Props {
+interface Props extends React.DOMAttributes<HTMLDivElement> {
   renderContent: (content: any) => any;
   renderContent: (content: any) => any;
   show: boolean;
   show: boolean;
-  placement?: any;
+  placement?: PopperJS.Placement;
   content: string | ((props: any) => JSX.Element);
   content: string | ((props: any) => JSX.Element);
-  refClassName?: string;
+  referenceElement: PopperJS.ReferenceObject;
+  theme?: Themes;
 }
 }
 
 
 class Popper extends PureComponent<Props> {
 class Popper extends PureComponent<Props> {
   render() {
   render() {
-    const { children, renderContent, show, placement, refClassName } = this.props;
+    const { renderContent, show, placement, onMouseEnter, onMouseLeave, theme } = this.props;
     const { content } = this.props;
     const { content } = this.props;
 
 
+    const popperBackgroundClassName = 'popper__background' + (theme ? ' ' + theme : '');
+
     return (
     return (
       <Manager>
       <Manager>
-        <Reference>
-          {({ ref }) => (
-            <div className={`popper_ref ${refClassName || ''}`} ref={ref}>
-              {children}
-            </div>
-          )}
-        </Reference>
         <Transition in={show} timeout={100} mountOnEnter={true} unmountOnExit={true}>
         <Transition in={show} timeout={100} mountOnEnter={true} unmountOnExit={true}>
           {transitionState => (
           {transitionState => (
             <Portal>
             <Portal>
-              <ReactPopper placement={placement}>
+              <ReactPopper placement={placement} referenceElement={this.props.referenceElement}>
                 {({ ref, style, placement, arrowProps }) => {
                 {({ ref, style, placement, arrowProps }) => {
                   return (
                   return (
                     <div
                     <div
+                      onMouseEnter={onMouseEnter}
+                      onMouseLeave={onMouseLeave}
                       ref={ref}
                       ref={ref}
                       style={{
                       style={{
                         ...style,
                         ...style,
@@ -53,7 +58,7 @@ class Popper extends PureComponent<Props> {
                       data-placement={placement}
                       data-placement={placement}
                       className="popper"
                       className="popper"
                     >
                     >
-                      <div className="popper__background">
+                      <div className={popperBackgroundClassName}>
                         {renderContent(content)}
                         {renderContent(content)}
                         <div ref={arrowProps.ref} data-placement={placement} className="popper__arrow" />
                         <div ref={arrowProps.ref} data-placement={placement} className="popper__arrow" />
                       </div>
                       </div>

+ 99 - 0
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<Props, State> {
+  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 <ReactComponent />;
+    }
+    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;

+ 5 - 3
public/app/core/components/Tooltip/Tooltip.test.tsx → packages/grafana-ui/src/components/Tooltip/Tooltip.test.tsx

@@ -1,13 +1,15 @@
 import React from 'react';
 import React from 'react';
 import renderer from 'react-test-renderer';
 import renderer from 'react-test-renderer';
-import Tooltip from './Tooltip';
+import { Tooltip } from './Tooltip';
 
 
 describe('Tooltip', () => {
 describe('Tooltip', () => {
   it('renders correctly', () => {
   it('renders correctly', () => {
     const tree = renderer
     const tree = renderer
       .create(
       .create(
-        <Tooltip className="test-class" placement="auto" content="Tooltip text">
-          <a href="http://www.grafana.com">Link with tooltip</a>
+        <Tooltip placement="auto" content="Tooltip text">
+          <a className="test-class" href="http://www.grafana.com">
+            Link with tooltip
+          </a>
         </Tooltip>
         </Tooltip>
       )
       )
       .toJSON();
       .toJSON();

+ 32 - 0
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<PopperJS.ReferenceObject>();
+
+  return (
+    <PopperController {...controllerProps}>
+      {(showPopper, hidePopper, popperProps) => {
+        return (
+          <>
+            {tooltipTriggerRef.current && (
+              <Popper
+                {...popperProps}
+                onMouseEnter={showPopper}
+                onMouseLeave={hidePopper}
+                referenceElement={tooltipTriggerRef.current}
+              />
+            )}
+            {React.cloneElement(children, {
+              ref: tooltipTriggerRef,
+              onMouseEnter: showPopper,
+              onMouseLeave: hidePopper,
+            })}
+          </>
+        );
+      }}
+    </PopperController>
+  );
+};

+ 27 - 9
public/sass/components/_popper.scss → packages/grafana-ui/src/components/Tooltip/_Tooltip.scss

@@ -1,5 +1,13 @@
 $popper-margin-from-ref: 5px;
 $popper-margin-from-ref: 5px;
 
 
+
+@mixin popper-theme($backgroundColor, $arrowColor) {
+  background: $backgroundColor;
+  .popper__arrow {
+    border-color: $arrowColor;
+  }
+}
+
 .popper {
 .popper {
   position: absolute;
   position: absolute;
   z-index: $zindex-tooltip;
   z-index: $zindex-tooltip;
@@ -8,7 +16,24 @@ $popper-margin-from-ref: 5px;
   text-align: center;
   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;
   width: 0;
   height: 0;
   height: 0;
   border-style: solid;
   border-style: solid;
@@ -16,17 +41,10 @@ $popper-margin-from-ref: 5px;
   margin: 0px;
   margin: 0px;
 }
 }
 
 
-.popper .popper__arrow {
+.popper__arrow {
   border-color: $tooltipBackground;
   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
 // Top
 .popper[data-placement^='top'] {
 .popper[data-placement^='top'] {
   padding-bottom: $popper-margin-from-ref;
   padding-bottom: $popper-margin-from-ref;

+ 12 - 0
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`] = `
+<a
+  className="test-class"
+  href="http://www.grafana.com"
+  onMouseEnter={[Function]}
+  onMouseLeave={[Function]}
+>
+  Link with tooltip
+</a>
+`;

+ 21 - 22
public/app/plugins/panel/gauge/MappingRow.tsx → packages/grafana-ui/src/components/ValueMappingsEditor/MappingRow.tsx

@@ -1,22 +1,23 @@
 import React, { PureComponent } from 'react';
 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 {
 interface State {
-  from: string;
+  from?: string;
   id: number;
   id: number;
   operator: string;
   operator: string;
   text: string;
   text: string;
-  to: string;
+  to?: string;
   type: MappingType;
   type: MappingType;
-  value: string;
+  value?: string;
 }
 }
 
 
 const mappingOptions = [
 const mappingOptions = [
@@ -25,36 +26,34 @@ const mappingOptions = [
 ];
 ];
 
 
 export default class MappingRow extends PureComponent<Props, State> {
 export default class MappingRow extends PureComponent<Props, State> {
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
     super(props);
 
 
-    this.state = {
-      ...props.mapping,
-    };
+    this.state = { ...props.valueMapping };
   }
   }
 
 
-  onMappingValueChange = event => {
+  onMappingValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
     this.setState({ value: event.target.value });
     this.setState({ value: event.target.value });
   };
   };
 
 
-  onMappingFromChange = event => {
+  onMappingFromChange = (event: React.ChangeEvent<HTMLInputElement>) => {
     this.setState({ from: event.target.value });
     this.setState({ from: event.target.value });
   };
   };
 
 
-  onMappingToChange = event => {
+  onMappingToChange = (event: React.ChangeEvent<HTMLInputElement>) => {
     this.setState({ to: event.target.value });
     this.setState({ to: event.target.value });
   };
   };
 
 
-  onMappingTextChange = event => {
+  onMappingTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
     this.setState({ text: event.target.value });
     this.setState({ text: event.target.value });
   };
   };
 
 
-  onMappingTypeChange = mappingType => {
+  onMappingTypeChange = (mappingType: MappingType) => {
     this.setState({ type: mappingType });
     this.setState({ type: mappingType });
   };
   };
 
 
   updateMapping = () => {
   updateMapping = () => {
-    this.props.updateMapping({ ...this.state });
+    this.props.updateValueMapping({ ...this.state } as ValueMapping);
   };
   };
 
 
   renderRow() {
   renderRow() {
@@ -136,7 +135,7 @@ export default class MappingRow extends PureComponent<Props, State> {
         </div>
         </div>
         {this.renderRow()}
         {this.renderRow()}
         <div className="gf-form">
         <div className="gf-form">
-          <button onClick={this.props.removeMapping} className="gf-form-label gf-form-label--btn">
+          <button onClick={this.props.removeValueMapping} className="gf-form-label gf-form-label--btn">
             <i className="fa fa-times" />
             <i className="fa fa-times" />
           </button>
           </button>
         </div>
         </div>

+ 15 - 16
public/app/plugins/panel/gauge/ValueMappings.test.tsx → packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.test.tsx

@@ -1,26 +1,23 @@
 import React from 'react';
 import React from 'react';
 import { shallow } from 'enzyme';
 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 setup = (propOverrides?: object) => {
-  const props: OptionModuleProps = {
+  const props: Props = {
     onChange: jest.fn(),
     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);
   Object.assign(props, propOverrides);
 
 
-  const wrapper = shallow(<ValueMappings {...props} />);
+  const wrapper = shallow(<ValueMappingsEditor {...props} />);
 
 
-  const instance = wrapper.instance() as ValueMappings;
+  const instance = wrapper.instance() as ValueMappingsEditor;
 
 
   return {
   return {
     instance,
     instance,
@@ -39,18 +36,20 @@ describe('Render', () => {
 describe('On remove mapping', () => {
 describe('On remove mapping', () => {
   it('Should remove mapping with id 0', () => {
   it('Should remove mapping with id 0', () => {
     const { instance } = setup();
     const { instance } = setup();
+
     instance.onRemoveMapping(1);
     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' },
       { id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
     ]);
     ]);
   });
   });
 
 
   it('should remove mapping with id 1', () => {
   it('should remove mapping with id 1', () => {
     const { instance } = setup();
     const { instance } = setup();
+
     instance.onRemoveMapping(2);
     instance.onRemoveMapping(2);
 
 
-    expect(instance.state.mappings).toEqual([
+    expect(instance.state.valueMappings).toEqual([
       { id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
       { id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
     ]);
     ]);
   });
   });
@@ -66,7 +65,7 @@ describe('Next id to add', () => {
   });
   });
 
 
   it('should default to 1', () => {
   it('should default to 1', () => {
-    const { instance } = setup({ options: { ...defaultProps.options } });
+    const { instance } = setup({ valueMappings: [] });
 
 
     expect(instance.state.nextIdToAdd).toEqual(1);
     expect(instance.state.nextIdToAdd).toEqual(1);
   });
   });

+ 105 - 0
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<Props, State> {
+  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 (
+      <PanelOptionsGroup title="Value Mappings">
+        <div>
+          {valueMappings.length > 0 &&
+            valueMappings.map((valueMapping, index) => (
+              <MappingRow
+                key={`${valueMapping.text}-${index}`}
+                valueMapping={valueMapping}
+                updateValueMapping={this.updateGauge}
+                removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
+              />
+            ))}
+        </div>
+        <div className="add-mapping-row" onClick={this.addMapping}>
+          <div className="add-mapping-row-icon">
+            <i className="fa fa-plus" />
+          </div>
+          <div className="add-mapping-row-label">Add mapping</div>
+        </div>
+      </PanelOptionsGroup>
+    );
+  }
+}

+ 0 - 0
public/sass/components/_value-mappings.scss → packages/grafana-ui/src/components/ValueMappingsEditor/_ValueMappingsEditor.scss


+ 9 - 14
public/app/plugins/panel/gauge/__snapshots__/ValueMappings.test.tsx.snap → packages/grafana-ui/src/components/ValueMappingsEditor/__snapshots__/ValueMappingsEditor.test.tsx.snap

@@ -1,18 +1,15 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 
 exports[`Render should render component 1`] = `
 exports[`Render should render component 1`] = `
-<div
-  className="section gf-form-group"
+<Component
+  title="Value Mappings"
 >
 >
-  <h5
-    className="section-heading"
-  >
-    Value mappings
-  </h5>
   <div>
   <div>
     <MappingRow
     <MappingRow
       key="Ok-0"
       key="Ok-0"
-      mapping={
+      removeValueMapping={[Function]}
+      updateValueMapping={[Function]}
+      valueMapping={
         Object {
         Object {
           "id": 1,
           "id": 1,
           "operator": "",
           "operator": "",
@@ -21,12 +18,12 @@ exports[`Render should render component 1`] = `
           "value": "20",
           "value": "20",
         }
         }
       }
       }
-      removeMapping={[Function]}
-      updateMapping={[Function]}
     />
     />
     <MappingRow
     <MappingRow
       key="Meh-1"
       key="Meh-1"
-      mapping={
+      removeValueMapping={[Function]}
+      updateValueMapping={[Function]}
+      valueMapping={
         Object {
         Object {
           "from": "21",
           "from": "21",
           "id": 2,
           "id": 2,
@@ -36,8 +33,6 @@ exports[`Render should render component 1`] = `
           "type": 2,
           "type": 2,
         }
         }
       }
       }
-      removeMapping={[Function]}
-      updateMapping={[Function]}
     />
     />
   </div>
   </div>
   <div
   <div
@@ -57,5 +52,5 @@ exports[`Render should render component 1`] = `
       Add mapping
       Add mapping
     </div>
     </div>
   </div>
   </div>
-</div>
+</Component>
 `;
 `;

+ 8 - 0
packages/grafana-ui/src/components/index.scss

@@ -1 +1,9 @@
+@import 'CustomScrollbar/CustomScrollbar';
 @import 'DeleteButton/DeleteButton';
 @import 'DeleteButton/DeleteButton';
+@import 'ThresholdsEditor/ThresholdsEditor';
+@import 'Tooltip/Tooltip';
+@import 'Select/Select';
+@import 'PanelOptionsGroup/PanelOptionsGroup';
+@import 'PanelOptionsGrid/PanelOptionsGrid';
+@import 'ColorPicker/ColorPicker';
+@import 'ValueMappingsEditor/ValueMappingsEditor';

+ 21 - 0
packages/grafana-ui/src/components/index.ts

@@ -1 +1,22 @@
 export { DeleteButton } from './DeleteButton/DeleteButton';
 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';

+ 0 - 1
packages/grafana-ui/src/forms/index.ts

@@ -1 +0,0 @@
-export { GfFormLabel } from './GfFormLabel/GfFormLabel';

+ 2 - 0
packages/grafana-ui/src/index.scss

@@ -1 +1,3 @@
+@import 'vendor/spectrum';
 @import 'components/index';
 @import 'components/index';
+

+ 0 - 2
packages/grafana-ui/src/index.ts

@@ -1,5 +1,3 @@
 export * from './components';
 export * from './components';
-export * from './visualizations';
 export * from './types';
 export * from './types';
 export * from './utils';
 export * from './utils';
-export * from './forms';

+ 37 - 0
packages/grafana-ui/src/types/panel.ts

@@ -1,6 +1,8 @@
 import { TimeSeries, LoadingState } from './series';
 import { TimeSeries, LoadingState } from './series';
 import { TimeRange } from './time';
 import { TimeRange } from './time';
 
 
+export type InterpolateFunction = (value: string, format?: string | Function) => string;
+
 export interface PanelProps<T = any> {
 export interface PanelProps<T = any> {
   timeSeries: TimeSeries[];
   timeSeries: TimeSeries[];
   timeRange: TimeRange;
   timeRange: TimeRange;
@@ -9,6 +11,7 @@ export interface PanelProps<T = any> {
   renderCounter: number;
   renderCounter: number;
   width: number;
   width: number;
   height: number;
   height: number;
+  onInterpolate: InterpolateFunction;
 }
 }
 
 
 export interface PanelOptionsProps<T = any> {
 export interface PanelOptionsProps<T = any> {
@@ -29,3 +32,37 @@ export interface PanelMenuItem {
   shortcut?: string;
   shortcut?: string;
   subMenu?: PanelMenuItem[];
   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;
+}

+ 93 - 0
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);

+ 2 - 0
packages/grafana-ui/src/utils/index.ts

@@ -1 +1,3 @@
 export * from './processTimeSeries';
 export * from './processTimeSeries';
+export * from './valueFormats/valueFormats';
+export * from './colors';

+ 40 - 0
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');
+  });
+});

+ 42 - 0
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);
+}

+ 322 - 0
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') },
+    ],
+  }
+];

+ 231 - 0
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');
+    });
+  });
+});

+ 312 - 0
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();
+}

+ 7 - 0
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');
+  });
+});

+ 30 - 0
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);
+}

+ 166 - 0
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,
+        };
+      }),
+    };
+  });
+}

+ 0 - 0
public/vendor/css/spectrum.css → packages/grafana-ui/src/vendor/spectrum.css


+ 0 - 0
public/vendor/spectrum.js → packages/grafana-ui/src/vendor/spectrum.js


+ 0 - 1
packages/grafana-ui/src/visualizations/index.ts

@@ -1 +0,0 @@
-export { Graph } from './Graph/Graph';

+ 4 - 2
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"
 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
 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_UID="472"
 ARG GF_GID="472"
 ARG GF_GID="472"

+ 0 - 1
packaging/docker/build-deploy.sh

@@ -8,6 +8,5 @@ docker login -u "$DOCKER_USER" -p "$DOCKER_PASS"
 ./push_to_docker_hub.sh "$_grafana_version"
 ./push_to_docker_hub.sh "$_grafana_version"
 
 
 if echo "$_grafana_version" | grep -q "^master-"; then
 if echo "$_grafana_version" | grep -q "^master-"; then
-  apk add --no-cache curl
   ./deploy_to_k8s.sh "grafana/grafana-dev:$_grafana_version"
   ./deploy_to_k8s.sh "grafana/grafana-dev:$_grafana_version"
 fi
 fi

+ 32 - 8
packaging/docker/build.sh

@@ -1,25 +1,49 @@
 #!/bin/sh
 #!/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 the tag starts with v, treat this as a official release
 if echo "$_grafana_tag" | grep -q "^v"; then
 if echo "$_grafana_tag" | grep -q "^v"; then
 	_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
 	_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
-	_docker_repo=${2:-grafana/grafana}
 else
 else
 	_grafana_version=$_grafana_tag
 	_grafana_version=$_grafana_tag
-	_docker_repo=${2:-grafana/grafana-dev}
 fi
 fi
 
 
 echo "Building ${_docker_repo}:${_grafana_version}"
 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
 # Tag as 'latest' for official release; otherwise tag as grafana/grafana:master
 if echo "$_grafana_tag" | grep -q "^v"; then
 if echo "$_grafana_tag" | grep -q "^v"; then
-	docker tag "${_docker_repo}:${_grafana_version}" "${_docker_repo}:latest"
+	docker_tag_all "${_docker_repo}" "latest"
 else
 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
 fi

+ 29 - 7
packaging/docker/push_to_docker_hub.sh

@@ -1,24 +1,46 @@
 #!/bin/sh
 #!/bin/sh
 set -e
 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 the tag starts with v, treat this as a official release
 if echo "$_grafana_tag" | grep -q "^v"; then
 if echo "$_grafana_tag" | grep -q "^v"; then
 	_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
 	_grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2)
-	_docker_repo=${2:-grafana/grafana}
 else
 else
 	_grafana_version=$_grafana_tag
 	_grafana_version=$_grafana_tag
-	_docker_repo=${2:-grafana/grafana-dev}
 fi
 fi
 
 
+export DOCKER_CLI_EXPERIMENTAL=enabled
+
 echo "pushing ${_docker_repo}:${_grafana_version}"
 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
 if echo "$_grafana_tag" | grep -q "^v" && echo "$_grafana_tag" | grep -vq "beta"; then
 	echo "pushing ${_docker_repo}:latest"
 	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
 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
 fi

+ 4 - 0
pkg/api/alerting.go

@@ -212,6 +212,10 @@ func GetAlertNotificationByID(c *m.ReqContext) Response {
 		return Error(500, "Failed to get alert notifications", err)
 		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))
 	return JSON(200, dtos.NewAlertNotification(query.Result))
 }
 }
 
 

+ 6 - 0
pkg/api/alerting_test.go

@@ -119,6 +119,12 @@ func TestAlertingApiEndpoint(t *testing.T) {
 			So(getAlertsQuery.Limit, ShouldEqual, 5)
 			So(getAlertsQuery.Limit, ShouldEqual, 5)
 			So(getAlertsQuery.Query, ShouldEqual, "alertQuery")
 			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)
+		})
 	})
 	})
 }
 }
 
 

+ 20 - 29
pkg/components/dashdiffs/formatter_json.go

@@ -206,10 +206,9 @@ func (f *JSONFormatter) processObject(object map[string]interface{}, deltas []di
 
 
 	// Added
 	// Added
 	for _, delta := range deltas {
 	for _, delta := range deltas {
-		switch delta.(type) {
+		switch delta := delta.(type) {
 		case *diff.Added:
 		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 {
 	if len(matchedDeltas) > 0 {
 		for _, matchedDelta := range matchedDeltas {
 		for _, matchedDelta := range matchedDeltas {
 
 
-			switch matchedDelta.(type) {
+			switch matchedDelta := matchedDelta.(type) {
 			case *diff.Object:
 			case *diff.Object:
-				d := matchedDelta.(*diff.Object)
 				switch value.(type) {
 				switch value.(type) {
 				case map[string]interface{}:
 				case map[string]interface{}:
 					//ok
 					//ok
@@ -238,7 +236,7 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi
 				f.print("{")
 				f.print("{")
 				f.closeLine()
 				f.closeLine()
 				f.push(positionStr, len(o), false)
 				f.push(positionStr, len(o), false)
-				f.processObject(o, d.Deltas)
+				f.processObject(o, matchedDelta.Deltas)
 				f.pop()
 				f.pop()
 				f.newLine(ChangeNil)
 				f.newLine(ChangeNil)
 				f.print("}")
 				f.print("}")
@@ -246,7 +244,6 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi
 				f.closeLine()
 				f.closeLine()
 
 
 			case *diff.Array:
 			case *diff.Array:
-				d := matchedDelta.(*diff.Array)
 				switch value.(type) {
 				switch value.(type) {
 				case []interface{}:
 				case []interface{}:
 					//ok
 					//ok
@@ -260,7 +257,7 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi
 				f.print("[")
 				f.print("[")
 				f.closeLine()
 				f.closeLine()
 				f.push(positionStr, len(a), true)
 				f.push(positionStr, len(a), true)
-				f.processArray(a, d.Deltas)
+				f.processArray(a, matchedDelta.Deltas)
 				f.pop()
 				f.pop()
 				f.newLine(ChangeNil)
 				f.newLine(ChangeNil)
 				f.print("]")
 				f.print("]")
@@ -268,27 +265,23 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi
 				f.closeLine()
 				f.closeLine()
 
 
 			case *diff.Added:
 			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]++
 				f.size[len(f.size)-1]++
 
 
 			case *diff.Modified:
 			case *diff.Modified:
-				d := matchedDelta.(*diff.Modified)
 				savedSize := f.size[len(f.size)-1]
 				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.size[len(f.size)-1] = savedSize
-				f.printRecursive(positionStr, d.NewValue, ChangeNew)
+				f.printRecursive(positionStr, matchedDelta.NewValue, ChangeNew)
 
 
 			case *diff.TextDiff:
 			case *diff.TextDiff:
 				savedSize := f.size[len(f.size)-1]
 				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.size[len(f.size)-1] = savedSize
-				f.printRecursive(positionStr, d.NewValue, ChangeNew)
+				f.printRecursive(positionStr, matchedDelta.NewValue, ChangeNew)
 
 
 			case *diff.Deleted:
 			case *diff.Deleted:
-				d := matchedDelta.(*diff.Deleted)
-				f.printRecursive(positionStr, d.Value, ChangeDeleted)
+				f.printRecursive(positionStr, matchedDelta.Value, ChangeDeleted)
 
 
 			default:
 			default:
 				return errors.New("Unknown Delta type detected")
 				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) {
 func (f *JSONFormatter) searchDeltas(deltas []diff.Delta, position diff.Position) (results []diff.Delta) {
 	results = make([]diff.Delta, 0)
 	results = make([]diff.Delta, 0)
 	for _, delta := range deltas {
 	for _, delta := range deltas {
-		switch delta.(type) {
+		switch typedDelta := delta.(type) {
 		case diff.PostDelta:
 		case diff.PostDelta:
-			if delta.(diff.PostDelta).PostPosition() == position {
+			if typedDelta.PostPosition() == position {
 				results = append(results, delta)
 				results = append(results, delta)
 			}
 			}
 		case diff.PreDelta:
 		case diff.PreDelta:
-			if delta.(diff.PreDelta).PrePosition() == position {
+			if typedDelta.PrePosition() == position {
 				results = append(results, delta)
 				results = append(results, delta)
 			}
 			}
 		default:
 		default:
@@ -417,20 +410,19 @@ func (f *JSONFormatter) print(a string) {
 }
 }
 
 
 func (f *JSONFormatter) printRecursive(name string, value interface{}, change ChangeType) {
 func (f *JSONFormatter) printRecursive(name string, value interface{}, change ChangeType) {
-	switch value.(type) {
+	switch value := value.(type) {
 	case map[string]interface{}:
 	case map[string]interface{}:
 		f.newLine(change)
 		f.newLine(change)
 		f.printKey(name)
 		f.printKey(name)
 		f.print("{")
 		f.print("{")
 		f.closeLine()
 		f.closeLine()
 
 
-		m := value.(map[string]interface{})
-		size := len(m)
+		size := len(value)
 		f.push(name, size, false)
 		f.push(name, size, false)
 
 
-		keys := sortKeys(m)
+		keys := sortKeys(value)
 		for _, key := range keys {
 		for _, key := range keys {
-			f.printRecursive(key, m[key], change)
+			f.printRecursive(key, value[key], change)
 		}
 		}
 		f.pop()
 		f.pop()
 
 
@@ -445,10 +437,9 @@ func (f *JSONFormatter) printRecursive(name string, value interface{}, change Ch
 		f.print("[")
 		f.print("[")
 		f.closeLine()
 		f.closeLine()
 
 
-		s := value.([]interface{})
-		size := len(s)
+		size := len(value)
 		f.push("", size, true)
 		f.push("", size, true)
-		for _, item := range s {
+		for _, item := range value {
 			f.printRecursive("", item, change)
 			f.printRecursive("", item, change)
 		}
 		}
 		f.pop()
 		f.pop()

+ 5 - 2
pkg/components/imguploader/imguploader.go

@@ -6,7 +6,6 @@ import (
 	"regexp"
 	"regexp"
 
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
-
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 )
 )
 
 
@@ -21,6 +20,10 @@ func (NopImageUploader) Upload(ctx context.Context, path string) (string, error)
 	return "", nil
 	return "", nil
 }
 }
 
 
+var (
+	logger = log.New("imguploader")
+)
+
 func NewImageUploader() (ImageUploader, error) {
 func NewImageUploader() (ImageUploader, error) {
 
 
 	switch setting.ImageUploadProvider {
 	switch setting.ImageUploadProvider {
@@ -94,7 +97,7 @@ func NewImageUploader() (ImageUploader, error) {
 	}
 	}
 
 
 	if setting.ImageUploadProvider != "" {
 	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
 	return NopImageUploader{}, nil

+ 2 - 20
pkg/log/log.go

@@ -10,13 +10,11 @@ import (
 	"path/filepath"
 	"path/filepath"
 	"strings"
 	"strings"
 
 
-	"gopkg.in/ini.v1"
-
 	"github.com/go-stack/stack"
 	"github.com/go-stack/stack"
+	"github.com/grafana/grafana/pkg/util"
 	"github.com/inconshreveable/log15"
 	"github.com/inconshreveable/log15"
 	isatty "github.com/mattn/go-isatty"
 	isatty "github.com/mattn/go-isatty"
-
-	"github.com/grafana/grafana/pkg/util"
+	"gopkg.in/ini.v1"
 )
 )
 
 
 var Root log15.Logger
 var Root log15.Logger
@@ -58,10 +56,6 @@ func Debug(format string, v ...interface{}) {
 	Root.Debug(message)
 	Root.Debug(message)
 }
 }
 
 
-func Debug2(message string, v ...interface{}) {
-	Root.Debug(message, v...)
-}
-
 func Info(format string, v ...interface{}) {
 func Info(format string, v ...interface{}) {
 	var message string
 	var message string
 	if len(v) > 0 {
 	if len(v) > 0 {
@@ -73,10 +67,6 @@ func Info(format string, v ...interface{}) {
 	Root.Info(message)
 	Root.Info(message)
 }
 }
 
 
-func Info2(message string, v ...interface{}) {
-	Root.Info(message, v...)
-}
-
 func Warn(format string, v ...interface{}) {
 func Warn(format string, v ...interface{}) {
 	var message string
 	var message string
 	if len(v) > 0 {
 	if len(v) > 0 {
@@ -88,18 +78,10 @@ func Warn(format string, v ...interface{}) {
 	Root.Warn(message)
 	Root.Warn(message)
 }
 }
 
 
-func Warn2(message string, v ...interface{}) {
-	Root.Warn(message, v...)
-}
-
 func Error(skip int, format string, v ...interface{}) {
 func Error(skip int, format string, v ...interface{}) {
 	Root.Error(fmt.Sprintf(format, v...))
 	Root.Error(fmt.Sprintf(format, v...))
 }
 }
 
 
-func Error2(message string, v ...interface{}) {
-	Root.Error(message, v...)
-}
-
 func Critical(skip int, format string, v ...interface{}) {
 func Critical(skip int, format string, v ...interface{}) {
 	Root.Crit(fmt.Sprintf(format, v...))
 	Root.Crit(fmt.Sprintf(format, v...))
 }
 }

+ 5 - 1
pkg/login/ext_user.go

@@ -11,6 +11,10 @@ func init() {
 	bus.AddHandler("auth", UpsertUser)
 	bus.AddHandler("auth", UpsertUser)
 }
 }
 
 
+var (
+	logger = log.New("login.ext_user")
+)
+
 func UpsertUser(cmd *m.UpsertUserCommand) error {
 func UpsertUser(cmd *m.UpsertUserCommand) error {
 	extUser := cmd.ExternalUser
 	extUser := cmd.ExternalUser
 
 
@@ -135,7 +139,7 @@ func updateUser(user *m.User, extUser *m.ExternalUserInfo) error {
 		return nil
 		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)
 	return bus.Dispatch(updateCmd)
 }
 }
 
 

+ 1 - 1
pkg/models/dashboards.go

@@ -112,7 +112,7 @@ func NewDashboard(title string) *Dashboard {
 func NewDashboardFolder(title string) *Dashboard {
 func NewDashboardFolder(title string) *Dashboard {
 	folder := NewDashboard(title)
 	folder := NewDashboard(title)
 	folder.IsFolder = true
 	folder.IsFolder = true
-	folder.Data.Set("schemaVersion", 16)
+	folder.Data.Set("schemaVersion", 17)
 	folder.Data.Set("version", 0)
 	folder.Data.Set("version", 0)
 	folder.IsFolder = true
 	folder.IsFolder = true
 	return folder
 	return folder

+ 1 - 1
pkg/services/alerting/extractor.go

@@ -112,7 +112,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
 
 
 		frequency, err := getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString())
 		frequency, err := getTimeDurationStringToSeconds(jsonAlert.Get("frequency").MustString())
 		if err != nil {
 		if err != nil {
-			return nil, ValidationError{Reason: "Could not parse frequency"}
+			return nil, ValidationError{Reason: err.Error()}
 		}
 		}
 
 
 		rawFor := jsonAlert.Get("for").MustString()
 		rawFor := jsonAlert.Get("for").MustString()

+ 1 - 1
pkg/services/alerting/notifiers/telegram.go

@@ -130,7 +130,7 @@ func (this *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.Eval
 	defer func() {
 	defer func() {
 		err := imageFile.Close()
 		err := imageFile.Close()
 		if err != nil {
 		if err != nil {
-			log.Error2("Could not close Telegram inline image.", "err", err)
+			this.log.Error("Could not close Telegram inline image.", "err", err)
 		}
 		}
 	}()
 	}()
 
 

+ 18 - 3
pkg/services/alerting/rule.go

@@ -1,16 +1,21 @@
 package alerting
 package alerting
 
 
 import (
 import (
+	"errors"
 	"fmt"
 	"fmt"
 	"regexp"
 	"regexp"
 	"strconv"
 	"strconv"
 	"time"
 	"time"
 
 
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
-
 	m "github.com/grafana/grafana/pkg/models"
 	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 {
 type Rule struct {
 	Id                  int64
 	Id                  int64
 	OrgId               int64
 	OrgId               int64
@@ -76,7 +81,7 @@ func getTimeDurationStringToSeconds(str string) (int64, error) {
 	matches := ValueFormatRegex.FindAllString(str, 1)
 	matches := ValueFormatRegex.FindAllString(str, 1)
 
 
 	if len(matches) <= 0 {
 	if len(matches) <= 0 {
-		return 0, fmt.Errorf("Frequency could not be parsed")
+		return 0, ErrFrequencyCouldNotBeParsed
 	}
 	}
 
 
 	value, err := strconv.Atoi(matches[0])
 	value, err := strconv.Atoi(matches[0])
@@ -84,6 +89,10 @@ func getTimeDurationStringToSeconds(str string) (int64, error) {
 		return 0, err
 		return 0, err
 	}
 	}
 
 
+	if value == 0 {
+		return 0, ErrFrequencyCannotBeZeroOrLess
+	}
+
 	unit := UnitFormatRegex.FindAllString(str, 1)[0]
 	unit := UnitFormatRegex.FindAllString(str, 1)[0]
 
 
 	if val, ok := unitMultiplier[unit]; ok {
 	if val, ok := unitMultiplier[unit]; ok {
@@ -101,7 +110,6 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
 	model.PanelId = ruleDef.PanelId
 	model.PanelId = ruleDef.PanelId
 	model.Name = ruleDef.Name
 	model.Name = ruleDef.Name
 	model.Message = ruleDef.Message
 	model.Message = ruleDef.Message
-	model.Frequency = ruleDef.Frequency
 	model.State = ruleDef.State
 	model.State = ruleDef.State
 	model.LastStateChange = ruleDef.NewStateDate
 	model.LastStateChange = ruleDef.NewStateDate
 	model.For = ruleDef.For
 	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.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting"))
 	model.StateChanges = ruleDef.StateChanges
 	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() {
 	for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
 		jsonModel := simplejson.NewFromAny(v)
 		jsonModel := simplejson.NewFromAny(v)
 		id, err := jsonModel.Get("id").Int64()
 		id, err := jsonModel.Get("id").Int64()

+ 60 - 20
pkg/services/alerting/rule_test.go

@@ -14,6 +14,36 @@ func (f *FakeCondition) Eval(context *EvalContext) (*ConditionResult, error) {
 	return &ConditionResult{}, nil
 	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) {
 func TestAlertRuleModel(t *testing.T) {
 	Convey("Testing alert rule", t, func() {
 	Convey("Testing alert rule", t, func() {
 
 
@@ -21,26 +51,6 @@ func TestAlertRuleModel(t *testing.T) {
 			return &FakeCondition{}, nil
 			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() {
 		Convey("should return err for empty string", func() {
 			_, err := getTimeDurationStringToSeconds("")
 			_, err := getTimeDurationStringToSeconds("")
 			So(err, ShouldNotBeNil)
 			So(err, ShouldNotBeNil)
@@ -89,5 +99,35 @@ func TestAlertRuleModel(t *testing.T) {
 				So(len(alertRule.Notifications), ShouldEqual, 2)
 				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)
+		})
 	})
 	})
 }
 }

+ 5 - 2
pkg/services/alerting/test_notification.go

@@ -18,9 +18,12 @@ type NotificationTestCommand struct {
 	Settings *simplejson.Json
 	Settings *simplejson.Json
 }
 }
 
 
+var (
+	logger = log.New("alerting.testnotification")
+)
+
 func init() {
 func init() {
 	bus.AddHandler("alerting", handleNotificationTestCommand)
 	bus.AddHandler("alerting", handleNotificationTestCommand)
-
 }
 }
 
 
 func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
 func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
@@ -35,7 +38,7 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
 	notifiers, err := InitNotifier(model)
 	notifiers, err := InitNotifier(model)
 
 
 	if err != nil {
 	if err != nil {
-		log.Error2("Failed to create notifier", "error", err.Error())
+		logger.Error("Failed to create notifier", "error", err.Error())
 		return err
 		return err
 	}
 	}
 
 

Some files were not shown because too many files changed in this diff