Przeglądaj źródła

Merge branch 'master' into react-query-editor

Torkel Ödegaard 7 lat temu
rodzic
commit
4bad76c0f4
100 zmienionych plików z 11290 dodań i 1440 usunięć
  1. 2 2
      .circleci/config.yml
  2. 19 2
      CHANGELOG.md
  3. 1 1
      devenv/dashboards.yaml
  4. 1674 0
      devenv/dev-dashboards-without-uid/panel_tests_graph.json
  5. 510 0
      devenv/dev-dashboards-without-uid/panel_tests_graph_time_regions.json
  6. 3342 0
      devenv/dev-dashboards-without-uid/panel_tests_polystat.json
  7. 545 7
      devenv/dev-dashboards/datasource_tests_elasticsearch_compare.json
  8. 6 1
      docs/sources/features/explore/index.md
  9. 1 1
      docs/sources/http_api/folder_permissions.md
  10. 2 2
      latest.json
  11. 1 1
      packages/grafana-ui/src/components/ColorPicker/SpectrumPicker.tsx
  12. 0 0
      packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss
  13. 4 3
      packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx
  14. 0 4
      packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap
  15. 1 4
      packages/grafana-ui/src/components/Portal/Portal.tsx
  16. 1 1
      packages/grafana-ui/src/components/Select/Select.tsx
  17. 1 0
      packages/grafana-ui/src/components/Select/SelectOption.test.tsx
  18. 9 3
      packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx
  19. 1 0
      packages/grafana-ui/src/components/Select/_Select.scss
  20. 126 16
      packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx
  21. 89 105
      packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx
  22. 69 66
      packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss
  23. 1 0
      packages/grafana-ui/src/components/index.scss
  24. 2 0
      packages/grafana-ui/src/index.scss
  25. 3 0
      packages/grafana-ui/src/types/panel.ts
  26. 0 0
      packages/grafana-ui/src/vendor/spectrum.css
  27. 0 0
      packages/grafana-ui/src/vendor/spectrum.js
  28. 5 2
      pkg/components/imguploader/imguploader.go
  29. 2 20
      pkg/log/log.go
  30. 5 1
      pkg/login/ext_user.go
  31. 1 1
      pkg/services/alerting/notifiers/telegram.go
  32. 5 2
      pkg/services/alerting/test_notification.go
  33. 2 1
      pkg/services/provisioning/dashboards/config_reader.go
  34. 2 1
      pkg/services/provisioning/dashboards/file_reader.go
  35. 45 4
      pkg/services/provisioning/dashboards/file_reader_test.go
  36. 1 1
      pkg/services/sqlstore/dashboard_provisioning.go
  37. 1 1
      pkg/tsdb/elasticsearch/client/models.go
  38. 2 2
      pkg/tsdb/elasticsearch/client/search_request.go
  39. 23 9
      pkg/tsdb/elasticsearch/models.go
  40. 31 8
      pkg/tsdb/elasticsearch/response_parser.go
  41. 78 0
      pkg/tsdb/elasticsearch/response_parser_test.go
  42. 53 15
      pkg/tsdb/elasticsearch/time_series_query.go
  43. 71 0
      pkg/tsdb/elasticsearch/time_series_query_test.go
  44. 27 0
      public/app/core/angular_wrappers.ts
  45. 90 0
      public/app/core/components/Select/MetricSelect.tsx
  46. 20 9
      public/app/core/directives/dropdown_typeahead.ts
  47. 22 50
      public/app/core/utils/explore.test.ts
  48. 85 22
      public/app/core/utils/explore.ts
  49. 1 0
      public/app/features/all.ts
  50. 7 1
      public/app/features/dashboard/dashgrid/PanelChrome.tsx
  51. 5 2
      public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx
  52. 0 0
      public/app/features/datasources/partials/http_settings.html
  53. 26 0
      public/app/features/datasources/settings/HttpSettingsCtrl.ts
  54. 113 788
      public/app/features/explore/Explore.tsx
  55. 61 0
      public/app/features/explore/GraphContainer.tsx
  56. 3 3
      public/app/features/explore/Logs.tsx
  57. 91 0
      public/app/features/explore/LogsContainer.tsx
  58. 1 1
      public/app/features/explore/QueryEditor.tsx
  59. 163 0
      public/app/features/explore/QueryRow.tsx
  60. 9 143
      public/app/features/explore/QueryRows.tsx
  61. 49 0
      public/app/features/explore/TableContainer.tsx
  62. 31 63
      public/app/features/explore/Wrapper.tsx
  63. 302 0
      public/app/features/explore/state/actionTypes.ts
  64. 757 0
      public/app/features/explore/state/actions.ts
  65. 462 0
      public/app/features/explore/state/reducers.ts
  66. 1 1
      public/app/features/templating/variable_srv.ts
  67. 22 5
      public/app/plugins/datasource/elasticsearch/elastic_response.ts
  68. 16 2
      public/app/plugins/datasource/elasticsearch/metric_agg.ts
  69. 26 2
      public/app/plugins/datasource/elasticsearch/partials/metric_agg.html
  70. 20 0
      public/app/plugins/datasource/elasticsearch/partials/pipeline_variables.html
  71. 45 0
      public/app/plugins/datasource/elasticsearch/pipeline_variables.ts
  72. 34 9
      public/app/plugins/datasource/elasticsearch/query_builder.ts
  73. 4 0
      public/app/plugins/datasource/elasticsearch/query_ctrl.ts
  74. 17 0
      public/app/plugins/datasource/elasticsearch/query_def.ts
  75. 66 0
      public/app/plugins/datasource/elasticsearch/specs/elastic_response.test.ts
  76. 77 0
      public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts
  77. 20 2
      public/app/plugins/datasource/elasticsearch/specs/query_def.test.ts
  78. 6 4
      public/app/plugins/datasource/prometheus/datasource.ts
  79. 73 0
      public/app/plugins/datasource/prometheus/specs/datasource.test.ts
  80. 9 22
      public/app/plugins/datasource/stackdriver/annotations_query_ctrl.ts
  81. 57 0
      public/app/plugins/datasource/stackdriver/components/Aggregations.test.tsx
  82. 94 0
      public/app/plugins/datasource/stackdriver/components/Aggregations.tsx
  83. 52 0
      public/app/plugins/datasource/stackdriver/components/AliasBy.tsx
  84. 56 0
      public/app/plugins/datasource/stackdriver/components/AlignmentPeriods.tsx
  85. 33 0
      public/app/plugins/datasource/stackdriver/components/Alignments.tsx
  86. 119 0
      public/app/plugins/datasource/stackdriver/components/AnnotationQueryEditor.tsx
  87. 44 0
      public/app/plugins/datasource/stackdriver/components/AnnotationsHelp.tsx
  88. 115 0
      public/app/plugins/datasource/stackdriver/components/Filter.tsx
  89. 115 0
      public/app/plugins/datasource/stackdriver/components/Help.tsx
  90. 195 0
      public/app/plugins/datasource/stackdriver/components/Metrics.tsx
  91. 31 0
      public/app/plugins/datasource/stackdriver/components/Project.tsx
  92. 23 0
      public/app/plugins/datasource/stackdriver/components/QueryEditor.test.tsx
  93. 206 0
      public/app/plugins/datasource/stackdriver/components/QueryEditor.tsx
  94. 8 8
      public/app/plugins/datasource/stackdriver/components/VariableQueryEditor.tsx
  95. 119 0
      public/app/plugins/datasource/stackdriver/components/__snapshots__/Aggregations.test.tsx.snap
  96. 459 0
      public/app/plugins/datasource/stackdriver/components/__snapshots__/QueryEditor.test.tsx.snap
  97. 8 14
      public/app/plugins/datasource/stackdriver/datasource.ts
  98. 2 2
      public/app/plugins/datasource/stackdriver/filter_segments.ts
  99. 38 0
      public/app/plugins/datasource/stackdriver/functions.test.ts
  100. 18 0
      public/app/plugins/datasource/stackdriver/functions.ts

+ 2 - 2
.circleci/config.yml

@@ -367,7 +367,7 @@ jobs:
           command: './scripts/build/publish.sh --enterprise'
           command: './scripts/build/publish.sh --enterprise'
       - run:
       - run:
           name: Load GPG private key
           name: Load GPG private key
-          comand: './scripts/build/load-signing-key.sh'
+          command: './scripts/build/load-signing-key.sh'
       - run:
       - run:
           name: Update Debian repository
           name: Update Debian repository
           command: './scripts/build/update_repo/update-deb.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"'
           command: './scripts/build/update_repo/update-deb.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"'
@@ -430,7 +430,7 @@ jobs:
           command: './scripts/build/publish.sh'
           command: './scripts/build/publish.sh'
       - run:
       - run:
           name: Load GPG private key
           name: Load GPG private key
-          comand: './scripts/build/load-signing-key.sh'
+          command: './scripts/build/load-signing-key.sh'
       - run:
       - run:
           name: Update Debian repository
           name: Update Debian repository
           command: './scripts/build/update_repo/update-deb.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"'
           command: './scripts/build/update_repo/update-deb.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"'

+ 19 - 2
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,17 +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)
 * **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)
-* **Docker**: Build and publish docker images for armv7 and arm64 [#14617](https://github.com/grafana/grafana/pull/14617), thx [@johanneswuerbach](https://github.com/johanneswuerbach)
+* **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)
 
 

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

+ 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.
 
 

+ 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",

+ 2 - 2
latest.json

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

+ 1 - 1
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;

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


+ 4 - 3
packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx

@@ -6,6 +6,7 @@ interface Props {
   autoHide?: boolean;
   autoHide?: boolean;
   autoHideTimeout?: number;
   autoHideTimeout?: number;
   autoHideDuration?: number;
   autoHideDuration?: number;
+  autoMaxHeight?: string;
   hideTracksWhenNotNeeded?: boolean;
   hideTracksWhenNotNeeded?: boolean;
 }
 }
 
 
@@ -18,11 +19,12 @@ export class CustomScrollbar extends PureComponent<Props> {
     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
@@ -31,13 +33,12 @@ export class CustomScrollbar extends PureComponent<Props> {
         // These autoHeightMin & autoHeightMax options affect firefox and chrome differently.
         // 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
         // Before these where set to inhert but that caused problems with cut of legends in firefox
         autoHeightMin={'0'}
         autoHeightMin={'0'}
-        autoHeightMax={'100%'}
+        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>

+ 0 - 4
packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap

@@ -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,
       }
       }
     }
     }

+ 1 - 4
packages/grafana-ui/src/components/Portal/Portal.tsx

@@ -12,10 +12,7 @@ export class Portal extends PureComponent<Props> {
 
 
   constructor(props: 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);

+ 1 - 1
packages/grafana-ui/src/components/Select/Select.tsx

@@ -61,7 +61,7 @@ interface AsyncProps {
 export const MenuList = (props: any) => {
 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>
   );
   );
 };
 };

+ 1 - 0
packages/grafana-ui/src/components/Select/SelectOption.test.tsx

@@ -4,6 +4,7 @@ import SelectOption from './SelectOption';
 import { OptionProps } from 'react-select/lib/components/Option';
 import { OptionProps } from 'react-select/lib/components/Option';
 
 
 const model: OptionProps<any> = {
 const model: OptionProps<any> = {
+  data: jest.fn(),
   cx: jest.fn(),
   cx: jest.fn(),
   clearValue: jest.fn(),
   clearValue: jest.fn(),
   getStyles: jest.fn(),
   getStyles: jest.fn(),

+ 9 - 3
packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx

@@ -2,7 +2,11 @@ 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 {
@@ -15,8 +19,10 @@ export default class SelectOptionGroup extends PureComponent<ExtendedGroupProps,
   };
   };
 
 
   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 });

+ 1 - 0
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 {

+ 126 - 16
packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx

@@ -2,7 +2,6 @@ import React from 'react';
 import { shallow } from 'enzyme';
 import { shallow } from 'enzyme';
 
 
 import { ThresholdsEditor, Props } from './ThresholdsEditor';
 import { ThresholdsEditor, Props } from './ThresholdsEditor';
-import { BasicGaugeColor } from '../../types';
 
 
 const setup = (propOverrides?: object) => {
 const setup = (propOverrides?: object) => {
   const props: Props = {
   const props: Props = {
@@ -15,49 +14,160 @@ const setup = (propOverrides?: object) => {
   return shallow(<ThresholdsEditor {...props} />).instance() as ThresholdsEditor;
   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', () => {
 describe('Add threshold', () => {
-  it('should add threshold', () => {
+  it('should not add threshold at index 0', () => {
     const instance = setup();
     const instance = setup();
 
 
     instance.onAddThreshold(0);
     instance.onAddThreshold(0);
 
 
-    expect(instance.state.thresholds).toEqual([{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }]);
+    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', () => {
   it('should add another threshold above a first', () => {
     const instance = setup({
     const instance = setup({
-      thresholds: [{ index: 0, value: 50, color: 'rgb(127, 115, 64)' }],
+      thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }],
     });
     });
 
 
-    instance.onAddThreshold(1);
+    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([
     expect(instance.state.thresholds).toEqual([
-      { index: 1, value: 75, color: 'rgb(170, 95, 61)' },
-      { index: 0, value: 50, color: 'rgb(127, 115, 64)' },
+      { 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', () => {
 describe('change threshold value', () => {
-  it('should update value and resort rows', () => {
+  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 instance = setup();
-    const mockThresholds = [
-      { index: 0, value: 50, color: 'rgba(237, 129, 40, 0.89)' },
-      { index: 1, value: 75, color: 'rgba(237, 129, 40, 0.89)' },
+    const thresholds = [
+      { index: 0, value: -Infinity, color: '#7EB26D' },
+      { index: 1, value: 50, color: '#EAB839' },
+      { index: 2, value: 75, color: '#6ED0E0' },
     ];
     ];
 
 
     instance.state = {
     instance.state = {
-      baseColor: BasicGaugeColor.Green,
-      thresholds: mockThresholds,
+      thresholds,
     };
     };
 
 
     const mockEvent = { target: { value: 78 } };
     const mockEvent = { target: { value: 78 } };
 
 
-    instance.onChangeThresholdValue(mockEvent, mockThresholds[0]);
+    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([
     expect(instance.state.thresholds).toEqual([
-      { index: 0, value: 78, color: 'rgba(237, 129, 40, 0.89)' },
-      { index: 1, value: 75, color: 'rgba(237, 129, 40, 0.89)' },
+      { index: 2, value: 78, color: '#EAB839' },
+      { index: 1, value: 75, color: '#6ED0E0' },
+      { index: 0, value: -Infinity, color: '#7EB26D' },
     ]);
     ]);
   });
   });
 });
 });

+ 89 - 105
packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx

@@ -1,9 +1,10 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
-import tinycolor, { ColorInput } from 'tinycolor2';
+// import tinycolor, { ColorInput } from 'tinycolor2';
 
 
-import { Threshold, BasicGaugeColor } from '../../types';
+import { Threshold } from '../../types';
 import { ColorPicker } from '../ColorPicker/ColorPicker';
 import { ColorPicker } from '../ColorPicker/ColorPicker';
 import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
 import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
+import { colors } from '../../utils';
 
 
 export interface Props {
 export interface Props {
   thresholds: Threshold[];
   thresholds: Threshold[];
@@ -12,50 +13,43 @@ export interface Props {
 
 
 interface State {
 interface State {
   thresholds: Threshold[];
   thresholds: Threshold[];
-  baseColor: string;
 }
 }
 
 
 export class ThresholdsEditor extends PureComponent<Props, State> {
 export class ThresholdsEditor extends PureComponent<Props, State> {
   constructor(props: Props) {
   constructor(props: Props) {
     super(props);
     super(props);
 
 
-    this.state = { thresholds: props.thresholds, baseColor: BasicGaugeColor.Green };
+    const thresholds: Threshold[] =
+      props.thresholds.length > 0 ? props.thresholds : [{ index: 0, value: -Infinity, color: colors[0] }];
+    this.state = { thresholds };
   }
   }
 
 
   onAddThreshold = (index: number) => {
   onAddThreshold = (index: number) => {
-    const maxValue = 100; // hardcoded for now before we add the base threshold
-    const minValue = 0; // hardcoded for now before we add the base threshold
     const { thresholds } = this.state;
     const { thresholds } = this.state;
+    const maxValue = 100;
+    const minValue = 0;
+
+    if (index === 0) {
+      return;
+    }
 
 
     const newThresholds = thresholds.map(threshold => {
     const newThresholds = thresholds.map(threshold => {
       if (threshold.index >= index) {
       if (threshold.index >= index) {
-        threshold = {
-          ...threshold,
-          index: threshold.index + 1,
-        };
+        const index = threshold.index + 1;
+        threshold = { ...threshold, index };
       }
       }
-
       return threshold;
       return threshold;
     });
     });
 
 
     // Setting value to a value between the previous thresholds
     // Setting value to a value between the previous thresholds
-    let value;
-
-    if (index === 0 && thresholds.length === 0) {
-      value = maxValue - (maxValue - minValue) / 2;
-    } else if (index === 0 && thresholds.length > 0) {
-      value = newThresholds[index + 1].value - (newThresholds[index + 1].value - minValue) / 2;
-    } else if (index > newThresholds[newThresholds.length - 1].index) {
-      value = maxValue - (maxValue - newThresholds[index - 1].value) / 2;
-    }
+    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 that lies between the previous thresholds
-    let color;
-    if (index === 0 && thresholds.length === 0) {
-      color = tinycolor.mix(BasicGaugeColor.Green, BasicGaugeColor.Red, 50).toRgbString();
-    } else {
-      color = tinycolor.mix(thresholds[index - 1].color as ColorInput, BasicGaugeColor.Red, 50).toRgbString();
-    }
+    // Set a color
+    const color = colors.filter(c => newThresholds.some(t => t.color === c) === false)[0];
 
 
     this.setState(
     this.setState(
       {
       {
@@ -73,18 +67,40 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
   };
   };
 
 
   onRemoveThreshold = (threshold: Threshold) => {
   onRemoveThreshold = (threshold: Threshold) => {
+    if (threshold.index === 0) {
+      return;
+    }
+
     this.setState(
     this.setState(
-      prevState => ({ thresholds: prevState.thresholds.filter(t => t !== threshold) }),
+      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()
       () => this.updateGauge()
     );
     );
   };
   };
 
 
   onChangeThresholdValue = (event: any, threshold: Threshold) => {
   onChangeThresholdValue = (event: any, threshold: Threshold) => {
+    if (threshold.index === 0) {
+      return;
+    }
+
     const { thresholds } = this.state;
     const { thresholds } = this.state;
+    const parsedValue = parseInt(event.target.value, 10);
+    const value = isNaN(parsedValue) ? null : parsedValue;
 
 
     const newThresholds = thresholds.map(t => {
     const newThresholds = thresholds.map(t => {
       if (t === threshold) {
       if (t === threshold) {
-        t = { ...t, value: event.target.value };
+        t = { ...t, value: value as number };
       }
       }
 
 
       return t;
       return t;
@@ -114,7 +130,14 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
 
 
   onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds);
   onChangeBaseColor = (color: string) => this.props.onChange(this.state.thresholds);
   onBlur = () => {
   onBlur = () => {
-    this.setState(prevState => ({ thresholds: this.sortThresholds(prevState.thresholds) }));
+    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();
     this.updateGauge();
   };
   };
@@ -129,92 +152,53 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
     });
     });
   };
   };
 
 
-  renderThresholds() {
-    const { thresholds } = this.state;
-
-    return thresholds.map((threshold, index) => {
-      return (
-        <div className="threshold-row" key={`${threshold.index}-${index}`}>
-          <div className="threshold-row-inner">
-            <div className="threshold-row-color">
-              {threshold.color && (
-                <div className="threshold-row-color-inner">
-                  <ColorPicker
-                    color={threshold.color}
-                    onChange={color => this.onChangeThresholdColor(threshold, color)}
-                  />
-                </div>
-              )}
-            </div>
-            <input
-              className="threshold-row-input"
-              type="text"
-              onChange={event => this.onChangeThresholdValue(event, threshold)}
-              value={threshold.value}
-              onBlur={this.onBlur}
-            />
-            <div onClick={() => this.onRemoveThreshold(threshold)} className="threshold-row-remove">
-              <i className="fa fa-times" />
+  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>
+          )}
         </div>
         </div>
-      );
-    });
-  }
-
-  renderIndicator() {
-    const { thresholds } = this.state;
-
-    return thresholds.map((t, i) => {
-      return (
-        <div key={`${t.value}-${i}`} className="indicator-section">
-          <div onClick={() => this.onAddThreshold(t.index + 1)} style={{ height: '50%', backgroundColor: t.color }} />
-          <div onClick={() => this.onAddThreshold(t.index)} style={{ height: '50%', backgroundColor: t.color }} />
+        <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>
         </div>
-      );
-    });
-  }
-
-  renderBaseIndicator() {
-    return (
-      <div className="indicator-section" style={{ height: '100%' }}>
-        <div
-          onClick={() => this.onAddThreshold(0)}
-          style={{ height: '100%', backgroundColor: BasicGaugeColor.Green }}
-        />
-      </div>
-    );
-  }
-
-  renderBase() {
-    const baseColor = BasicGaugeColor.Green;
-
-    return (
-      <div className="threshold-row threshold-row-base">
-        <div className="threshold-row-inner threshold-row-inner--base">
-          <div className="threshold-row-color">
-            <div className="threshold-row-color-inner">
-              <ColorPicker color={baseColor} onChange={color => this.onChangeBaseColor(color)} />
-            </div>
+        {threshold.index > 0 && (
+          <div className="thresholds-row-input-inner-remove" onClick={() => this.onRemoveThreshold(threshold)}>
+            <i className="fa fa-times" />
           </div>
           </div>
-          <div className="threshold-row-label">Base</div>
-        </div>
+        )}
       </div>
       </div>
     );
     );
-  }
+  };
 
 
   render() {
   render() {
+    const { thresholds } = this.state;
+
     return (
     return (
       <PanelOptionsGroup title="Thresholds">
       <PanelOptionsGroup title="Thresholds">
         <div className="thresholds">
         <div className="thresholds">
-          <div className="color-indicators">
-            {this.renderIndicator()}
-            {this.renderBaseIndicator()}
-          </div>
-          <div className="threshold-rows">
-            {this.renderThresholds()}
-            {this.renderBase()}
-          </div>
+          {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>
         </div>
       </PanelOptionsGroup>
       </PanelOptionsGroup>
     );
     );

+ 69 - 66
packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss

@@ -1,103 +1,106 @@
 .thresholds {
 .thresholds {
+  margin-bottom: 10px;
+}
+
+.thresholds-row {
   display: flex;
   display: flex;
+  flex-direction: row;
+  height: 70px;
 }
 }
 
 
-.threshold-rows {
-  margin-left: 5px;
+.thresholds-row:last-child > .thresholds-row-color-indicator {
+  border-bottom-left-radius: $border-radius;
+  border-bottom-right-radius: $border-radius;
+  overflow: hidden;
 }
 }
 
 
-.threshold-row {
+.thresholds-row-add-button {
+  align-self: center;
+  margin-right: 5px;
+  color: $green;
+  height: 24px;
+  width: 24px;
+  background-color: $green;
+  border-radius: 50%;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
-  margin-top: 3px;
-  padding: 5px;
+  justify-content: center;
+  cursor: pointer;
+}
 
 
-  &::before {
-    font-family: 'FontAwesome';
-    content: '\f0d9';
-    color: $input-label-border-color;
-  }
+.thresholds-row-add-button > i {
+  color: $white;
 }
 }
 
 
-.threshold-row-inner {
-  border: 1px solid $input-label-border-color;
-  border-radius: $border-radius;
-  display: flex;
-  overflow: hidden;
-  height: 37px;
+.thresholds-row-color-indicator {
+  width: 20px;
+}
 
 
-  &--base {
-    width: auto;
-  }
+.thresholds-row-input {
+  margin-top: 49px;
+  margin-left: 2px;
 }
 }
 
 
-.threshold-row-color {
-  width: 36px;
-  border-right: 1px solid $input-label-border-color;
+.thresholds-row-input-inner {
   display: flex;
   display: flex;
-  align-items: center;
   justify-content: center;
   justify-content: center;
-  background-color: $input-bg;
+  flex-direction: row;
+  height: 42px;
 }
 }
 
 
-.threshold-row-color-inner {
-  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 > div {
+  border-left: 1px solid $input-label-border-color;
+  border-top: 1px solid $input-label-border-color;
+  border-bottom: 1px solid $input-label-border-color;
 }
 }
 
 
-.threshold-row-input {
-  padding: 8px 10px;
-  width: 150px;
+.thresholds-row-input-inner > *:nth-child(2) {
+  border-top-left-radius: $border-radius;
+  border-bottom-left-radius: $border-radius;
 }
 }
 
 
-.threshold-row-label {
-  background-color: $input-label-bg;
-  padding: 5px;
-  display: flex;
-  align-items: center;
+.thresholds-row-input-inner > *:last-child {
+  border-top-right-radius: $border-radius;
+  border-bottom-right-radius: $border-radius;
 }
 }
 
 
-.threshold-row-add-label {
-  align-items: center;
-  display: flex;
-  padding: 5px 8px;
+.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;
 }
 }
 
 
-.threshold-row-remove {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  height: 37px;
-  width: 37px;
-  cursor: pointer;
+.thresholds-row-input-inner-value > input {
+  height: 100%;
+  padding: 8px 10px;
+  width: 150px;
 }
 }
 
 
-.threshold-row-add {
-  border-right: $border-width solid $input-label-border-color;
+.thresholds-row-input-inner-color {
+  width: 42px;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
-  width: 36px;
-  background-color: $green;
+  background-color: $input-bg;
 }
 }
 
 
-.threshold-row-label {
-  border-top-left-radius: 0;
-  border-bottom-left-radius: 0;
+.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);
 }
 }
 
 
-.indicator-section {
-  width: 100%;
-  height: 50px;
+.thresholds-row-input-inner-remove {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 42px;
+  width: 42px;
+  background-color: $input-label-border-color;
   cursor: pointer;
   cursor: pointer;
 }
 }
-
-.color-indicators {
-  width: 15px;
-  border-bottom-left-radius: $border-radius;
-  border-bottom-right-radius: $border-radius;
-  overflow: hidden;
-}

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

@@ -5,3 +5,4 @@
 @import 'Select/Select';
 @import 'Select/Select';
 @import 'PanelOptionsGroup/PanelOptionsGroup';
 @import 'PanelOptionsGroup/PanelOptionsGroup';
 @import 'PanelOptionsGrid/PanelOptionsGrid';
 @import 'PanelOptionsGrid/PanelOptionsGrid';
+@import 'ColorPicker/ColorPicker';

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

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

+ 3 - 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> {

+ 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


+ 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/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)
 		}
 		}
 	}()
 	}()
 
 

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

+ 2 - 1
pkg/services/provisioning/dashboards/config_reader.go

@@ -1,6 +1,7 @@
 package dashboards
 package dashboards
 
 
 import (
 import (
+	"fmt"
 	"io/ioutil"
 	"io/ioutil"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
@@ -69,7 +70,7 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
 
 
 		parsedDashboards, err := cr.parseConfigs(file)
 		parsedDashboards, err := cr.parseConfigs(file)
 		if err != nil {
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("could not parse provisioning config file: %s error: %v", file.Name(), err)
 		}
 		}
 
 
 		if len(parsedDashboards) > 0 {
 		if len(parsedDashboards) > 0 {

+ 2 - 1
pkg/services/provisioning/dashboards/file_reader.go

@@ -118,6 +118,7 @@ func (fr *fileReader) startWalkingDisk() error {
 
 
 	return nil
 	return nil
 }
 }
+
 func (fr *fileReader) deleteDashboardIfFileIsMissing(provisionedDashboardRefs map[string]*models.DashboardProvisioning, filesFoundOnDisk map[string]os.FileInfo) {
 func (fr *fileReader) deleteDashboardIfFileIsMissing(provisionedDashboardRefs map[string]*models.DashboardProvisioning, filesFoundOnDisk map[string]os.FileInfo) {
 	if fr.Cfg.DisableDeletion {
 	if fr.Cfg.DisableDeletion {
 		return
 		return
@@ -180,7 +181,7 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil
 		dash.Dashboard.SetId(provisionedData.DashboardId)
 		dash.Dashboard.SetId(provisionedData.DashboardId)
 	}
 	}
 
 
-	fr.log.Debug("saving new dashboard", "file", path)
+	fr.log.Debug("saving new dashboard", "provisoner", fr.Cfg.Name, "file", path, "folderId", dash.Dashboard.FolderId)
 	dp := &models.DashboardProvisioning{
 	dp := &models.DashboardProvisioning{
 		ExternalId: path,
 		ExternalId: path,
 		Name:       fr.Cfg.Name,
 		Name:       fr.Cfg.Name,

+ 45 - 4
pkg/services/provisioning/dashboards/file_reader_test.go

@@ -166,6 +166,36 @@ func TestDashboardFileReader(t *testing.T) {
 				_, err := NewDashboardFileReader(cfg, logger)
 				_, err := NewDashboardFileReader(cfg, logger)
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 			})
 			})
+
+			Convey("Two dashboard providers should be able to provisioned the same dashboard without uid", func() {
+				cfg1 := &DashboardsAsConfig{Name: "1", Type: "file", OrgId: 1, Folder: "f1", Options: map[string]interface{}{"path": containingId}}
+				cfg2 := &DashboardsAsConfig{Name: "2", Type: "file", OrgId: 1, Folder: "f2", Options: map[string]interface{}{"path": containingId}}
+
+				reader1, err := NewDashboardFileReader(cfg1, logger)
+				So(err, ShouldBeNil)
+
+				err = reader1.startWalkingDisk()
+				So(err, ShouldBeNil)
+
+				reader2, err := NewDashboardFileReader(cfg2, logger)
+				So(err, ShouldBeNil)
+
+				err = reader2.startWalkingDisk()
+				So(err, ShouldBeNil)
+
+				var folderCount int
+				var dashCount int
+				for _, o := range fakeService.inserted {
+					if o.Dashboard.IsFolder {
+						folderCount++
+					} else {
+						dashCount++
+					}
+				}
+
+				So(folderCount, ShouldEqual, 2)
+				So(dashCount, ShouldEqual, 2)
+			})
 		})
 		})
 
 
 		Convey("Should not create new folder if folder name is missing", func() {
 		Convey("Should not create new folder if folder name is missing", func() {
@@ -256,7 +286,9 @@ func (ffi FakeFileInfo) Sys() interface{} {
 }
 }
 
 
 func mockDashboardProvisioningService() *fakeDashboardProvisioningService {
 func mockDashboardProvisioningService() *fakeDashboardProvisioningService {
-	mock := fakeDashboardProvisioningService{}
+	mock := fakeDashboardProvisioningService{
+		provisioned: map[string][]*models.DashboardProvisioning{},
+	}
 	dashboards.NewProvisioningService = func() dashboards.DashboardProvisioningService {
 	dashboards.NewProvisioningService = func() dashboards.DashboardProvisioningService {
 		return &mock
 		return &mock
 	}
 	}
@@ -265,17 +297,26 @@ func mockDashboardProvisioningService() *fakeDashboardProvisioningService {
 
 
 type fakeDashboardProvisioningService struct {
 type fakeDashboardProvisioningService struct {
 	inserted     []*dashboards.SaveDashboardDTO
 	inserted     []*dashboards.SaveDashboardDTO
-	provisioned  []*models.DashboardProvisioning
+	provisioned  map[string][]*models.DashboardProvisioning
 	getDashboard []*models.Dashboard
 	getDashboard []*models.Dashboard
 }
 }
 
 
 func (s *fakeDashboardProvisioningService) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
 func (s *fakeDashboardProvisioningService) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
-	return s.provisioned, nil
+	if _, ok := s.provisioned[name]; !ok {
+		s.provisioned[name] = []*models.DashboardProvisioning{}
+	}
+
+	return s.provisioned[name], nil
 }
 }
 
 
 func (s *fakeDashboardProvisioningService) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
 func (s *fakeDashboardProvisioningService) SaveProvisionedDashboard(dto *dashboards.SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) {
 	s.inserted = append(s.inserted, dto)
 	s.inserted = append(s.inserted, dto)
-	s.provisioned = append(s.provisioned, provisioning)
+
+	if _, ok := s.provisioned[provisioning.Name]; !ok {
+		s.provisioned[provisioning.Name] = []*models.DashboardProvisioning{}
+	}
+
+	s.provisioned[provisioning.Name] = append(s.provisioned[provisioning.Name], provisioning)
 	return dto.Dashboard, nil
 	return dto.Dashboard, nil
 }
 }
 
 

+ 1 - 1
pkg/services/sqlstore/dashboard_provisioning.go

@@ -51,7 +51,7 @@ func SaveProvisionedDashboard(cmd *models.SaveProvisionedDashboardCommand) error
 func saveProvionedData(sess *DBSession, cmd *models.DashboardProvisioning, dashboard *models.Dashboard) error {
 func saveProvionedData(sess *DBSession, cmd *models.DashboardProvisioning, dashboard *models.Dashboard) error {
 	result := &models.DashboardProvisioning{}
 	result := &models.DashboardProvisioning{}
 
 
-	exist, err := sess.Where("dashboard_id=?", dashboard.Id).Get(result)
+	exist, err := sess.Where("dashboard_id=? AND name = ?", dashboard.Id, cmd.Name).Get(result)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}

+ 1 - 1
pkg/tsdb/elasticsearch/client/models.go

@@ -292,7 +292,7 @@ func (a *MetricAggregation) MarshalJSON() ([]byte, error) {
 
 
 // PipelineAggregation represents a metric aggregation
 // PipelineAggregation represents a metric aggregation
 type PipelineAggregation struct {
 type PipelineAggregation struct {
-	BucketPath string
+	BucketPath interface{}
 	Settings   map[string]interface{}
 	Settings   map[string]interface{}
 }
 }
 
 

+ 2 - 2
pkg/tsdb/elasticsearch/client/search_request.go

@@ -268,7 +268,7 @@ type AggBuilder interface {
 	Filters(key string, fn func(a *FiltersAggregation, b AggBuilder)) AggBuilder
 	Filters(key string, fn func(a *FiltersAggregation, b AggBuilder)) AggBuilder
 	GeoHashGrid(key, field string, fn func(a *GeoHashGridAggregation, b AggBuilder)) AggBuilder
 	GeoHashGrid(key, field string, fn func(a *GeoHashGridAggregation, b AggBuilder)) AggBuilder
 	Metric(key, metricType, field string, fn func(a *MetricAggregation)) AggBuilder
 	Metric(key, metricType, field string, fn func(a *MetricAggregation)) AggBuilder
-	Pipeline(key, pipelineType, bucketPath string, fn func(a *PipelineAggregation)) AggBuilder
+	Pipeline(key, pipelineType string, bucketPath interface{}, fn func(a *PipelineAggregation)) AggBuilder
 	Build() (AggArray, error)
 	Build() (AggArray, error)
 }
 }
 
 
@@ -438,7 +438,7 @@ func (b *aggBuilderImpl) Metric(key, metricType, field string, fn func(a *Metric
 	return b
 	return b
 }
 }
 
 
-func (b *aggBuilderImpl) Pipeline(key, pipelineType, bucketPath string, fn func(a *PipelineAggregation)) AggBuilder {
+func (b *aggBuilderImpl) Pipeline(key, pipelineType string, bucketPath interface{}, fn func(a *PipelineAggregation)) AggBuilder {
 	innerAgg := &PipelineAggregation{
 	innerAgg := &PipelineAggregation{
 		BucketPath: bucketPath,
 		BucketPath: bucketPath,
 		Settings:   make(map[string]interface{}),
 		Settings:   make(map[string]interface{}),

+ 23 - 9
pkg/tsdb/elasticsearch/models.go

@@ -25,13 +25,14 @@ type BucketAgg struct {
 
 
 // MetricAgg represents a metric aggregation of the time series query model of the datasource
 // MetricAgg represents a metric aggregation of the time series query model of the datasource
 type MetricAgg struct {
 type MetricAgg struct {
-	Field             string           `json:"field"`
-	Hide              bool             `json:"hide"`
-	ID                string           `json:"id"`
-	PipelineAggregate string           `json:"pipelineAgg"`
-	Settings          *simplejson.Json `json:"settings"`
-	Meta              *simplejson.Json `json:"meta"`
-	Type              string           `json:"type"`
+	Field             string            `json:"field"`
+	Hide              bool              `json:"hide"`
+	ID                string            `json:"id"`
+	PipelineAggregate string            `json:"pipelineAgg"`
+	PipelineVariables map[string]string `json:"pipelineVariables"`
+	Settings          *simplejson.Json  `json:"settings"`
+	Meta              *simplejson.Json  `json:"meta"`
+	Type              string            `json:"type"`
 }
 }
 
 
 var metricAggType = map[string]string{
 var metricAggType = map[string]string{
@@ -45,6 +46,7 @@ var metricAggType = map[string]string{
 	"cardinality":    "Unique Count",
 	"cardinality":    "Unique Count",
 	"moving_avg":     "Moving Average",
 	"moving_avg":     "Moving Average",
 	"derivative":     "Derivative",
 	"derivative":     "Derivative",
+	"bucket_script":  "Bucket Script",
 	"raw_document":   "Raw Document",
 	"raw_document":   "Raw Document",
 }
 }
 
 
@@ -60,8 +62,13 @@ var extendedStats = map[string]string{
 }
 }
 
 
 var pipelineAggType = map[string]string{
 var pipelineAggType = map[string]string{
-	"moving_avg": "moving_avg",
-	"derivative": "derivative",
+	"moving_avg":    "moving_avg",
+	"derivative":    "derivative",
+	"bucket_script": "bucket_script",
+}
+
+var pipelineAggWithMultipleBucketPathsType = map[string]string{
+	"bucket_script": "bucket_script",
 }
 }
 
 
 func isPipelineAgg(metricType string) bool {
 func isPipelineAgg(metricType string) bool {
@@ -71,6 +78,13 @@ func isPipelineAgg(metricType string) bool {
 	return false
 	return false
 }
 }
 
 
+func isPipelineAggWithMultipleBucketPaths(metricType string) bool {
+	if _, ok := pipelineAggWithMultipleBucketPathsType[metricType]; ok {
+		return true
+	}
+	return false
+}
+
 func describeMetric(metricType, field string) string {
 func describeMetric(metricType, field string) string {
 	text := metricAggType[metricType]
 	text := metricAggType[metricType]
 	if metricType == countType {
 	if metricType == countType {

+ 31 - 8
pkg/tsdb/elasticsearch/response_parser.go

@@ -260,6 +260,7 @@ func (rp *responseParser) processMetrics(esAgg *simplejson.Json, target *Query,
 
 
 			newSeries.Tags["metric"] = metric.Type
 			newSeries.Tags["metric"] = metric.Type
 			newSeries.Tags["field"] = metric.Field
 			newSeries.Tags["field"] = metric.Field
+			newSeries.Tags["metricId"] = metric.ID
 			for _, v := range esAgg.Get("buckets").MustArray() {
 			for _, v := range esAgg.Get("buckets").MustArray() {
 				bucket := simplejson.NewFromAny(v)
 				bucket := simplejson.NewFromAny(v)
 				key := castToNullFloat(bucket.Get("key"))
 				key := castToNullFloat(bucket.Get("key"))
@@ -459,20 +460,42 @@ func (rp *responseParser) getSeriesName(series *tsdb.TimeSeries, target *Query,
 	}
 	}
 	// todo, if field and pipelineAgg
 	// todo, if field and pipelineAgg
 	if field != "" && isPipelineAgg(metricType) {
 	if field != "" && isPipelineAgg(metricType) {
-		found := false
-		for _, metric := range target.Metrics {
-			if metric.ID == field {
-				metricName += " " + describeMetric(metric.Type, field)
-				found = true
+		if isPipelineAggWithMultipleBucketPaths(metricType) {
+			metricID := ""
+			if v, ok := series.Tags["metricId"]; ok {
+				metricID = v
+			}
+
+			for _, metric := range target.Metrics {
+				if metric.ID == metricID {
+					metricName = metric.Settings.Get("script").MustString()
+					for name, pipelineAgg := range metric.PipelineVariables {
+						for _, m := range target.Metrics {
+							if m.ID == pipelineAgg {
+								metricName = strings.Replace(metricName, "params."+name, describeMetric(m.Type, m.Field), -1)
+							}
+						}
+					}
+				}
+			}
+		} else {
+			found := false
+			for _, metric := range target.Metrics {
+				if metric.ID == field {
+					metricName += " " + describeMetric(metric.Type, field)
+					found = true
+				}
+			}
+			if !found {
+				metricName = "Unset"
 			}
 			}
-		}
-		if !found {
-			metricName = "Unset"
 		}
 		}
 	} else if field != "" {
 	} else if field != "" {
 		metricName += " " + field
 		metricName += " " + field
 	}
 	}
 
 
+	delete(series.Tags, "metricId")
+
 	if len(series.Tags) == 0 {
 	if len(series.Tags) == 0 {
 		return metricName
 		return metricName
 	}
 	}

+ 78 - 0
pkg/tsdb/elasticsearch/response_parser_test.go

@@ -787,6 +787,84 @@ func TestResponseParser(t *testing.T) {
 			So(rows[0][2].(null.Float).Float64, ShouldEqual, 3000)
 			So(rows[0][2].(null.Float).Float64, ShouldEqual, 3000)
 		})
 		})
 
 
+		Convey("With bucket_script", func() {
+			targets := map[string]string{
+				"A": `{
+					"timeField": "@timestamp",
+					"metrics": [
+						{ "id": "1", "type": "sum", "field": "@value" },
+            { "id": "3", "type": "max", "field": "@value" },
+            {
+              "id": "4",
+              "field": "select field",
+              "pipelineVariables": [{ "name": "var1", "pipelineAgg": "1" }, { "name": "var2", "pipelineAgg": "3" }],
+              "settings": { "script": "params.var1 * params.var2" },
+              "type": "bucket_script"
+            }
+					],
+          "bucketAggs": [{ "type": "date_histogram", "field": "@timestamp", "id": "2" }]
+				}`,
+			}
+			response := `{
+        "responses": [
+          {
+            "aggregations": {
+              "2": {
+                "buckets": [
+                  {
+                    "1": { "value": 2 },
+                    "3": { "value": 3 },
+                    "4": { "value": 6 },
+                    "doc_count": 60,
+                    "key": 1000
+                  },
+                  {
+                    "1": { "value": 3 },
+                    "3": { "value": 4 },
+                    "4": { "value": 12 },
+                    "doc_count": 60,
+                    "key": 2000
+                  }
+                ]
+              }
+            }
+          }
+        ]
+			}`
+			rp, err := newResponseParserForTest(targets, response)
+			So(err, ShouldBeNil)
+			result, err := rp.getTimeSeries()
+			So(err, ShouldBeNil)
+			So(result.Results, ShouldHaveLength, 1)
+
+			queryRes := result.Results["A"]
+			So(queryRes, ShouldNotBeNil)
+			So(queryRes.Series, ShouldHaveLength, 3)
+			seriesOne := queryRes.Series[0]
+			So(seriesOne.Name, ShouldEqual, "Sum @value")
+			So(seriesOne.Points, ShouldHaveLength, 2)
+			So(seriesOne.Points[0][0].Float64, ShouldEqual, 2)
+			So(seriesOne.Points[0][1].Float64, ShouldEqual, 1000)
+			So(seriesOne.Points[1][0].Float64, ShouldEqual, 3)
+			So(seriesOne.Points[1][1].Float64, ShouldEqual, 2000)
+
+			seriesTwo := queryRes.Series[1]
+			So(seriesTwo.Name, ShouldEqual, "Max @value")
+			So(seriesTwo.Points, ShouldHaveLength, 2)
+			So(seriesTwo.Points[0][0].Float64, ShouldEqual, 3)
+			So(seriesTwo.Points[0][1].Float64, ShouldEqual, 1000)
+			So(seriesTwo.Points[1][0].Float64, ShouldEqual, 4)
+			So(seriesTwo.Points[1][1].Float64, ShouldEqual, 2000)
+
+			seriesThree := queryRes.Series[2]
+			So(seriesThree.Name, ShouldEqual, "Sum @value * Max @value")
+			So(seriesThree.Points, ShouldHaveLength, 2)
+			So(seriesThree.Points[0][0].Float64, ShouldEqual, 6)
+			So(seriesThree.Points[0][1].Float64, ShouldEqual, 1000)
+			So(seriesThree.Points[1][0].Float64, ShouldEqual, 12)
+			So(seriesThree.Points[1][1].Float64, ShouldEqual, 2000)
+		})
+
 		// Convey("Raw documents query", func() {
 		// Convey("Raw documents query", func() {
 		// 	targets := map[string]string{
 		// 	targets := map[string]string{
 		// 		"A": `{
 		// 		"A": `{

+ 53 - 15
pkg/tsdb/elasticsearch/time_series_query.go

@@ -94,26 +94,56 @@ func (e *timeSeriesQuery) execute() (*tsdb.Response, error) {
 			}
 			}
 
 
 			if isPipelineAgg(m.Type) {
 			if isPipelineAgg(m.Type) {
-				if _, err := strconv.Atoi(m.PipelineAggregate); err == nil {
-					var appliedAgg *MetricAgg
-					for _, pipelineMetric := range q.Metrics {
-						if pipelineMetric.ID == m.PipelineAggregate {
-							appliedAgg = pipelineMetric
-							break
-						}
-					}
-					if appliedAgg != nil {
-						bucketPath := m.PipelineAggregate
-						if appliedAgg.Type == countType {
-							bucketPath = "_count"
+				if isPipelineAggWithMultipleBucketPaths(m.Type) {
+					if len(m.PipelineVariables) > 0 {
+						bucketPaths := map[string]interface{}{}
+						for name, pipelineAgg := range m.PipelineVariables {
+							if _, err := strconv.Atoi(pipelineAgg); err == nil {
+								var appliedAgg *MetricAgg
+								for _, pipelineMetric := range q.Metrics {
+									if pipelineMetric.ID == pipelineAgg {
+										appliedAgg = pipelineMetric
+										break
+									}
+								}
+								if appliedAgg != nil {
+									if appliedAgg.Type == countType {
+										bucketPaths[name] = "_count"
+									} else {
+										bucketPaths[name] = pipelineAgg
+									}
+								}
+							}
 						}
 						}
 
 
-						aggBuilder.Pipeline(m.ID, m.Type, bucketPath, func(a *es.PipelineAggregation) {
+						aggBuilder.Pipeline(m.ID, m.Type, bucketPaths, func(a *es.PipelineAggregation) {
 							a.Settings = m.Settings.MustMap()
 							a.Settings = m.Settings.MustMap()
 						})
 						})
+					} else {
+						continue
 					}
 					}
 				} else {
 				} else {
-					continue
+					if _, err := strconv.Atoi(m.PipelineAggregate); err == nil {
+						var appliedAgg *MetricAgg
+						for _, pipelineMetric := range q.Metrics {
+							if pipelineMetric.ID == m.PipelineAggregate {
+								appliedAgg = pipelineMetric
+								break
+							}
+						}
+						if appliedAgg != nil {
+							bucketPath := m.PipelineAggregate
+							if appliedAgg.Type == countType {
+								bucketPath = "_count"
+							}
+
+							aggBuilder.Pipeline(m.ID, m.Type, bucketPath, func(a *es.PipelineAggregation) {
+								a.Settings = m.Settings.MustMap()
+							})
+						}
+					} else {
+						continue
+					}
 				}
 				}
 			} else {
 			} else {
 				aggBuilder.Metric(m.ID, m.Type, m.Field, func(a *es.MetricAggregation) {
 				aggBuilder.Metric(m.ID, m.Type, m.Field, func(a *es.MetricAggregation) {
@@ -328,12 +358,20 @@ func (p *timeSeriesQueryParser) parseMetrics(model *simplejson.Json) ([]*MetricA
 		metric.PipelineAggregate = metricJSON.Get("pipelineAgg").MustString()
 		metric.PipelineAggregate = metricJSON.Get("pipelineAgg").MustString()
 		metric.Settings = simplejson.NewFromAny(metricJSON.Get("settings").MustMap())
 		metric.Settings = simplejson.NewFromAny(metricJSON.Get("settings").MustMap())
 		metric.Meta = simplejson.NewFromAny(metricJSON.Get("meta").MustMap())
 		metric.Meta = simplejson.NewFromAny(metricJSON.Get("meta").MustMap())
-
 		metric.Type, err = metricJSON.Get("type").String()
 		metric.Type, err = metricJSON.Get("type").String()
 		if err != nil {
 		if err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
 
 
+		if isPipelineAggWithMultipleBucketPaths(metric.Type) {
+			metric.PipelineVariables = map[string]string{}
+			pvArr := metricJSON.Get("pipelineVariables").MustArray()
+			for _, v := range pvArr {
+				kv := v.(map[string]interface{})
+				metric.PipelineVariables[kv["name"].(string)] = kv["pipelineAgg"].(string)
+			}
+		}
+
 		result = append(result, metric)
 		result = append(result, metric)
 	}
 	}
 	return result, nil
 	return result, nil

+ 71 - 0
pkg/tsdb/elasticsearch/time_series_query_test.go

@@ -543,6 +543,77 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
 			plAgg := derivativeAgg.Aggregation.Aggregation.(*es.PipelineAggregation)
 			plAgg := derivativeAgg.Aggregation.Aggregation.(*es.PipelineAggregation)
 			So(plAgg.BucketPath, ShouldEqual, "_count")
 			So(plAgg.BucketPath, ShouldEqual, "_count")
 		})
 		})
+
+		Convey("With bucket_script", func() {
+			c := newFakeClient(5)
+			_, err := executeTsdbQuery(c, `{
+				"timeField": "@timestamp",
+				"bucketAggs": [
+					{ "type": "date_histogram", "field": "@timestamp", "id": "4" }
+				],
+				"metrics": [
+					{ "id": "3", "type": "sum", "field": "@value" },
+					{ "id": "5", "type": "max", "field": "@value" },
+					{
+						"id": "2",
+						"type": "bucket_script",
+						"pipelineVariables": [
+							{ "name": "var1", "pipelineAgg": "3" },
+							{ "name": "var2", "pipelineAgg": "5" }
+						],
+						"settings": { "script": "params.var1 * params.var2" }
+					}
+				]
+			}`, from, to, 15*time.Second)
+			So(err, ShouldBeNil)
+			sr := c.multisearchRequests[0].Requests[0]
+
+			firstLevel := sr.Aggs[0]
+			So(firstLevel.Key, ShouldEqual, "4")
+			So(firstLevel.Aggregation.Type, ShouldEqual, "date_histogram")
+
+			bucketScriptAgg := firstLevel.Aggregation.Aggs[2]
+			So(bucketScriptAgg.Key, ShouldEqual, "2")
+			plAgg := bucketScriptAgg.Aggregation.Aggregation.(*es.PipelineAggregation)
+			So(plAgg.BucketPath.(map[string]interface{}), ShouldResemble, map[string]interface{}{
+				"var1": "3",
+				"var2": "5",
+			})
+		})
+
+		Convey("With bucket_script doc count", func() {
+			c := newFakeClient(5)
+			_, err := executeTsdbQuery(c, `{
+				"timeField": "@timestamp",
+				"bucketAggs": [
+					{ "type": "date_histogram", "field": "@timestamp", "id": "4" }
+				],
+				"metrics": [
+					{ "id": "3", "type": "count", "field": "select field" },
+					{
+						"id": "2",
+						"type": "bucket_script",
+						"pipelineVariables": [
+							{ "name": "var1", "pipelineAgg": "3" }
+						],
+						"settings": { "script": "params.var1 * 1000" }
+					}
+				]
+			}`, from, to, 15*time.Second)
+			So(err, ShouldBeNil)
+			sr := c.multisearchRequests[0].Requests[0]
+
+			firstLevel := sr.Aggs[0]
+			So(firstLevel.Key, ShouldEqual, "4")
+			So(firstLevel.Aggregation.Type, ShouldEqual, "date_histogram")
+
+			bucketScriptAgg := firstLevel.Aggregation.Aggs[0]
+			So(bucketScriptAgg.Key, ShouldEqual, "2")
+			plAgg := bucketScriptAgg.Aggregation.Aggregation.(*es.PipelineAggregation)
+			So(plAgg.BucketPath.(map[string]interface{}), ShouldResemble, map[string]interface{}{
+				"var1": "_count",
+			})
+		})
 	})
 	})
 }
 }
 
 

+ 27 - 0
public/app/core/angular_wrappers.ts

@@ -1,10 +1,13 @@
 import { react2AngularDirective } from 'app/core/utils/react2angular';
 import { react2AngularDirective } from 'app/core/utils/react2angular';
+import { QueryEditor as StackdriverQueryEditor } from 'app/plugins/datasource/stackdriver/components/QueryEditor';
+import { AnnotationQueryEditor as StackdriverAnnotationQueryEditor } from 'app/plugins/datasource/stackdriver/components/AnnotationQueryEditor';
 import { PasswordStrength } from './components/PasswordStrength';
 import { PasswordStrength } from './components/PasswordStrength';
 import PageHeader from './components/PageHeader/PageHeader';
 import PageHeader from './components/PageHeader/PageHeader';
 import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
 import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
 import { SearchResult } from './components/search/SearchResult';
 import { SearchResult } from './components/search/SearchResult';
 import { TagFilter } from './components/TagFilter/TagFilter';
 import { TagFilter } from './components/TagFilter/TagFilter';
 import { SideMenu } from './components/sidemenu/SideMenu';
 import { SideMenu } from './components/sidemenu/SideMenu';
+import { MetricSelect } from './components/Select/MetricSelect';
 import AppNotificationList from './components/AppNotifications/AppNotificationList';
 import AppNotificationList from './components/AppNotifications/AppNotificationList';
 import { ColorPicker, SeriesColorPickerPopover } from '@grafana/ui';
 import { ColorPicker, SeriesColorPickerPopover } from '@grafana/ui';
 
 
@@ -29,4 +32,28 @@ export function registerAngularDirectives() {
     'onColorChange',
     'onColorChange',
     'onToggleAxis',
     'onToggleAxis',
   ]);
   ]);
+  react2AngularDirective('metricSelect', MetricSelect, [
+    'options',
+    'onChange',
+    'value',
+    'isSearchable',
+    'className',
+    'placeholder',
+    ['variables', { watchDepth: 'reference' }],
+  ]);
+  react2AngularDirective('stackdriverQueryEditor', StackdriverQueryEditor, [
+    'target',
+    'onQueryChange',
+    'onExecuteQuery',
+    ['events', { watchDepth: 'reference' }],
+    ['datasource', { watchDepth: 'reference' }],
+    ['templateSrv', { watchDepth: 'reference' }],
+  ]);
+  react2AngularDirective('stackdriverAnnotationQueryEditor', StackdriverAnnotationQueryEditor, [
+    'target',
+    'onQueryChange',
+    'onExecuteQuery',
+    ['datasource', { watchDepth: 'reference' }],
+    ['templateSrv', { watchDepth: 'reference' }],
+  ]);
 }
 }

+ 90 - 0
public/app/core/components/Select/MetricSelect.tsx

@@ -0,0 +1,90 @@
+import React from 'react';
+import _ from 'lodash';
+
+import { Select } from '@grafana/ui';
+import { SelectOptionItem } from '@grafana/ui';
+import { Variable } from 'app/types/templates';
+
+export interface Props {
+  onChange: (value: string) => void;
+  options: SelectOptionItem[];
+  isSearchable: boolean;
+  value: string;
+  placeholder?: string;
+  className?: string;
+  variables?: Variable[];
+}
+
+interface State {
+  options: any[];
+}
+
+export class MetricSelect extends React.Component<Props, State> {
+  static defaultProps = {
+    variables: [],
+    options: [],
+    isSearchable: true,
+  };
+
+  constructor(props) {
+    super(props);
+    this.state = { options: [] };
+  }
+
+  componentDidMount() {
+    this.setState({ options: this.buildOptions(this.props) });
+  }
+
+  componentWillReceiveProps(nextProps: Props) {
+    if (nextProps.options.length > 0 || nextProps.variables.length) {
+      this.setState({ options: this.buildOptions(nextProps) });
+    }
+  }
+
+  shouldComponentUpdate(nextProps: Props) {
+    const nextOptions = this.buildOptions(nextProps);
+    return nextProps.value !== this.props.value || !_.isEqual(nextOptions, this.state.options);
+  }
+
+  buildOptions({ variables = [], options }) {
+    return variables.length > 0 ? [this.getVariablesGroup(), ...options] : options;
+  }
+
+  getVariablesGroup() {
+    return {
+      label: 'Template Variables',
+      options: this.props.variables.map(v => ({
+        label: `$${v.name}`,
+        value: `$${v.name}`,
+      })),
+    };
+  }
+
+  getSelectedOption() {
+    const { options } = this.state;
+    const allOptions = options.every(o => o.options) ? _.flatten(options.map(o => o.options)) : options;
+    return allOptions.find(option => option.value === this.props.value);
+  }
+
+  render() {
+    const { placeholder, className, isSearchable, onChange } = this.props;
+    const { options } = this.state;
+    const selectedOption = this.getSelectedOption();
+
+    return (
+      <Select
+        className={className}
+        isMulti={false}
+        isClearable={false}
+        backspaceRemovesValue={false}
+        onChange={item => onChange(item.value)}
+        options={options}
+        isSearchable={isSearchable}
+        maxMenuHeight={500}
+        placeholder={placeholder}
+        noOptionsMessage={() => 'No options found'}
+        value={selectedOption}
+      />
+    );
+  }
+}

+ 20 - 9
public/app/core/directives/dropdown_typeahead.ts

@@ -141,6 +141,9 @@ export function dropdownTypeahead2($compile) {
     link: ($scope, elem, attrs) => {
     link: ($scope, elem, attrs) => {
       const $input = $(inputTemplate);
       const $input = $(inputTemplate);
       const $button = $(buttonTemplate);
       const $button = $(buttonTemplate);
+      const timeoutId = {
+        blur: null
+      };
       $input.appendTo(elem);
       $input.appendTo(elem);
       $button.appendTo(elem);
       $button.appendTo(elem);
 
 
@@ -177,6 +180,14 @@ export function dropdownTypeahead2($compile) {
         []
         []
       );
       );
 
 
+      const closeDropdownMenu = () => {
+        $input.hide();
+        $input.val('');
+        $button.show();
+        $button.focus();
+        elem.removeClass('open');
+      };
+
       $scope.menuItemSelected = (index, subIndex) => {
       $scope.menuItemSelected = (index, subIndex) => {
         const menuItem = $scope.menuItems[index];
         const menuItem = $scope.menuItems[index];
         const payload: any = { $item: menuItem };
         const payload: any = { $item: menuItem };
@@ -184,6 +195,7 @@ export function dropdownTypeahead2($compile) {
           payload.$subItem = menuItem.submenu[subIndex];
           payload.$subItem = menuItem.submenu[subIndex];
         }
         }
         $scope.dropdownTypeaheadOnSelect(payload);
         $scope.dropdownTypeaheadOnSelect(payload);
+        closeDropdownMenu();
       };
       };
 
 
       $input.attr('data-provide', 'typeahead');
       $input.attr('data-provide', 'typeahead');
@@ -223,16 +235,15 @@ export function dropdownTypeahead2($compile) {
         elem.toggleClass('open', $input.val() === '');
         elem.toggleClass('open', $input.val() === '');
       });
       });
 
 
+      elem.mousedown((evt: Event) => {
+        evt.preventDefault();
+        timeoutId.blur = null;
+      });
+
       $input.blur(() => {
       $input.blur(() => {
-        $input.hide();
-        $input.val('');
-        $button.show();
-        $button.focus();
-        // clicking the function dropdown menu won't
-        // work if you remove class at once
-        setTimeout(() => {
-          elem.removeClass('open');
-        }, 200);
+        timeoutId.blur = setTimeout(() => {
+          closeDropdownMenu();
+        }, 1);
       });
       });
 
 
       $compile(elem.contents())($scope);
       $compile(elem.contents())($scope);

+ 22 - 50
public/app/core/utils/explore.test.ts

@@ -6,26 +6,13 @@ import {
   clearHistory,
   clearHistory,
   hasNonEmptyQuery,
   hasNonEmptyQuery,
 } from './explore';
 } from './explore';
-import { ExploreState } from 'app/types/explore';
+import { ExploreUrlState } from 'app/types/explore';
 import store from 'app/core/store';
 import store from 'app/core/store';
 
 
-const DEFAULT_EXPLORE_STATE: ExploreState = {
+const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
   datasource: null,
   datasource: null,
-  datasourceError: null,
-  datasourceLoading: null,
-  datasourceMissing: false,
-  exploreDatasources: [],
-  graphInterval: 1000,
-  history: [],
-  initialQueries: [],
-  queryTransactions: [],
+  queries: [],
   range: DEFAULT_RANGE,
   range: DEFAULT_RANGE,
-  showingGraph: true,
-  showingLogs: true,
-  showingTable: true,
-  supportsGraph: null,
-  supportsLogs: null,
-  supportsTable: null,
 };
 };
 
 
 describe('state functions', () => {
 describe('state functions', () => {
@@ -68,21 +55,19 @@ describe('state functions', () => {
     it('returns url parameter value for a state object', () => {
     it('returns url parameter value for a state object', () => {
       const state = {
       const state = {
         ...DEFAULT_EXPLORE_STATE,
         ...DEFAULT_EXPLORE_STATE,
-        initialDatasource: 'foo',
-        range: {
-          from: 'now-5h',
-          to: 'now',
-        },
-        initialQueries: [
+        datasource: 'foo',
+        queries: [
           {
           {
-            refId: '1',
             expr: 'metric{test="a/b"}',
             expr: 'metric{test="a/b"}',
           },
           },
           {
           {
-            refId: '2',
             expr: 'super{foo="x/z"}',
             expr: 'super{foo="x/z"}',
           },
           },
         ],
         ],
+        range: {
+          from: 'now-5h',
+          to: 'now',
+        },
       };
       };
       expect(serializeStateToUrlParam(state)).toBe(
       expect(serializeStateToUrlParam(state)).toBe(
         '{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
         '{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' +
@@ -93,21 +78,19 @@ describe('state functions', () => {
     it('returns url parameter value for a state object', () => {
     it('returns url parameter value for a state object', () => {
       const state = {
       const state = {
         ...DEFAULT_EXPLORE_STATE,
         ...DEFAULT_EXPLORE_STATE,
-        initialDatasource: 'foo',
-        range: {
-          from: 'now-5h',
-          to: 'now',
-        },
-        initialQueries: [
+        datasource: 'foo',
+        queries: [
           {
           {
-            refId: '1',
             expr: 'metric{test="a/b"}',
             expr: 'metric{test="a/b"}',
           },
           },
           {
           {
-            refId: '2',
             expr: 'super{foo="x/z"}',
             expr: 'super{foo="x/z"}',
           },
           },
         ],
         ],
+        range: {
+          from: 'now-5h',
+          to: 'now',
+        },
       };
       };
       expect(serializeStateToUrlParam(state, true)).toBe(
       expect(serializeStateToUrlParam(state, true)).toBe(
         '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]'
         '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]'
@@ -119,35 +102,24 @@ describe('state functions', () => {
     it('can parse the serialized state into the original state', () => {
     it('can parse the serialized state into the original state', () => {
       const state = {
       const state = {
         ...DEFAULT_EXPLORE_STATE,
         ...DEFAULT_EXPLORE_STATE,
-        initialDatasource: 'foo',
-        range: {
-          from: 'now - 5h',
-          to: 'now',
-        },
-        initialQueries: [
+        datasource: 'foo',
+        queries: [
           {
           {
-            refId: '1',
             expr: 'metric{test="a/b"}',
             expr: 'metric{test="a/b"}',
           },
           },
           {
           {
-            refId: '2',
             expr: 'super{foo="x/z"}',
             expr: 'super{foo="x/z"}',
           },
           },
         ],
         ],
+        range: {
+          from: 'now - 5h',
+          to: 'now',
+        },
       };
       };
       const serialized = serializeStateToUrlParam(state);
       const serialized = serializeStateToUrlParam(state);
       const parsed = parseUrlState(serialized);
       const parsed = parseUrlState(serialized);
 
 
-      // Account for datasource vs datasourceName
-      const { datasource, queries, ...rest } = parsed;
-      const resultState = {
-        ...rest,
-        datasource: DEFAULT_EXPLORE_STATE.datasource,
-        initialDatasource: datasource,
-        initialQueries: queries,
-      };
-
-      expect(state).toMatchObject(resultState);
+      expect(state).toMatchObject(parsed);
     });
     });
   });
   });
 });
 });

+ 85 - 22
public/app/core/utils/explore.ts

@@ -1,6 +1,7 @@
 import _ from 'lodash';
 import _ from 'lodash';
-import { colors } from '@grafana/ui';
+import { colors, RawTimeRange, IntervalValues } from '@grafana/ui';
 
 
+import * as dateMath from 'app/core/utils/datemath';
 import { renderUrl } from 'app/core/utils/url';
 import { renderUrl } from 'app/core/utils/url';
 import kbn from 'app/core/utils/kbn';
 import kbn from 'app/core/utils/kbn';
 import store from 'app/core/store';
 import store from 'app/core/store';
@@ -8,9 +9,15 @@ import { parse as parseDate } from 'app/core/utils/datemath';
 
 
 import TimeSeries from 'app/core/time_series2';
 import TimeSeries from 'app/core/time_series2';
 import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
 import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
-import { ExploreState, ExploreUrlState, HistoryItem, QueryTransaction } from 'app/types/explore';
-import { DataQuery, DataSourceApi } from 'app/types/series';
-import { RawTimeRange, IntervalValues } from '@grafana/ui';
+import {
+  ExploreUrlState,
+  HistoryItem,
+  QueryTransaction,
+  ResultType,
+  QueryIntervals,
+  QueryOptions,
+} from 'app/types/explore';
+import { DataQuery } from 'app/types/series';
 
 
 export const DEFAULT_RANGE = {
 export const DEFAULT_RANGE = {
   from: 'now-6h',
   from: 'now-6h',
@@ -19,6 +26,8 @@ export const DEFAULT_RANGE = {
 
 
 const MAX_HISTORY_ITEMS = 100;
 const MAX_HISTORY_ITEMS = 100;
 
 
+export const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource';
+
 /**
 /**
  * Returns an Explore-URL that contains a panel's queries and the dashboard time range.
  * Returns an Explore-URL that contains a panel's queries and the dashboard time range.
  *
  *
@@ -77,7 +86,63 @@ export async function getExploreUrl(
   return url;
   return url;
 }
 }
 
 
-const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
+export function buildQueryTransaction(
+  query: DataQuery,
+  rowIndex: number,
+  resultType: ResultType,
+  queryOptions: QueryOptions,
+  range: RawTimeRange,
+  queryIntervals: QueryIntervals,
+  scanning: boolean
+): QueryTransaction {
+  const { interval, intervalMs } = queryIntervals;
+
+  const configuredQueries = [
+    {
+      ...query,
+      ...queryOptions,
+    },
+  ];
+
+  // Clone range for query request
+  // const queryRange: RawTimeRange = { ...range };
+  // const { from, to, raw } = this.timeSrv.timeRange();
+  // Most datasource is using `panelId + query.refId` for cancellation logic.
+  // Using `format` here because it relates to the view panel that the request is for.
+  // However, some datasources don't use `panelId + query.refId`, but only `panelId`.
+  // Therefore panel id has to be unique.
+  const panelId = `${queryOptions.format}-${query.key}`;
+
+  const options = {
+    interval,
+    intervalMs,
+    panelId,
+    targets: configuredQueries, // Datasources rely on DataQueries being passed under the targets key.
+    range: {
+      from: dateMath.parse(range.from, false),
+      to: dateMath.parse(range.to, true),
+      raw: range,
+    },
+    rangeRaw: range,
+    scopedVars: {
+      __interval: { text: interval, value: interval },
+      __interval_ms: { text: intervalMs, value: intervalMs },
+    },
+  };
+
+  return {
+    options,
+    query,
+    resultType,
+    rowIndex,
+    scanning,
+    id: generateKey(), // reusing for unique ID
+    done: false,
+    latency: 0,
+  };
+}
+
+export const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest;
 
 
 export function parseUrlState(initial: string | undefined): ExploreUrlState {
 export function parseUrlState(initial: string | undefined): ExploreUrlState {
   if (initial) {
   if (initial) {
@@ -103,12 +168,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState {
   return { datasource: null, queries: [], range: DEFAULT_RANGE };
   return { datasource: null, queries: [], range: DEFAULT_RANGE };
 }
 }
 
 
-export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string {
-  const urlState: ExploreUrlState = {
-    datasource: state.initialDatasource,
-    queries: state.initialQueries.map(clearQueryKeys),
-    range: state.range,
-  };
+export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string {
   if (compact) {
   if (compact) {
     return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]);
     return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]);
   }
   }
@@ -123,7 +183,7 @@ export function generateRefId(index = 0): string {
   return `${index + 1}`;
   return `${index + 1}`;
 }
 }
 
 
-export function generateQueryKeys(index = 0): { refId: string; key: string } {
+export function generateEmptyQuery(index = 0): { refId: string; key: string } {
   return { refId: generateRefId(index), key: generateKey(index) };
   return { refId: generateRefId(index), key: generateKey(index) };
 }
 }
 
 
@@ -132,20 +192,23 @@ export function generateQueryKeys(index = 0): { refId: string; key: string } {
  */
  */
 export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
 export function ensureQueries(queries?: DataQuery[]): DataQuery[] {
   if (queries && typeof queries === 'object' && queries.length > 0) {
   if (queries && typeof queries === 'object' && queries.length > 0) {
-    return queries.map((query, i) => ({ ...query, ...generateQueryKeys(i) }));
+    return queries.map((query, i) => ({ ...query, ...generateEmptyQuery(i) }));
   }
   }
-  return [{ ...generateQueryKeys() }];
+  return [{ ...generateEmptyQuery() }];
 }
 }
 
 
 /**
 /**
  * A target is non-empty when it has keys (with non-empty values) other than refId and key.
  * A target is non-empty when it has keys (with non-empty values) other than refId and key.
  */
  */
 export function hasNonEmptyQuery(queries: DataQuery[]): boolean {
 export function hasNonEmptyQuery(queries: DataQuery[]): boolean {
-  return queries.some(
-    query =>
-      Object.keys(query)
-        .map(k => query[k])
-        .filter(v => v).length > 2
+  return (
+    queries &&
+    queries.some(
+      query =>
+        Object.keys(query)
+          .map(k => query[k])
+          .filter(v => v).length > 2
+    )
   );
   );
 }
 }
 
 
@@ -180,8 +243,8 @@ export function calculateResultsFromQueryTransactions(
   };
   };
 }
 }
 
 
-export function getIntervals(range: RawTimeRange, datasource: DataSourceApi, resolution: number): IntervalValues {
-  if (!datasource || !resolution) {
+export function getIntervals(range: RawTimeRange, lowLimit: string, resolution: number): IntervalValues {
+  if (!resolution) {
     return { interval: '1s', intervalMs: 1000 };
     return { interval: '1s', intervalMs: 1000 };
   }
   }
 
 
@@ -190,7 +253,7 @@ export function getIntervals(range: RawTimeRange, datasource: DataSourceApi, res
     to: parseDate(range.to, true),
     to: parseDate(range.to, true),
   };
   };
 
 
-  return kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
+  return kbn.calculateInterval(absoluteRange, resolution, lowLimit);
 }
 }
 
 
 export function makeTimeSeriesList(dataList) {
 export function makeTimeSeriesList(dataList) {

+ 1 - 0
public/app/features/all.ts

@@ -11,3 +11,4 @@ import './alerting/NotificationsListCtrl';
 import './manage-dashboards';
 import './manage-dashboards';
 import './teams/CreateTeamCtrl';
 import './teams/CreateTeamCtrl';
 import './profile/all';
 import './profile/all';
+import './datasources/settings/HttpSettingsCtrl';

+ 7 - 1
public/app/features/dashboard/dashgrid/PanelChrome.tsx

@@ -20,6 +20,7 @@ import { PanelPlugin } from 'app/types';
 import { TimeRange } from '@grafana/ui';
 import { TimeRange } from '@grafana/ui';
 
 
 import variables from 'sass/_variables.scss';
 import variables from 'sass/_variables.scss';
+import templateSrv from 'app/features/templating/template_srv';
 
 
 export interface Props {
 export interface Props {
   panel: PanelModel;
   panel: PanelModel;
@@ -78,6 +79,10 @@ export class PanelChrome extends PureComponent<Props, State> {
     });
     });
   };
   };
 
 
+  onInterpolate = (value: string, format?: string) => {
+    return templateSrv.replace(value, this.props.panel.scopedVars, format);
+  };
+
   get isVisible() {
   get isVisible() {
     return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
     return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
   }
   }
@@ -124,9 +129,10 @@ export class PanelChrome extends PureComponent<Props, State> {
                         timeSeries={timeSeries}
                         timeSeries={timeSeries}
                         timeRange={timeRange}
                         timeRange={timeRange}
                         options={panel.getOptions(plugin.exports.PanelDefaults)}
                         options={panel.getOptions(plugin.exports.PanelDefaults)}
-                        width={width - 2 * variables.panelHorizontalPadding }
+                        width={width - 2 * variables.panelHorizontalPadding}
                         height={height - PANEL_HEADER_HEIGHT - variables.panelVerticalPadding}
                         height={height - PANEL_HEADER_HEIGHT - variables.panelVerticalPadding}
                         renderCounter={renderCounter}
                         renderCounter={renderCounter}
+                        onInterpolate={this.onInterpolate}
                       />
                       />
                     </div>
                     </div>
                   );
                   );

+ 5 - 2
public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx

@@ -3,6 +3,7 @@ import classNames from 'classnames';
 
 
 import PanelHeaderCorner from './PanelHeaderCorner';
 import PanelHeaderCorner from './PanelHeaderCorner';
 import { PanelHeaderMenu } from './PanelHeaderMenu';
 import { PanelHeaderMenu } from './PanelHeaderMenu';
+import templateSrv from 'app/features/templating/template_srv';
 
 
 import { DashboardModel } from 'app/features/dashboard/dashboard_model';
 import { DashboardModel } from 'app/features/dashboard/dashboard_model';
 import { PanelModel } from 'app/features/dashboard/panel_model';
 import { PanelModel } from 'app/features/dashboard/panel_model';
@@ -45,7 +46,9 @@ export class PanelHeader extends Component<Props, State> {
     const isFullscreen = false;
     const isFullscreen = false;
     const isLoading = false;
     const isLoading = false;
     const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
     const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
-    const { panel, dashboard, timeInfo } = this.props;
+    const { panel, dashboard, timeInfo, scopedVars } = this.props;
+    const title = templateSrv.replaceWithText(panel.title, scopedVars);
+
     return (
     return (
       <>
       <>
         <PanelHeaderCorner
         <PanelHeaderCorner
@@ -65,7 +68,7 @@ export class PanelHeader extends Component<Props, State> {
             <div className="panel-title">
             <div className="panel-title">
               <span className="icon-gf panel-alert-icon" />
               <span className="icon-gf panel-alert-icon" />
               <span className="panel-title-text">
               <span className="panel-title-text">
-                {panel.title} <span className="fa fa-caret-down panel-menu-toggle" />
+                {title} <span className="fa fa-caret-down panel-menu-toggle" />
               </span>
               </span>
 
 
               {this.state.panelMenuOpen && (
               {this.state.panelMenuOpen && (

+ 0 - 0
public/app/features/plugins/partials/ds_http_settings.html → public/app/features/datasources/partials/http_settings.html


+ 26 - 0
public/app/features/datasources/settings/HttpSettingsCtrl.ts

@@ -0,0 +1,26 @@
+import { coreModule } from 'app/core/core';
+
+coreModule.directive('datasourceHttpSettings', () => {
+  return {
+    scope: {
+      current: '=',
+      suggestUrl: '@',
+      noDirectAccess: '@',
+    },
+    templateUrl: 'public/app/features/datasources/partials/http_settings.html',
+    link: {
+      pre: ($scope, elem, attrs) => {
+        // do not show access option if direct access is disabled
+        $scope.showAccessOption = $scope.noDirectAccess !== 'true';
+        $scope.showAccessHelp = false;
+        $scope.toggleAccessHelp = () => {
+          $scope.showAccessHelp = !$scope.showAccessHelp;
+        };
+
+        $scope.getSuggestUrls = () => {
+          return [$scope.suggestUrl];
+        };
+      },
+    },
+  };
+});

Plik diff jest za duży
+ 113 - 788
public/app/features/explore/Explore.tsx


+ 61 - 0
public/app/features/explore/GraphContainer.tsx

@@ -0,0 +1,61 @@
+import React, { PureComponent } from 'react';
+import { hot } from 'react-hot-loader';
+import { connect } from 'react-redux';
+import { RawTimeRange, TimeRange } from '@grafana/ui';
+
+import { ExploreId, ExploreItemState } from 'app/types/explore';
+import { StoreState } from 'app/types';
+
+import { toggleGraph } from './state/actions';
+import Graph from './Graph';
+import Panel from './Panel';
+
+interface GraphContainerProps {
+  onChangeTime: (range: TimeRange) => void;
+  exploreId: ExploreId;
+  graphResult?: any[];
+  loading: boolean;
+  range: RawTimeRange;
+  showingGraph: boolean;
+  showingTable: boolean;
+  split: boolean;
+  toggleGraph: typeof toggleGraph;
+}
+
+export class GraphContainer extends PureComponent<GraphContainerProps> {
+  onClickGraphButton = () => {
+    this.props.toggleGraph(this.props.exploreId);
+  };
+
+  render() {
+    const { exploreId, graphResult, loading, onChangeTime, showingGraph, showingTable, range, split } = this.props;
+    const graphHeight = showingGraph && showingTable ? '200px' : '400px';
+    return (
+      <Panel label="Graph" isOpen={showingGraph} loading={loading} onToggle={this.onClickGraphButton}>
+        <Graph
+          data={graphResult}
+          height={graphHeight}
+          id={`explore-graph-${exploreId}`}
+          onChangeTime={onChangeTime}
+          range={range}
+          split={split}
+        />
+      </Panel>
+    );
+  }
+}
+
+function mapStateToProps(state: StoreState, { exploreId }) {
+  const explore = state.explore;
+  const { split } = explore;
+  const item: ExploreItemState = explore[exploreId];
+  const { graphResult, queryTransactions, range, showingGraph, showingTable } = item;
+  const loading = queryTransactions.some(qt => qt.resultType === 'Graph' && !qt.done);
+  return { graphResult, loading, range, showingGraph, showingTable, split };
+}
+
+const mapDispatchToProps = {
+  toggleGraph,
+};
+
+export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(GraphContainer));

+ 3 - 3
public/app/features/explore/Logs.tsx

@@ -241,9 +241,9 @@ function renderMetaItem(value: any, kind: LogsMetaKind) {
 
 
 interface LogsProps {
 interface LogsProps {
   data: LogsModel;
   data: LogsModel;
+  exploreId: string;
   highlighterExpressions: string[];
   highlighterExpressions: string[];
   loading: boolean;
   loading: boolean;
-  position: string;
   range?: RawTimeRange;
   range?: RawTimeRange;
   scanning?: boolean;
   scanning?: boolean;
   scanRange?: RawTimeRange;
   scanRange?: RawTimeRange;
@@ -348,10 +348,10 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
   render() {
   render() {
     const {
     const {
       data,
       data,
+      exploreId,
       highlighterExpressions,
       highlighterExpressions,
       loading = false,
       loading = false,
       onClickLabel,
       onClickLabel,
-      position,
       range,
       range,
       scanning,
       scanning,
       scanRange,
       scanRange,
@@ -400,7 +400,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
             data={data.series}
             data={data.series}
             height="100px"
             height="100px"
             range={range}
             range={range}
-            id={`explore-logs-graph-${position}`}
+            id={`explore-logs-graph-${exploreId}`}
             onChangeTime={this.props.onChangeTime}
             onChangeTime={this.props.onChangeTime}
             onToggleSeries={this.onToggleLogLevel}
             onToggleSeries={this.onToggleLogLevel}
             userOptions={graphOptions}
             userOptions={graphOptions}

+ 91 - 0
public/app/features/explore/LogsContainer.tsx

@@ -0,0 +1,91 @@
+import React, { PureComponent } from 'react';
+import { hot } from 'react-hot-loader';
+import { connect } from 'react-redux';
+import { RawTimeRange, TimeRange } from '@grafana/ui';
+
+import { ExploreId, ExploreItemState } from 'app/types/explore';
+import { LogsModel } from 'app/core/logs_model';
+import { StoreState } from 'app/types';
+
+import { toggleLogs } from './state/actions';
+import Logs from './Logs';
+import Panel from './Panel';
+
+interface LogsContainerProps {
+  exploreId: ExploreId;
+  loading: boolean;
+  logsHighlighterExpressions?: string[];
+  logsResult?: LogsModel;
+  onChangeTime: (range: TimeRange) => void;
+  onClickLabel: (key: string, value: string) => void;
+  onStartScanning: () => void;
+  onStopScanning: () => void;
+  range: RawTimeRange;
+  scanning?: boolean;
+  scanRange?: RawTimeRange;
+  showingLogs: boolean;
+  toggleLogs: typeof toggleLogs;
+}
+
+export class LogsContainer extends PureComponent<LogsContainerProps> {
+  onClickLogsButton = () => {
+    this.props.toggleLogs(this.props.exploreId);
+  };
+
+  render() {
+    const {
+      exploreId,
+      loading,
+      logsHighlighterExpressions,
+      logsResult,
+      onChangeTime,
+      onClickLabel,
+      onStartScanning,
+      onStopScanning,
+      range,
+      showingLogs,
+      scanning,
+      scanRange,
+    } = this.props;
+    return (
+      <Panel label="Logs" loading={loading} isOpen={showingLogs} onToggle={this.onClickLogsButton}>
+        <Logs
+          data={logsResult}
+          exploreId={exploreId}
+          key={logsResult.id}
+          highlighterExpressions={logsHighlighterExpressions}
+          loading={loading}
+          onChangeTime={onChangeTime}
+          onClickLabel={onClickLabel}
+          onStartScanning={onStartScanning}
+          onStopScanning={onStopScanning}
+          range={range}
+          scanning={scanning}
+          scanRange={scanRange}
+        />
+      </Panel>
+    );
+  }
+}
+
+function mapStateToProps(state: StoreState, { exploreId }) {
+  const explore = state.explore;
+  const item: ExploreItemState = explore[exploreId];
+  const { logsHighlighterExpressions, logsResult, queryTransactions, scanning, scanRange, showingLogs, range } = item;
+  const loading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done);
+  return {
+    loading,
+    logsHighlighterExpressions,
+    logsResult,
+    scanning,
+    scanRange,
+    showingLogs,
+    range,
+  };
+}
+
+const mapDispatchToProps = {
+  toggleLogs,
+};
+
+export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LogsContainer));

+ 1 - 1
public/app/features/explore/QueryEditor.tsx

@@ -48,7 +48,7 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
           getNextQueryLetter: x => '',
           getNextQueryLetter: x => '',
         },
         },
         hideEditorRowActions: true,
         hideEditorRowActions: true,
-        ...getIntervals(range, datasource, null), // Possible to get resolution?
+        ...getIntervals(range, (datasource || {}).interval, null), // Possible to get resolution?
       },
       },
     };
     };
 
 

+ 163 - 0
public/app/features/explore/QueryRow.tsx

@@ -0,0 +1,163 @@
+import React, { PureComponent } from 'react';
+import { hot } from 'react-hot-loader';
+import { connect } from 'react-redux';
+import { RawTimeRange } from '@grafana/ui';
+import _ from 'lodash';
+
+import { QueryTransaction, HistoryItem, QueryHint, ExploreItemState, ExploreId } from 'app/types/explore';
+import { Emitter } from 'app/core/utils/emitter';
+import { DataQuery, StoreState } from 'app/types';
+
+// import DefaultQueryField from './QueryField';
+import QueryEditor from './QueryEditor';
+import QueryTransactionStatus from './QueryTransactionStatus';
+import {
+  addQueryRow,
+  changeQuery,
+  highlightLogsExpression,
+  modifyQueries,
+  removeQueryRow,
+  runQueries,
+} from './state/actions';
+
+function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
+  const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
+  if (transaction) {
+    return transaction.hints[0];
+  }
+  return undefined;
+}
+
+interface QueryRowProps {
+  addQueryRow: typeof addQueryRow;
+  changeQuery: typeof changeQuery;
+  className?: string;
+  exploreId: ExploreId;
+  datasourceInstance: any;
+  highlightLogsExpression: typeof highlightLogsExpression;
+  history: HistoryItem[];
+  index: number;
+  initialQuery: DataQuery;
+  modifyQueries: typeof modifyQueries;
+  queryTransactions: QueryTransaction[];
+  exploreEvents: Emitter;
+  range: RawTimeRange;
+  removeQueryRow: typeof removeQueryRow;
+  runQueries: typeof runQueries;
+}
+
+export class QueryRow extends PureComponent<QueryRowProps> {
+  onExecuteQuery = () => {
+    const { exploreId } = this.props;
+    this.props.runQueries(exploreId);
+  };
+
+  onChangeQuery = (query: DataQuery, override?: boolean) => {
+    const { datasourceInstance, exploreId, index } = this.props;
+    this.props.changeQuery(exploreId, query, index, override);
+    if (query && !override && datasourceInstance.getHighlighterExpression && index === 0) {
+      // Live preview of log search matches. Only use on first row for now
+      this.updateLogsHighlights(query);
+    }
+  };
+
+  onClickAddButton = () => {
+    const { exploreId, index } = this.props;
+    this.props.addQueryRow(exploreId, index);
+  };
+
+  onClickClearButton = () => {
+    this.onChangeQuery(null, true);
+  };
+
+  onClickHintFix = action => {
+    const { datasourceInstance, exploreId, index } = this.props;
+    if (datasourceInstance && datasourceInstance.modifyQuery) {
+      const modifier = (queries: DataQuery, action: any) => datasourceInstance.modifyQuery(queries, action);
+      this.props.modifyQueries(exploreId, action, index, modifier);
+    }
+  };
+
+  onClickRemoveButton = () => {
+    const { exploreId, index } = this.props;
+    this.props.removeQueryRow(exploreId, index);
+  };
+
+  updateLogsHighlights = _.debounce((value: DataQuery) => {
+    const { datasourceInstance } = this.props;
+    if (datasourceInstance.getHighlighterExpression) {
+      const expressions = [datasourceInstance.getHighlighterExpression(value)];
+      this.props.highlightLogsExpression(this.props.exploreId, expressions);
+    }
+  }, 500);
+
+  render() {
+    const { datasourceInstance, history, index, initialQuery, queryTransactions, exploreEvents, range } = this.props;
+    const transactions = queryTransactions.filter(t => t.rowIndex === index);
+    const transactionWithError = transactions.find(t => t.error !== undefined);
+    const hint = getFirstHintFromTransactions(transactions);
+    const queryError = transactionWithError ? transactionWithError.error : null;
+    const QueryField = datasourceInstance.pluginExports.ExploreQueryField;
+    return (
+      <div className="query-row">
+        <div className="query-row-status">
+          <QueryTransactionStatus transactions={transactions} />
+        </div>
+        <div className="query-row-field">
+          {QueryField ? (
+            <QueryField
+              datasource={datasourceInstance}
+              error={queryError}
+              hint={hint}
+              initialQuery={initialQuery}
+              history={history}
+              onClickHintFix={this.onClickHintFix}
+              onPressEnter={this.onExecuteQuery}
+              onQueryChange={this.onChangeQuery}
+            />
+          ) : (
+            <QueryEditor
+              datasource={datasourceInstance}
+              error={queryError}
+              onQueryChange={this.onChangeQuery}
+              onExecuteQuery={this.onExecuteQuery}
+              initialQuery={initialQuery}
+              exploreEvents={exploreEvents}
+              range={range}
+            />
+          )}
+        </div>
+        <div className="query-row-tools">
+          <button className="btn navbar-button navbar-button--tight" onClick={this.onClickClearButton}>
+            <i className="fa fa-times" />
+          </button>
+          <button className="btn navbar-button navbar-button--tight" onClick={this.onClickAddButton}>
+            <i className="fa fa-plus" />
+          </button>
+          <button className="btn navbar-button navbar-button--tight" onClick={this.onClickRemoveButton}>
+            <i className="fa fa-minus" />
+          </button>
+        </div>
+      </div>
+    );
+  }
+}
+
+function mapStateToProps(state: StoreState, { exploreId, index }) {
+  const explore = state.explore;
+  const item: ExploreItemState = explore[exploreId];
+  const { datasourceInstance, history, initialQueries, queryTransactions, range } = item;
+  const initialQuery = initialQueries[index];
+  return { datasourceInstance, history, initialQuery, queryTransactions, range };
+}
+
+const mapDispatchToProps = {
+  addQueryRow,
+  changeQuery,
+  highlightLogsExpression,
+  modifyQueries,
+  removeQueryRow,
+  runQueries,
+};
+
+export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(QueryRow));

+ 9 - 143
public/app/features/explore/QueryRows.tsx

@@ -1,159 +1,25 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 
 
-import { QueryTransaction, HistoryItem, QueryHint } from 'app/types/explore';
 import { Emitter } from 'app/core/utils/emitter';
 import { Emitter } from 'app/core/utils/emitter';
+import { DataQuery } from 'app/types';
+import { ExploreId } from 'app/types/explore';
 
 
-// import DefaultQueryField from './QueryField';
-import QueryEditor from './QueryEditor';
-import QueryTransactionStatus from './QueryTransactionStatus';
-import { DataSource, DataQuery } from 'app/types';
-import { RawTimeRange } from '@grafana/ui';
+import QueryRow from './QueryRow';
 
 
-function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
-  const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
-  if (transaction) {
-    return transaction.hints[0];
-  }
-  return undefined;
-}
-
-interface QueryRowEventHandlers {
-  onAddQueryRow: (index: number) => void;
-  onChangeQuery: (value: DataQuery, index: number, override?: boolean) => void;
-  onClickHintFix: (action: object, index?: number) => void;
-  onExecuteQuery: () => void;
-  onRemoveQueryRow: (index: number) => void;
-}
-
-interface QueryRowCommonProps {
+interface QueryRowsProps {
   className?: string;
   className?: string;
-  datasource: DataSource;
-  history: HistoryItem[];
-  transactions: QueryTransaction[];
   exploreEvents: Emitter;
   exploreEvents: Emitter;
-  range: RawTimeRange;
-}
-
-type QueryRowProps = QueryRowCommonProps &
-  QueryRowEventHandlers & {
-    index: number;
-    initialQuery: DataQuery;
-  };
-
-class QueryRow extends PureComponent<QueryRowProps> {
-  onExecuteQuery = () => {
-    const { onExecuteQuery } = this.props;
-    onExecuteQuery();
-  };
-
-  onChangeQuery = (value: DataQuery, override?: boolean) => {
-    const { index, onChangeQuery } = this.props;
-    if (onChangeQuery) {
-      onChangeQuery(value, index, override);
-    }
-  };
-
-  onClickAddButton = () => {
-    const { index, onAddQueryRow } = this.props;
-    if (onAddQueryRow) {
-      onAddQueryRow(index);
-    }
-  };
-
-  onClickClearButton = () => {
-    this.onChangeQuery(null, true);
-  };
-
-  onClickHintFix = action => {
-    const { index, onClickHintFix } = this.props;
-    if (onClickHintFix) {
-      onClickHintFix(action, index);
-    }
-  };
-
-  onClickRemoveButton = () => {
-    const { index, onRemoveQueryRow } = this.props;
-    if (onRemoveQueryRow) {
-      onRemoveQueryRow(index);
-    }
-  };
-
-  onPressEnter = () => {
-    const { onExecuteQuery } = this.props;
-    if (onExecuteQuery) {
-      onExecuteQuery();
-    }
-  };
-
-  render() {
-    const { datasource, history, initialQuery, transactions, exploreEvents, range } = this.props;
-    const transactionWithError = transactions.find(t => t.error !== undefined);
-    const hint = getFirstHintFromTransactions(transactions);
-    const queryError = transactionWithError ? transactionWithError.error : null;
-    const QueryField = datasource.pluginExports.ExploreQueryField;
-    return (
-      <div className="query-row">
-        <div className="query-row-status">
-          <QueryTransactionStatus transactions={transactions} />
-        </div>
-        <div className="query-row-field">
-          {QueryField ? (
-            <QueryField
-              datasource={datasource}
-              error={queryError}
-              hint={hint}
-              initialQuery={initialQuery}
-              history={history}
-              onClickHintFix={this.onClickHintFix}
-              onPressEnter={this.onPressEnter}
-              onQueryChange={this.onChangeQuery}
-            />
-          ) : (
-            <QueryEditor
-              datasource={datasource}
-              error={queryError}
-              onQueryChange={this.onChangeQuery}
-              onExecuteQuery={this.onExecuteQuery}
-              initialQuery={initialQuery}
-              exploreEvents={exploreEvents}
-              range={range}
-            />
-          )}
-        </div>
-        <div className="query-row-tools">
-          <button className="btn navbar-button navbar-button--tight" onClick={this.onClickClearButton}>
-            <i className="fa fa-times" />
-          </button>
-          <button className="btn navbar-button navbar-button--tight" onClick={this.onClickAddButton}>
-            <i className="fa fa-plus" />
-          </button>
-          <button className="btn navbar-button navbar-button--tight" onClick={this.onClickRemoveButton}>
-            <i className="fa fa-minus" />
-          </button>
-        </div>
-      </div>
-    );
-  }
+  exploreId: ExploreId;
+  initialQueries: DataQuery[];
 }
 }
-
-type QueryRowsProps = QueryRowCommonProps &
-  QueryRowEventHandlers & {
-    initialQueries: DataQuery[];
-  };
-
 export default class QueryRows extends PureComponent<QueryRowsProps> {
 export default class QueryRows extends PureComponent<QueryRowsProps> {
   render() {
   render() {
-    const { className = '', initialQueries, transactions, ...handlers } = this.props;
+    const { className = '', exploreEvents, exploreId, initialQueries } = this.props;
     return (
     return (
       <div className={className}>
       <div className={className}>
         {initialQueries.map((query, index) => (
         {initialQueries.map((query, index) => (
-          <QueryRow
-            key={query.key}
-            index={index}
-            initialQuery={query}
-            transactions={transactions.filter(t => t.rowIndex === index)}
-            {...handlers}
-          />
+          // TODO instead of relying on initialQueries, move to react key list in redux
+          <QueryRow key={query.key} exploreEvents={exploreEvents} exploreId={exploreId} index={index} />
         ))}
         ))}
       </div>
       </div>
     );
     );

+ 49 - 0
public/app/features/explore/TableContainer.tsx

@@ -0,0 +1,49 @@
+import React, { PureComponent } from 'react';
+import { hot } from 'react-hot-loader';
+import { connect } from 'react-redux';
+
+import { ExploreId, ExploreItemState } from 'app/types/explore';
+import { StoreState } from 'app/types';
+
+import { toggleGraph } from './state/actions';
+import Table from './Table';
+import Panel from './Panel';
+import TableModel from 'app/core/table_model';
+
+interface TableContainerProps {
+  exploreId: ExploreId;
+  loading: boolean;
+  onClickCell: (key: string, value: string) => void;
+  showingTable: boolean;
+  tableResult?: TableModel;
+  toggleGraph: typeof toggleGraph;
+}
+
+export class TableContainer extends PureComponent<TableContainerProps> {
+  onClickTableButton = () => {
+    this.props.toggleGraph(this.props.exploreId);
+  };
+
+  render() {
+    const { loading, onClickCell, showingTable, tableResult } = this.props;
+    return (
+      <Panel label="Table" loading={loading} isOpen={showingTable} onToggle={this.onClickTableButton}>
+        <Table data={tableResult} loading={loading} onClickCell={onClickCell} />
+      </Panel>
+    );
+  }
+}
+
+function mapStateToProps(state: StoreState, { exploreId }) {
+  const explore = state.explore;
+  const item: ExploreItemState = explore[exploreId];
+  const { queryTransactions, showingTable, tableResult } = item;
+  const loading = queryTransactions.some(qt => qt.resultType === 'Table' && !qt.done);
+  return { loading, showingTable, tableResult };
+}
+
+const mapDispatchToProps = {
+  toggleGraph,
+};
+
+export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TableContainer));

+ 31 - 63
public/app/features/explore/Wrapper.tsx

@@ -3,91 +3,56 @@ import { hot } from 'react-hot-loader';
 import { connect } from 'react-redux';
 import { connect } from 'react-redux';
 
 
 import { updateLocation } from 'app/core/actions';
 import { updateLocation } from 'app/core/actions';
-import { serializeStateToUrlParam, parseUrlState } from 'app/core/utils/explore';
 import { StoreState } from 'app/types';
 import { StoreState } from 'app/types';
-import { ExploreState } from 'app/types/explore';
+import { ExploreId, ExploreUrlState } from 'app/types/explore';
+import { parseUrlState } from 'app/core/utils/explore';
 
 
+import { initializeExploreSplit } from './state/actions';
 import ErrorBoundary from './ErrorBoundary';
 import ErrorBoundary from './ErrorBoundary';
 import Explore from './Explore';
 import Explore from './Explore';
 
 
 interface WrapperProps {
 interface WrapperProps {
-  backendSrv?: any;
-  datasourceSrv?: any;
+  initializeExploreSplit: typeof initializeExploreSplit;
+  split: boolean;
   updateLocation: typeof updateLocation;
   updateLocation: typeof updateLocation;
   urlStates: { [key: string]: string };
   urlStates: { [key: string]: string };
 }
 }
 
 
-interface WrapperState {
-  split: boolean;
-  splitState: ExploreState;
-}
-
-const STATE_KEY_LEFT = 'state';
-const STATE_KEY_RIGHT = 'stateRight';
-
-export class Wrapper extends Component<WrapperProps, WrapperState> {
-  urlStates: { [key: string]: string };
+export class Wrapper extends Component<WrapperProps> {
+  initialSplit: boolean;
+  urlStates: { [key: string]: ExploreUrlState };
 
 
   constructor(props: WrapperProps) {
   constructor(props: WrapperProps) {
     super(props);
     super(props);
-    this.urlStates = props.urlStates;
-    this.state = {
-      split: Boolean(props.urlStates[STATE_KEY_RIGHT]),
-      splitState: undefined,
-    };
+    this.urlStates = {};
+    const { left, right } = props.urlStates;
+    if (props.urlStates.left) {
+      this.urlStates.leftState = parseUrlState(left);
+    }
+    if (props.urlStates.right) {
+      this.urlStates.rightState = parseUrlState(right);
+      this.initialSplit = true;
+    }
   }
   }
 
 
-  onChangeSplit = (split: boolean, splitState: ExploreState) => {
-    this.setState({ split, splitState });
-    // When closing split, remove URL state for split part
-    if (!split) {
-      delete this.urlStates[STATE_KEY_RIGHT];
-      this.props.updateLocation({
-        query: this.urlStates,
-      });
+  componentDidMount() {
+    if (this.initialSplit) {
+      this.props.initializeExploreSplit();
     }
     }
-  };
-
-  onSaveState = (key: string, state: ExploreState) => {
-    const urlState = serializeStateToUrlParam(state, true);
-    this.urlStates[key] = urlState;
-    this.props.updateLocation({
-      query: this.urlStates,
-    });
-  };
+  }
 
 
   render() {
   render() {
-    const { datasourceSrv } = this.props;
-    // State overrides for props from first Explore
-    const { split, splitState } = this.state;
-    const urlStateLeft = parseUrlState(this.urlStates[STATE_KEY_LEFT]);
-    const urlStateRight = parseUrlState(this.urlStates[STATE_KEY_RIGHT]);
+    const { split } = this.props;
+    const { leftState, rightState } = this.urlStates;
 
 
     return (
     return (
       <div className="explore-wrapper">
       <div className="explore-wrapper">
         <ErrorBoundary>
         <ErrorBoundary>
-          <Explore
-            datasourceSrv={datasourceSrv}
-            onChangeSplit={this.onChangeSplit}
-            onSaveState={this.onSaveState}
-            position="left"
-            split={split}
-            stateKey={STATE_KEY_LEFT}
-            urlState={urlStateLeft}
-          />
+          <Explore exploreId={ExploreId.left} urlState={leftState} />
         </ErrorBoundary>
         </ErrorBoundary>
         {split && (
         {split && (
           <ErrorBoundary>
           <ErrorBoundary>
-            <Explore
-              datasourceSrv={datasourceSrv}
-              onChangeSplit={this.onChangeSplit}
-              onSaveState={this.onSaveState}
-              position="right"
-              split={split}
-              splitState={splitState}
-              stateKey={STATE_KEY_RIGHT}
-              urlState={urlStateRight}
-            />
+            <Explore exploreId={ExploreId.right} urlState={rightState} />
           </ErrorBoundary>
           </ErrorBoundary>
         )}
         )}
       </div>
       </div>
@@ -95,11 +60,14 @@ export class Wrapper extends Component<WrapperProps, WrapperState> {
   }
   }
 }
 }
 
 
-const mapStateToProps = (state: StoreState) => ({
-  urlStates: state.location.query,
-});
+const mapStateToProps = (state: StoreState) => {
+  const urlStates = state.location.query;
+  const { split } = state.explore;
+  return { split, urlStates };
+};
 
 
 const mapDispatchToProps = {
 const mapDispatchToProps = {
+  initializeExploreSplit,
   updateLocation,
   updateLocation,
 };
 };
 
 

+ 302 - 0
public/app/features/explore/state/actionTypes.ts

@@ -0,0 +1,302 @@
+import { RawTimeRange, TimeRange } from '@grafana/ui';
+
+import { Emitter } from 'app/core/core';
+import {
+  ExploreId,
+  ExploreItemState,
+  HistoryItem,
+  RangeScanner,
+  ResultType,
+  QueryTransaction,
+} from 'app/types/explore';
+import { DataSourceSelectItem } from 'app/types/datasources';
+import { DataQuery } from 'app/types';
+
+export enum ActionTypes {
+  AddQueryRow = 'explore/ADD_QUERY_ROW',
+  ChangeDatasource = 'explore/CHANGE_DATASOURCE',
+  ChangeQuery = 'explore/CHANGE_QUERY',
+  ChangeSize = 'explore/CHANGE_SIZE',
+  ChangeTime = 'explore/CHANGE_TIME',
+  ClearQueries = 'explore/CLEAR_QUERIES',
+  HighlightLogsExpression = 'explore/HIGHLIGHT_LOGS_EXPRESSION',
+  InitializeExplore = 'explore/INITIALIZE_EXPLORE',
+  InitializeExploreSplit = 'explore/INITIALIZE_EXPLORE_SPLIT',
+  LoadDatasourceFailure = 'explore/LOAD_DATASOURCE_FAILURE',
+  LoadDatasourceMissing = 'explore/LOAD_DATASOURCE_MISSING',
+  LoadDatasourcePending = 'explore/LOAD_DATASOURCE_PENDING',
+  LoadDatasourceSuccess = 'explore/LOAD_DATASOURCE_SUCCESS',
+  ModifyQueries = 'explore/MODIFY_QUERIES',
+  QueryTransactionFailure = 'explore/QUERY_TRANSACTION_FAILURE',
+  QueryTransactionStart = 'explore/QUERY_TRANSACTION_START',
+  QueryTransactionSuccess = 'explore/QUERY_TRANSACTION_SUCCESS',
+  RemoveQueryRow = 'explore/REMOVE_QUERY_ROW',
+  RunQueries = 'explore/RUN_QUERIES',
+  RunQueriesEmpty = 'explore/RUN_QUERIES_EMPTY',
+  ScanRange = 'explore/SCAN_RANGE',
+  ScanStart = 'explore/SCAN_START',
+  ScanStop = 'explore/SCAN_STOP',
+  SetQueries = 'explore/SET_QUERIES',
+  SplitClose = 'explore/SPLIT_CLOSE',
+  SplitOpen = 'explore/SPLIT_OPEN',
+  StateSave = 'explore/STATE_SAVE',
+  ToggleGraph = 'explore/TOGGLE_GRAPH',
+  ToggleLogs = 'explore/TOGGLE_LOGS',
+  ToggleTable = 'explore/TOGGLE_TABLE',
+}
+
+export interface AddQueryRowAction {
+  type: ActionTypes.AddQueryRow;
+  payload: {
+    exploreId: ExploreId;
+    index: number;
+    query: DataQuery;
+  };
+}
+
+export interface ChangeQueryAction {
+  type: ActionTypes.ChangeQuery;
+  payload: {
+    exploreId: ExploreId;
+    query: DataQuery;
+    index: number;
+    override: boolean;
+  };
+}
+
+export interface ChangeSizeAction {
+  type: ActionTypes.ChangeSize;
+  payload: {
+    exploreId: ExploreId;
+    width: number;
+    height: number;
+  };
+}
+
+export interface ChangeTimeAction {
+  type: ActionTypes.ChangeTime;
+  payload: {
+    exploreId: ExploreId;
+    range: TimeRange;
+  };
+}
+
+export interface ClearQueriesAction {
+  type: ActionTypes.ClearQueries;
+  payload: {
+    exploreId: ExploreId;
+  };
+}
+
+export interface HighlightLogsExpressionAction {
+  type: ActionTypes.HighlightLogsExpression;
+  payload: {
+    exploreId: ExploreId;
+    expressions: string[];
+  };
+}
+
+export interface InitializeExploreAction {
+  type: ActionTypes.InitializeExplore;
+  payload: {
+    exploreId: ExploreId;
+    containerWidth: number;
+    datasource: string;
+    eventBridge: Emitter;
+    exploreDatasources: DataSourceSelectItem[];
+    queries: DataQuery[];
+    range: RawTimeRange;
+  };
+}
+
+export interface InitializeExploreSplitAction {
+  type: ActionTypes.InitializeExploreSplit;
+}
+
+export interface LoadDatasourceFailureAction {
+  type: ActionTypes.LoadDatasourceFailure;
+  payload: {
+    exploreId: ExploreId;
+    error: string;
+  };
+}
+
+export interface LoadDatasourcePendingAction {
+  type: ActionTypes.LoadDatasourcePending;
+  payload: {
+    exploreId: ExploreId;
+    datasourceId: number;
+  };
+}
+
+export interface LoadDatasourceMissingAction {
+  type: ActionTypes.LoadDatasourceMissing;
+  payload: {
+    exploreId: ExploreId;
+  };
+}
+
+export interface LoadDatasourceSuccessAction {
+  type: ActionTypes.LoadDatasourceSuccess;
+  payload: {
+    exploreId: ExploreId;
+    StartPage?: any;
+    datasourceInstance: any;
+    history: HistoryItem[];
+    initialDatasource: string;
+    initialQueries: DataQuery[];
+    logsHighlighterExpressions?: any[];
+    showingStartPage: boolean;
+    supportsGraph: boolean;
+    supportsLogs: boolean;
+    supportsTable: boolean;
+  };
+}
+
+export interface ModifyQueriesAction {
+  type: ActionTypes.ModifyQueries;
+  payload: {
+    exploreId: ExploreId;
+    modification: any;
+    index: number;
+    modifier: (queries: DataQuery[], modification: any) => DataQuery[];
+  };
+}
+
+export interface QueryTransactionFailureAction {
+  type: ActionTypes.QueryTransactionFailure;
+  payload: {
+    exploreId: ExploreId;
+    queryTransactions: QueryTransaction[];
+  };
+}
+
+export interface QueryTransactionStartAction {
+  type: ActionTypes.QueryTransactionStart;
+  payload: {
+    exploreId: ExploreId;
+    resultType: ResultType;
+    rowIndex: number;
+    transaction: QueryTransaction;
+  };
+}
+
+export interface QueryTransactionSuccessAction {
+  type: ActionTypes.QueryTransactionSuccess;
+  payload: {
+    exploreId: ExploreId;
+    history: HistoryItem[];
+    queryTransactions: QueryTransaction[];
+  };
+}
+
+export interface RemoveQueryRowAction {
+  type: ActionTypes.RemoveQueryRow;
+  payload: {
+    exploreId: ExploreId;
+    index: number;
+  };
+}
+
+export interface RunQueriesEmptyAction {
+  type: ActionTypes.RunQueriesEmpty;
+  payload: {
+    exploreId: ExploreId;
+  };
+}
+
+export interface ScanStartAction {
+  type: ActionTypes.ScanStart;
+  payload: {
+    exploreId: ExploreId;
+    scanner: RangeScanner;
+  };
+}
+
+export interface ScanRangeAction {
+  type: ActionTypes.ScanRange;
+  payload: {
+    exploreId: ExploreId;
+    range: RawTimeRange;
+  };
+}
+
+export interface ScanStopAction {
+  type: ActionTypes.ScanStop;
+  payload: {
+    exploreId: ExploreId;
+  };
+}
+
+export interface SetQueriesAction {
+  type: ActionTypes.SetQueries;
+  payload: {
+    exploreId: ExploreId;
+    queries: DataQuery[];
+  };
+}
+
+export interface SplitCloseAction {
+  type: ActionTypes.SplitClose;
+}
+
+export interface SplitOpenAction {
+  type: ActionTypes.SplitOpen;
+  payload: {
+    itemState: ExploreItemState;
+  };
+}
+
+export interface StateSaveAction {
+  type: ActionTypes.StateSave;
+}
+
+export interface ToggleTableAction {
+  type: ActionTypes.ToggleTable;
+  payload: {
+    exploreId: ExploreId;
+  };
+}
+
+export interface ToggleGraphAction {
+  type: ActionTypes.ToggleGraph;
+  payload: {
+    exploreId: ExploreId;
+  };
+}
+
+export interface ToggleLogsAction {
+  type: ActionTypes.ToggleLogs;
+  payload: {
+    exploreId: ExploreId;
+  };
+}
+
+export type Action =
+  | AddQueryRowAction
+  | ChangeQueryAction
+  | ChangeSizeAction
+  | ChangeTimeAction
+  | ClearQueriesAction
+  | HighlightLogsExpressionAction
+  | InitializeExploreAction
+  | InitializeExploreSplitAction
+  | LoadDatasourceFailureAction
+  | LoadDatasourceMissingAction
+  | LoadDatasourcePendingAction
+  | LoadDatasourceSuccessAction
+  | ModifyQueriesAction
+  | QueryTransactionFailureAction
+  | QueryTransactionStartAction
+  | QueryTransactionSuccessAction
+  | RemoveQueryRowAction
+  | RunQueriesEmptyAction
+  | ScanRangeAction
+  | ScanStartAction
+  | ScanStopAction
+  | SetQueriesAction
+  | SplitCloseAction
+  | SplitOpenAction
+  | ToggleGraphAction
+  | ToggleLogsAction
+  | ToggleTableAction;

+ 757 - 0
public/app/features/explore/state/actions.ts

@@ -0,0 +1,757 @@
+import _ from 'lodash';
+import { ThunkAction } from 'redux-thunk';
+import { RawTimeRange, TimeRange } from '@grafana/ui';
+
+import {
+  LAST_USED_DATASOURCE_KEY,
+  clearQueryKeys,
+  ensureQueries,
+  generateEmptyQuery,
+  hasNonEmptyQuery,
+  makeTimeSeriesList,
+  updateHistory,
+  buildQueryTransaction,
+  serializeStateToUrlParam,
+} from 'app/core/utils/explore';
+
+import { updateLocation } from 'app/core/actions';
+import store from 'app/core/store';
+import { DataSourceSelectItem } from 'app/types/datasources';
+import { DataQuery, StoreState } from 'app/types';
+import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
+import {
+  ExploreId,
+  ExploreUrlState,
+  RangeScanner,
+  ResultType,
+  QueryOptions,
+  QueryTransaction,
+  QueryHint,
+  QueryHintGetter,
+} from 'app/types/explore';
+import { Emitter } from 'app/core/core';
+
+import {
+  Action as ThunkableAction,
+  ActionTypes,
+  AddQueryRowAction,
+  ChangeSizeAction,
+  HighlightLogsExpressionAction,
+  LoadDatasourceFailureAction,
+  LoadDatasourceMissingAction,
+  LoadDatasourcePendingAction,
+  LoadDatasourceSuccessAction,
+  QueryTransactionStartAction,
+  ScanStopAction,
+} from './actionTypes';
+
+type ThunkResult<R> = ThunkAction<R, StoreState, undefined, ThunkableAction>;
+
+/**
+ * Adds a query row after the row with the given index.
+ */
+export function addQueryRow(exploreId: ExploreId, index: number): AddQueryRowAction {
+  const query = generateEmptyQuery(index + 1);
+  return { type: ActionTypes.AddQueryRow, payload: { exploreId, index, query } };
+}
+
+/**
+ * Loads a new datasource identified by the given name.
+ */
+export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult<void> {
+  return async dispatch => {
+    const instance = await getDatasourceSrv().get(datasource);
+    dispatch(loadDatasource(exploreId, instance));
+  };
+}
+
+/**
+ * Query change handler for the query row with the given index.
+ * If `override` is reset the query modifications and run the queries. Use this to set queries via a link.
+ */
+export function changeQuery(
+  exploreId: ExploreId,
+  query: DataQuery,
+  index: number,
+  override: boolean
+): ThunkResult<void> {
+  return dispatch => {
+    // Null query means reset
+    if (query === null) {
+      query = { ...generateEmptyQuery(index) };
+    }
+
+    dispatch({ type: ActionTypes.ChangeQuery, payload: { exploreId, query, index, override } });
+    if (override) {
+      dispatch(runQueries(exploreId));
+    }
+  };
+}
+
+/**
+ * Keep track of the Explore container size, in particular the width.
+ * The width will be used to calculate graph intervals (number of datapoints).
+ */
+export function changeSize(
+  exploreId: ExploreId,
+  { height, width }: { height: number; width: number }
+): ChangeSizeAction {
+  return { type: ActionTypes.ChangeSize, payload: { exploreId, height, width } };
+}
+
+/**
+ * Change the time range of Explore. Usually called from the Timepicker or a graph interaction.
+ */
+export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<void> {
+  return dispatch => {
+    dispatch({ type: ActionTypes.ChangeTime, payload: { exploreId, range } });
+    dispatch(runQueries(exploreId));
+  };
+}
+
+/**
+ * Clear all queries and results.
+ */
+export function clearQueries(exploreId: ExploreId): ThunkResult<void> {
+  return dispatch => {
+    dispatch(scanStop(exploreId));
+    dispatch({ type: ActionTypes.ClearQueries, payload: { exploreId } });
+    dispatch(stateSave());
+  };
+}
+
+/**
+ * Highlight expressions in the log results
+ */
+export function highlightLogsExpression(exploreId: ExploreId, expressions: string[]): HighlightLogsExpressionAction {
+  return { type: ActionTypes.HighlightLogsExpression, payload: { exploreId, expressions } };
+}
+
+/**
+ * Initialize Explore state with state from the URL and the React component.
+ * Call this only on components for with the Explore state has not been initialized.
+ */
+export function initializeExplore(
+  exploreId: ExploreId,
+  datasource: string,
+  queries: DataQuery[],
+  range: RawTimeRange,
+  containerWidth: number,
+  eventBridge: Emitter
+): ThunkResult<void> {
+  return async dispatch => {
+    const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv()
+      .getExternal()
+      .map(ds => ({
+        value: ds.name,
+        name: ds.name,
+        meta: ds.meta,
+      }));
+
+    dispatch({
+      type: ActionTypes.InitializeExplore,
+      payload: {
+        exploreId,
+        containerWidth,
+        datasource,
+        eventBridge,
+        exploreDatasources,
+        queries,
+        range,
+      },
+    });
+
+    if (exploreDatasources.length > 1) {
+      let instance;
+      if (datasource) {
+        instance = await getDatasourceSrv().get(datasource);
+      } else {
+        instance = await getDatasourceSrv().get();
+      }
+      dispatch(loadDatasource(exploreId, instance));
+    } else {
+      dispatch(loadDatasourceMissing(exploreId));
+    }
+  };
+}
+
+/**
+ * Initialize the wrapper split state
+ */
+export function initializeExploreSplit() {
+  return async dispatch => {
+    dispatch({ type: ActionTypes.InitializeExploreSplit });
+  };
+}
+
+/**
+ * Display an error that happened during the selection of a datasource
+ */
+export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({
+  type: ActionTypes.LoadDatasourceFailure,
+  payload: {
+    exploreId,
+    error,
+  },
+});
+
+/**
+ * Display an error when no datasources have been configured
+ */
+export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissingAction => ({
+  type: ActionTypes.LoadDatasourceMissing,
+  payload: { exploreId },
+});
+
+/**
+ * Start the async process of loading a datasource to display a loading indicator
+ */
+export const loadDatasourcePending = (exploreId: ExploreId, datasourceId: number): LoadDatasourcePendingAction => ({
+  type: ActionTypes.LoadDatasourcePending,
+  payload: {
+    exploreId,
+    datasourceId,
+  },
+});
+
+/**
+ * Datasource loading was successfully completed. The instance is stored in the state as well in case we need to
+ * run datasource-specific code. Existing queries are imported to the new datasource if an importer exists,
+ * e.g., Prometheus -> Loki queries.
+ */
+export const loadDatasourceSuccess = (
+  exploreId: ExploreId,
+  instance: any,
+  queries: DataQuery[]
+): LoadDatasourceSuccessAction => {
+  // Capabilities
+  const supportsGraph = instance.meta.metrics;
+  const supportsLogs = instance.meta.logs;
+  const supportsTable = instance.meta.tables;
+  // Custom components
+  const StartPage = instance.pluginExports.ExploreStartPage;
+
+  const historyKey = `grafana.explore.history.${instance.meta.id}`;
+  const history = store.getObject(historyKey, []);
+  // Save last-used datasource
+  store.set(LAST_USED_DATASOURCE_KEY, instance.name);
+
+  return {
+    type: ActionTypes.LoadDatasourceSuccess,
+    payload: {
+      exploreId,
+      StartPage,
+      datasourceInstance: instance,
+      history,
+      initialDatasource: instance.name,
+      initialQueries: queries,
+      showingStartPage: Boolean(StartPage),
+      supportsGraph,
+      supportsLogs,
+      supportsTable,
+    },
+  };
+};
+
+/**
+ * Main action to asynchronously load a datasource. Dispatches lots of smaller actions for feedback.
+ */
+export function loadDatasource(exploreId: ExploreId, instance: any): ThunkResult<void> {
+  return async (dispatch, getState) => {
+    const datasourceId = instance.meta.id;
+
+    // Keep ID to track selection
+    dispatch(loadDatasourcePending(exploreId, datasourceId));
+
+    let datasourceError = null;
+    try {
+      const testResult = await instance.testDatasource();
+      datasourceError = testResult.status === 'success' ? null : testResult.message;
+    } catch (error) {
+      datasourceError = (error && error.statusText) || 'Network error';
+    }
+    if (datasourceError) {
+      dispatch(loadDatasourceFailure(exploreId, datasourceError));
+      return;
+    }
+
+    if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) {
+      // User already changed datasource again, discard results
+      return;
+    }
+
+    if (instance.init) {
+      instance.init();
+    }
+
+    // Check if queries can be imported from previously selected datasource
+    const queries = getState().explore[exploreId].modifiedQueries;
+    let importedQueries = queries;
+    const origin = getState().explore[exploreId].datasourceInstance;
+    if (origin) {
+      if (origin.meta.id === instance.meta.id) {
+        // Keep same queries if same type of datasource
+        importedQueries = [...queries];
+      } else if (instance.importQueries) {
+        // Datasource-specific importers
+        importedQueries = await instance.importQueries(queries, origin.meta);
+      } else {
+        // Default is blank queries
+        importedQueries = ensureQueries();
+      }
+    }
+
+    if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) {
+      // User already changed datasource again, discard results
+      return;
+    }
+
+    // Reset edit state with new queries
+    const nextQueries = importedQueries.map((q, i) => ({
+      ...importedQueries[i],
+      ...generateEmptyQuery(i),
+    }));
+
+    dispatch(loadDatasourceSuccess(exploreId, instance, nextQueries));
+    dispatch(runQueries(exploreId));
+  };
+}
+
+/**
+ * Action to modify a query given a datasource-specific modifier action.
+ * @param exploreId Explore area
+ * @param modification Action object with a type, e.g., ADD_FILTER
+ * @param index Optional query row index. If omitted, the modification is applied to all query rows.
+ * @param modifier Function that executes the modification, typically `datasourceInstance.modifyQueries`.
+ */
+export function modifyQueries(
+  exploreId: ExploreId,
+  modification: any,
+  index: number,
+  modifier: any
+): ThunkResult<void> {
+  return dispatch => {
+    dispatch({ type: ActionTypes.ModifyQueries, payload: { exploreId, modification, index, modifier } });
+    if (!modification.preventSubmit) {
+      dispatch(runQueries(exploreId));
+    }
+  };
+}
+
+/**
+ * Mark a query transaction as failed with an error extracted from the query response.
+ * The transaction will be marked as `done`.
+ */
+export function queryTransactionFailure(
+  exploreId: ExploreId,
+  transactionId: string,
+  response: any,
+  datasourceId: string
+): ThunkResult<void> {
+  return (dispatch, getState) => {
+    const { datasourceInstance, queryTransactions } = getState().explore[exploreId];
+    if (datasourceInstance.meta.id !== datasourceId || response.cancelled) {
+      // Navigated away, queries did not matter
+      return;
+    }
+
+    // Transaction might have been discarded
+    if (!queryTransactions.find(qt => qt.id === transactionId)) {
+      return;
+    }
+
+    console.error(response);
+
+    let error: string;
+    let errorDetails: string;
+    if (response.data) {
+      if (typeof response.data === 'string') {
+        error = response.data;
+      } else if (response.data.error) {
+        error = response.data.error;
+        if (response.data.response) {
+          errorDetails = response.data.response;
+        }
+      } else {
+        throw new Error('Could not handle error response');
+      }
+    } else if (response.message) {
+      error = response.message;
+    } else if (typeof response === 'string') {
+      error = response;
+    } else {
+      error = 'Unknown error during query transaction. Please check JS console logs.';
+    }
+
+    // Mark transactions as complete
+    const nextQueryTransactions = queryTransactions.map(qt => {
+      if (qt.id === transactionId) {
+        return {
+          ...qt,
+          error,
+          errorDetails,
+          done: true,
+        };
+      }
+      return qt;
+    });
+
+    dispatch({
+      type: ActionTypes.QueryTransactionFailure,
+      payload: { exploreId, queryTransactions: nextQueryTransactions },
+    });
+  };
+}
+
+/**
+ * Start a query transaction for the given result type.
+ * @param exploreId Explore area
+ * @param transaction Query options and `done` status.
+ * @param resultType Associate the transaction with a result viewer, e.g., Graph
+ * @param rowIndex Index is used to associate latency for this transaction with a query row
+ */
+export function queryTransactionStart(
+  exploreId: ExploreId,
+  transaction: QueryTransaction,
+  resultType: ResultType,
+  rowIndex: number
+): QueryTransactionStartAction {
+  return { type: ActionTypes.QueryTransactionStart, payload: { exploreId, resultType, rowIndex, transaction } };
+}
+
+/**
+ * Complete a query transaction, mark the transaction as `done` and store query state in URL.
+ * If the transaction was started by a scanner, it keeps on scanning for more results.
+ * Side-effect: the query is stored in localStorage.
+ * @param exploreId Explore area
+ * @param transactionId ID
+ * @param result Response from `datasourceInstance.query()`
+ * @param latency Duration between request and response
+ * @param queries Queries from all query rows
+ * @param datasourceId Origin datasource instance, used to discard results if current datasource is different
+ */
+export function queryTransactionSuccess(
+  exploreId: ExploreId,
+  transactionId: string,
+  result: any,
+  latency: number,
+  queries: DataQuery[],
+  datasourceId: string
+): ThunkResult<void> {
+  return (dispatch, getState) => {
+    const { datasourceInstance, history, queryTransactions, scanner, scanning } = getState().explore[exploreId];
+
+    // If datasource already changed, results do not matter
+    if (datasourceInstance.meta.id !== datasourceId) {
+      return;
+    }
+
+    // Transaction might have been discarded
+    const transaction = queryTransactions.find(qt => qt.id === transactionId);
+    if (!transaction) {
+      return;
+    }
+
+    // Get query hints
+    let hints: QueryHint[];
+    if (datasourceInstance.getQueryHints as QueryHintGetter) {
+      hints = datasourceInstance.getQueryHints(transaction.query, result);
+    }
+
+    // Mark transactions as complete and attach result
+    const nextQueryTransactions = queryTransactions.map(qt => {
+      if (qt.id === transactionId) {
+        return {
+          ...qt,
+          hints,
+          latency,
+          result,
+          done: true,
+        };
+      }
+      return qt;
+    });
+
+    // Side-effect: Saving history in localstorage
+    const nextHistory = updateHistory(history, datasourceId, queries);
+
+    dispatch({
+      type: ActionTypes.QueryTransactionSuccess,
+      payload: {
+        exploreId,
+        history: nextHistory,
+        queryTransactions: nextQueryTransactions,
+      },
+    });
+
+    // Keep scanning for results if this was the last scanning transaction
+    if (scanning) {
+      if (_.size(result) === 0) {
+        const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done);
+        if (!other) {
+          const range = scanner();
+          dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } });
+        }
+      } else {
+        // We can stop scanning if we have a result
+        dispatch(scanStop(exploreId));
+      }
+    }
+  };
+}
+
+/**
+ * Remove query row of the given index, as well as associated query results.
+ */
+export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
+  return dispatch => {
+    dispatch({ type: ActionTypes.RemoveQueryRow, payload: { exploreId, index } });
+    dispatch(runQueries(exploreId));
+  };
+}
+
+/**
+ * Main action to run queries and dispatches sub-actions based on which result viewers are active
+ */
+export function runQueries(exploreId: ExploreId) {
+  return (dispatch, getState) => {
+    const {
+      datasourceInstance,
+      modifiedQueries,
+      showingLogs,
+      showingGraph,
+      showingTable,
+      supportsGraph,
+      supportsLogs,
+      supportsTable,
+    } = getState().explore[exploreId];
+
+    if (!hasNonEmptyQuery(modifiedQueries)) {
+      dispatch({ type: ActionTypes.RunQueriesEmpty, payload: { exploreId } });
+      return;
+    }
+
+    // Some datasource's query builders allow per-query interval limits,
+    // but we're using the datasource interval limit for now
+    const interval = datasourceInstance.interval;
+
+    // Keep table queries first since they need to return quickly
+    if (showingTable && supportsTable) {
+      dispatch(
+        runQueriesForType(
+          exploreId,
+          'Table',
+          {
+            interval,
+            format: 'table',
+            instant: true,
+            valueWithRefId: true,
+          },
+          data => data[0]
+        )
+      );
+    }
+    if (showingGraph && supportsGraph) {
+      dispatch(
+        runQueriesForType(
+          exploreId,
+          'Graph',
+          {
+            interval,
+            format: 'time_series',
+            instant: false,
+          },
+          makeTimeSeriesList
+        )
+      );
+    }
+    if (showingLogs && supportsLogs) {
+      dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' }));
+    }
+    dispatch(stateSave());
+  };
+}
+
+/**
+ * Helper action to build a query transaction object and handing the query to the datasource.
+ * @param exploreId Explore area
+ * @param resultType Result viewer that will be associated with this query result
+ * @param queryOptions Query options as required by the datasource's `query()` function.
+ * @param resultGetter Optional result extractor, e.g., if the result is a list and you only need the first element.
+ */
+function runQueriesForType(
+  exploreId: ExploreId,
+  resultType: ResultType,
+  queryOptions: QueryOptions,
+  resultGetter?: any
+) {
+  return async (dispatch, getState) => {
+    const {
+      datasourceInstance,
+      eventBridge,
+      modifiedQueries: queries,
+      queryIntervals,
+      range,
+      scanning,
+    } = getState().explore[exploreId];
+    const datasourceId = datasourceInstance.meta.id;
+
+    // Run all queries concurrently
+    queries.forEach(async (query, rowIndex) => {
+      const transaction = buildQueryTransaction(
+        query,
+        rowIndex,
+        resultType,
+        queryOptions,
+        range,
+        queryIntervals,
+        scanning
+      );
+      dispatch(queryTransactionStart(exploreId, transaction, resultType, rowIndex));
+      try {
+        const now = Date.now();
+        const res = await datasourceInstance.query(transaction.options);
+        eventBridge.emit('data-received', res.data || []);
+        const latency = Date.now() - now;
+        const results = resultGetter ? resultGetter(res.data) : res.data;
+        dispatch(queryTransactionSuccess(exploreId, transaction.id, results, latency, queries, datasourceId));
+      } catch (response) {
+        eventBridge.emit('data-error', response);
+        dispatch(queryTransactionFailure(exploreId, transaction.id, response, datasourceId));
+      }
+    });
+  };
+}
+
+/**
+ * Start a scan for more results using the given scanner.
+ * @param exploreId Explore area
+ * @param scanner Function that a) returns a new time range and b) triggers a query run for the new range
+ */
+export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult<void> {
+  return dispatch => {
+    // Register the scanner
+    dispatch({ type: ActionTypes.ScanStart, payload: { exploreId, scanner } });
+    // Scanning must trigger query run, and return the new range
+    const range = scanner();
+    // Set the new range to be displayed
+    dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } });
+  };
+}
+
+/**
+ * Stop any scanning for more results.
+ */
+export function scanStop(exploreId: ExploreId): ScanStopAction {
+  return { type: ActionTypes.ScanStop, payload: { exploreId } };
+}
+
+/**
+ * Reset queries to the given queries. Any modifications will be discarded.
+ * Use this action for clicks on query examples. Triggers a query run.
+ */
+export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult<void> {
+  return dispatch => {
+    // Inject react keys into query objects
+    const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery() }));
+    dispatch({
+      type: ActionTypes.SetQueries,
+      payload: {
+        exploreId,
+        queries,
+      },
+    });
+    dispatch(runQueries(exploreId));
+  };
+}
+
+/**
+ * Close the split view and save URL state.
+ */
+export function splitClose(): ThunkResult<void> {
+  return dispatch => {
+    dispatch({ type: ActionTypes.SplitClose });
+    dispatch(stateSave());
+  };
+}
+
+/**
+ * Open the split view and copy the left state to be the right state.
+ * The right state is automatically initialized.
+ * The copy keeps all query modifications but wipes the query results.
+ */
+export function splitOpen(): ThunkResult<void> {
+  return (dispatch, getState) => {
+    // Clone left state to become the right state
+    const leftState = getState().explore.left;
+    const itemState = {
+      ...leftState,
+      queryTransactions: [],
+      initialQueries: leftState.modifiedQueries.slice(),
+    };
+    dispatch({ type: ActionTypes.SplitOpen, payload: { itemState } });
+    dispatch(stateSave());
+  };
+}
+
+/**
+ * Saves Explore state to URL using the `left` and `right` parameters.
+ * If split view is not active, `right` will not be set.
+ */
+export function stateSave() {
+  return (dispatch, getState) => {
+    const { left, right, split } = getState().explore;
+    const urlStates: { [index: string]: string } = {};
+    const leftUrlState: ExploreUrlState = {
+      datasource: left.datasourceInstance.name,
+      queries: left.modifiedQueries.map(clearQueryKeys),
+      range: left.range,
+    };
+    urlStates.left = serializeStateToUrlParam(leftUrlState, true);
+    if (split) {
+      const rightUrlState: ExploreUrlState = {
+        datasource: right.datasourceInstance.name,
+        queries: right.modifiedQueries.map(clearQueryKeys),
+        range: right.range,
+      };
+      urlStates.right = serializeStateToUrlParam(rightUrlState, true);
+    }
+    dispatch(updateLocation({ query: urlStates }));
+  };
+}
+
+/**
+ * Expand/collapse the graph result viewer. When collapsed, graph queries won't be run.
+ */
+export function toggleGraph(exploreId: ExploreId): ThunkResult<void> {
+  return (dispatch, getState) => {
+    dispatch({ type: ActionTypes.ToggleGraph, payload: { exploreId } });
+    if (getState().explore[exploreId].showingGraph) {
+      dispatch(runQueries(exploreId));
+    }
+  };
+}
+
+/**
+ * Expand/collapse the logs result viewer. When collapsed, log queries won't be run.
+ */
+export function toggleLogs(exploreId: ExploreId): ThunkResult<void> {
+  return (dispatch, getState) => {
+    dispatch({ type: ActionTypes.ToggleLogs, payload: { exploreId } });
+    if (getState().explore[exploreId].showingLogs) {
+      dispatch(runQueries(exploreId));
+    }
+  };
+}
+
+/**
+ * Expand/collapse the table result viewer. When collapsed, table queries won't be run.
+ */
+export function toggleTable(exploreId: ExploreId): ThunkResult<void> {
+  return (dispatch, getState) => {
+    dispatch({ type: ActionTypes.ToggleTable, payload: { exploreId } });
+    if (getState().explore[exploreId].showingTable) {
+      dispatch(runQueries(exploreId));
+    }
+  };
+}

+ 462 - 0
public/app/features/explore/state/reducers.ts

@@ -0,0 +1,462 @@
+import {
+  calculateResultsFromQueryTransactions,
+  generateEmptyQuery,
+  getIntervals,
+  ensureQueries,
+} from 'app/core/utils/explore';
+import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore';
+import { DataQuery } from 'app/types/series';
+
+import { Action, ActionTypes } from './actionTypes';
+
+export const DEFAULT_RANGE = {
+  from: 'now-6h',
+  to: 'now',
+};
+
+// Millies step for helper bar charts
+const DEFAULT_GRAPH_INTERVAL = 15 * 1000;
+
+/**
+ * Returns a fresh Explore area state
+ */
+const makeExploreItemState = (): ExploreItemState => ({
+  StartPage: undefined,
+  containerWidth: 0,
+  datasourceInstance: null,
+  datasourceError: null,
+  datasourceLoading: null,
+  datasourceMissing: false,
+  exploreDatasources: [],
+  history: [],
+  initialQueries: [],
+  initialized: false,
+  modifiedQueries: [],
+  queryTransactions: [],
+  queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL },
+  range: DEFAULT_RANGE,
+  scanning: false,
+  scanRange: null,
+  showingGraph: true,
+  showingLogs: true,
+  showingTable: true,
+  supportsGraph: null,
+  supportsLogs: null,
+  supportsTable: null,
+});
+
+/**
+ * Global Explore state that handles multiple Explore areas and the split state
+ */
+const initialExploreState: ExploreState = {
+  split: null,
+  left: makeExploreItemState(),
+  right: makeExploreItemState(),
+};
+
+/**
+ * Reducer for an Explore area, to be used by the global Explore reducer.
+ */
+const itemReducer = (state, action: Action): ExploreItemState => {
+  switch (action.type) {
+    case ActionTypes.AddQueryRow: {
+      const { initialQueries, modifiedQueries, queryTransactions } = state;
+      const { index, query } = action.payload;
+
+      // Add new query row after given index, keep modifications of existing rows
+      const nextModifiedQueries = [
+        ...modifiedQueries.slice(0, index + 1),
+        { ...query },
+        ...initialQueries.slice(index + 1),
+      ];
+
+      // Add to initialQueries, which will cause a new row to be rendered
+      const nextQueries = [...initialQueries.slice(0, index + 1), { ...query }, ...initialQueries.slice(index + 1)];
+
+      // Ongoing transactions need to update their row indices
+      const nextQueryTransactions = queryTransactions.map(qt => {
+        if (qt.rowIndex > index) {
+          return {
+            ...qt,
+            rowIndex: qt.rowIndex + 1,
+          };
+        }
+        return qt;
+      });
+
+      return {
+        ...state,
+        initialQueries: nextQueries,
+        logsHighlighterExpressions: undefined,
+        modifiedQueries: nextModifiedQueries,
+        queryTransactions: nextQueryTransactions,
+      };
+    }
+
+    case ActionTypes.ChangeQuery: {
+      const { initialQueries, queryTransactions } = state;
+      let { modifiedQueries } = state;
+      const { query, index, override } = action.payload;
+
+      // Fast path: only change modifiedQueries to not trigger an update
+      modifiedQueries[index] = query;
+      if (!override) {
+        return {
+          ...state,
+          modifiedQueries,
+        };
+      }
+
+      // Override path: queries are completely reset
+      const nextQuery: DataQuery = {
+        ...query,
+        ...generateEmptyQuery(index),
+      };
+      const nextQueries = [...initialQueries];
+      nextQueries[index] = nextQuery;
+      modifiedQueries = [...nextQueries];
+
+      // Discard ongoing transaction related to row query
+      const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
+
+      return {
+        ...state,
+        initialQueries: nextQueries,
+        modifiedQueries: nextQueries.slice(),
+        queryTransactions: nextQueryTransactions,
+      };
+    }
+
+    case ActionTypes.ChangeSize: {
+      const { range, datasourceInstance } = state;
+      let interval = '1s';
+      if (datasourceInstance && datasourceInstance.interval) {
+        interval = datasourceInstance.interval;
+      }
+      const containerWidth = action.payload.width;
+      const queryIntervals = getIntervals(range, interval, containerWidth);
+      return { ...state, containerWidth, queryIntervals };
+    }
+
+    case ActionTypes.ChangeTime: {
+      return {
+        ...state,
+        range: action.payload.range,
+      };
+    }
+
+    case ActionTypes.ClearQueries: {
+      const queries = ensureQueries();
+      return {
+        ...state,
+        initialQueries: queries.slice(),
+        modifiedQueries: queries.slice(),
+        queryTransactions: [],
+        showingStartPage: Boolean(state.StartPage),
+      };
+    }
+
+    case ActionTypes.HighlightLogsExpression: {
+      const { expressions } = action.payload;
+      return { ...state, logsHighlighterExpressions: expressions };
+    }
+
+    case ActionTypes.InitializeExplore: {
+      const { containerWidth, datasource, eventBridge, exploreDatasources, queries, range } = action.payload;
+      return {
+        ...state,
+        containerWidth,
+        eventBridge,
+        exploreDatasources,
+        range,
+        initialDatasource: datasource,
+        initialQueries: queries,
+        initialized: true,
+        modifiedQueries: queries.slice(),
+      };
+    }
+
+    case ActionTypes.LoadDatasourceFailure: {
+      return { ...state, datasourceError: action.payload.error, datasourceLoading: false };
+    }
+
+    case ActionTypes.LoadDatasourceMissing: {
+      return { ...state, datasourceMissing: true, datasourceLoading: false };
+    }
+
+    case ActionTypes.LoadDatasourcePending: {
+      return { ...state, datasourceLoading: true, requestedDatasourceId: action.payload.datasourceId };
+    }
+
+    case ActionTypes.LoadDatasourceSuccess: {
+      const { containerWidth, range } = state;
+      const {
+        StartPage,
+        datasourceInstance,
+        history,
+        initialDatasource,
+        initialQueries,
+        showingStartPage,
+        supportsGraph,
+        supportsLogs,
+        supportsTable,
+      } = action.payload;
+      const queryIntervals = getIntervals(range, datasourceInstance.interval, containerWidth);
+
+      return {
+        ...state,
+        queryIntervals,
+        StartPage,
+        datasourceInstance,
+        history,
+        initialDatasource,
+        initialQueries,
+        showingStartPage,
+        supportsGraph,
+        supportsLogs,
+        supportsTable,
+        datasourceLoading: false,
+        datasourceMissing: false,
+        logsHighlighterExpressions: undefined,
+        modifiedQueries: initialQueries.slice(),
+        queryTransactions: [],
+      };
+    }
+
+    case ActionTypes.ModifyQueries: {
+      const { initialQueries, modifiedQueries, queryTransactions } = state;
+      const { modification, index, modifier } = action.payload as any;
+      let nextQueries: DataQuery[];
+      let nextQueryTransactions;
+      if (index === undefined) {
+        // Modify all queries
+        nextQueries = initialQueries.map((query, i) => ({
+          ...modifier(modifiedQueries[i], modification),
+          ...generateEmptyQuery(i),
+        }));
+        // Discard all ongoing transactions
+        nextQueryTransactions = [];
+      } else {
+        // Modify query only at index
+        nextQueries = initialQueries.map((query, i) => {
+          // Synchronize all queries with local query cache to ensure consistency
+          // TODO still needed?
+          return i === index
+            ? {
+                ...modifier(modifiedQueries[i], modification),
+                ...generateEmptyQuery(i),
+              }
+            : query;
+        });
+        nextQueryTransactions = queryTransactions
+          // Consume the hint corresponding to the action
+          .map(qt => {
+            if (qt.hints != null && qt.rowIndex === index) {
+              qt.hints = qt.hints.filter(hint => hint.fix.action !== modification);
+            }
+            return qt;
+          })
+          // Preserve previous row query transaction to keep results visible if next query is incomplete
+          .filter(qt => modification.preventSubmit || qt.rowIndex !== index);
+      }
+      return {
+        ...state,
+        initialQueries: nextQueries,
+        modifiedQueries: nextQueries.slice(),
+        queryTransactions: nextQueryTransactions,
+      };
+    }
+
+    case ActionTypes.QueryTransactionFailure: {
+      const { queryTransactions } = action.payload;
+      return {
+        ...state,
+        queryTransactions,
+        showingStartPage: false,
+      };
+    }
+
+    case ActionTypes.QueryTransactionStart: {
+      const { datasourceInstance, queryIntervals, queryTransactions } = state;
+      const { resultType, rowIndex, transaction } = action.payload;
+      // Discarding existing transactions of same type
+      const remainingTransactions = queryTransactions.filter(
+        qt => !(qt.resultType === resultType && qt.rowIndex === rowIndex)
+      );
+
+      // Append new transaction
+      const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction];
+
+      const results = calculateResultsFromQueryTransactions(
+        nextQueryTransactions,
+        datasourceInstance,
+        queryIntervals.intervalMs
+      );
+
+      return {
+        ...state,
+        ...results,
+        queryTransactions: nextQueryTransactions,
+        showingStartPage: false,
+      };
+    }
+
+    case ActionTypes.QueryTransactionSuccess: {
+      const { datasourceInstance, queryIntervals } = state;
+      const { history, queryTransactions } = action.payload;
+      const results = calculateResultsFromQueryTransactions(
+        queryTransactions,
+        datasourceInstance,
+        queryIntervals.intervalMs
+      );
+
+      return {
+        ...state,
+        ...results,
+        history,
+        queryTransactions,
+        showingStartPage: false,
+      };
+    }
+
+    case ActionTypes.RemoveQueryRow: {
+      const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state;
+      let { modifiedQueries } = state;
+      const { index } = action.payload;
+
+      modifiedQueries = [...modifiedQueries.slice(0, index), ...modifiedQueries.slice(index + 1)];
+
+      if (initialQueries.length <= 1) {
+        return state;
+      }
+
+      const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)];
+
+      // Discard transactions related to row query
+      const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
+      const results = calculateResultsFromQueryTransactions(
+        nextQueryTransactions,
+        datasourceInstance,
+        queryIntervals.intervalMs
+      );
+
+      return {
+        ...state,
+        ...results,
+        initialQueries: nextQueries,
+        logsHighlighterExpressions: undefined,
+        modifiedQueries: nextQueries.slice(),
+        queryTransactions: nextQueryTransactions,
+      };
+    }
+
+    case ActionTypes.RunQueriesEmpty: {
+      return { ...state, queryTransactions: [] };
+    }
+
+    case ActionTypes.ScanRange: {
+      return { ...state, scanRange: action.payload.range };
+    }
+
+    case ActionTypes.ScanStart: {
+      return { ...state, scanning: true };
+    }
+
+    case ActionTypes.ScanStop: {
+      const { queryTransactions } = state;
+      const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done);
+      return { ...state, queryTransactions: nextQueryTransactions, scanning: false, scanRange: undefined };
+    }
+
+    case ActionTypes.SetQueries: {
+      const { queries } = action.payload;
+      return { ...state, initialQueries: queries.slice(), modifiedQueries: queries.slice() };
+    }
+
+    case ActionTypes.ToggleGraph: {
+      const showingGraph = !state.showingGraph;
+      let nextQueryTransactions = state.queryTransactions;
+      if (!showingGraph) {
+        // Discard transactions related to Graph query
+        nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph');
+      }
+      return { ...state, queryTransactions: nextQueryTransactions, showingGraph };
+    }
+
+    case ActionTypes.ToggleLogs: {
+      const showingLogs = !state.showingLogs;
+      let nextQueryTransactions = state.queryTransactions;
+      if (!showingLogs) {
+        // Discard transactions related to Logs query
+        nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs');
+      }
+      return { ...state, queryTransactions: nextQueryTransactions, showingLogs };
+    }
+
+    case ActionTypes.ToggleTable: {
+      const showingTable = !state.showingTable;
+      if (showingTable) {
+        return { ...state, showingTable, queryTransactions: state.queryTransactions };
+      }
+
+      // Toggle off needs discarding of table queries and results
+      const nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Table');
+      const results = calculateResultsFromQueryTransactions(
+        nextQueryTransactions,
+        state.datasourceInstance,
+        state.queryIntervals.intervalMs
+      );
+
+      return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable };
+    }
+  }
+
+  return state;
+};
+
+/**
+ * Global Explore reducer that handles multiple Explore areas (left and right).
+ * Actions that have an `exploreId` get routed to the ExploreItemReducer.
+ */
+export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => {
+  switch (action.type) {
+    case ActionTypes.SplitClose: {
+      return {
+        ...state,
+        split: false,
+      };
+    }
+
+    case ActionTypes.SplitOpen: {
+      return {
+        ...state,
+        split: true,
+        right: action.payload.itemState,
+      };
+    }
+
+    case ActionTypes.InitializeExploreSplit: {
+      return {
+        ...state,
+        split: true,
+      };
+    }
+  }
+
+  if (action.payload) {
+    const { exploreId } = action.payload as any;
+    if (exploreId !== undefined) {
+      const exploreItemState = state[exploreId];
+      return {
+        ...state,
+        [exploreId]: itemReducer(exploreItemState, action),
+      };
+    }
+  }
+
+  return state;
+};
+
+export default {
+  explore: exploreReducer,
+};

+ 1 - 1
public/app/features/templating/variable_srv.ts

@@ -132,7 +132,7 @@ export class VariableSrv {
 
 
     return this.$q.all(promises).then(() => {
     return this.$q.all(promises).then(() => {
       if (emitChangeEvents) {
       if (emitChangeEvents) {
-        this.$rootScope.$emit('template-variable-value-updated');
+        this.$rootScope.appEvent('template-variable-value-updated');
         this.dashboard.startRefresh();
         this.dashboard.startRefresh();
       }
       }
     });
     });

+ 22 - 5
public/app/plugins/datasource/elasticsearch/elastic_response.ts

@@ -88,6 +88,7 @@ export class ElasticResponse {
             datapoints: [],
             datapoints: [],
             metric: metric.type,
             metric: metric.type,
             field: metric.field,
             field: metric.field,
+            metricId: metric.id,
             props: props,
             props: props,
           };
           };
           for (i = 0; i < esAgg.buckets.length; i++) {
           for (i = 0; i < esAgg.buckets.length; i++) {
@@ -240,7 +241,7 @@ export class ElasticResponse {
           return metricName;
           return metricName;
         }
         }
         if (group === 'field') {
         if (group === 'field') {
-          return series.field;
+          return series.field || '';
         }
         }
 
 
         return match;
         return match;
@@ -248,11 +249,27 @@ export class ElasticResponse {
     }
     }
 
 
     if (series.field && queryDef.isPipelineAgg(series.metric)) {
     if (series.field && queryDef.isPipelineAgg(series.metric)) {
-      const appliedAgg = _.find(target.metrics, { id: series.field });
-      if (appliedAgg) {
-        metricName += ' ' + queryDef.describeMetric(appliedAgg);
+      if (series.metric && queryDef.isPipelineAggWithMultipleBucketPaths(series.metric)) {
+        const agg = _.find(target.metrics, { id: series.metricId });
+        if (agg && agg.settings.script) {
+          metricName = agg.settings.script;
+
+          for (const pv of agg.pipelineVariables) {
+            const appliedAgg = _.find(target.metrics, { id: pv.pipelineAgg });
+            if (appliedAgg) {
+              metricName = metricName.replace('params.' + pv.name, queryDef.describeMetric(appliedAgg));
+            }
+          }
+        } else {
+          metricName = 'Unset';
+        }
       } else {
       } else {
-        metricName = 'Unset';
+        const appliedAgg = _.find(target.metrics, { id: series.field });
+        if (appliedAgg) {
+          metricName += ' ' + queryDef.describeMetric(appliedAgg);
+        } else {
+          metricName = 'Unset';
+        }
       }
       }
     } else if (series.field) {
     } else if (series.field) {
       metricName += ' ' + series.field;
       metricName += ' ' + series.field;

+ 16 - 2
public/app/plugins/datasource/elasticsearch/metric_agg.ts

@@ -35,11 +35,20 @@ export class ElasticMetricAggCtrl {
       $scope.isFirst = $scope.index === 0;
       $scope.isFirst = $scope.index === 0;
       $scope.isSingle = metricAggs.length === 1;
       $scope.isSingle = metricAggs.length === 1;
       $scope.settingsLinkText = '';
       $scope.settingsLinkText = '';
+      $scope.variablesLinkText = '';
       $scope.aggDef = _.find($scope.metricAggTypes, { value: $scope.agg.type });
       $scope.aggDef = _.find($scope.metricAggTypes, { value: $scope.agg.type });
 
 
       if (queryDef.isPipelineAgg($scope.agg.type)) {
       if (queryDef.isPipelineAgg($scope.agg.type)) {
-        $scope.agg.pipelineAgg = $scope.agg.pipelineAgg || 'select metric';
-        $scope.agg.field = $scope.agg.pipelineAgg;
+        if (queryDef.isPipelineAggWithMultipleBucketPaths($scope.agg.type)) {
+          $scope.variablesLinkText = 'Options';
+
+          if ($scope.agg.settings.script) {
+            $scope.variablesLinkText = 'Script: ' + $scope.agg.settings.script.replace(new RegExp('params.', 'g'), '');
+          }
+        } else {
+          $scope.agg.pipelineAgg = $scope.agg.pipelineAgg || 'select metric';
+          $scope.agg.field = $scope.agg.pipelineAgg;
+        }
 
 
         const pipelineOptions = queryDef.getPipelineOptions($scope.agg);
         const pipelineOptions = queryDef.getPipelineOptions($scope.agg);
         if (pipelineOptions.length > 0) {
         if (pipelineOptions.length > 0) {
@@ -119,6 +128,10 @@ export class ElasticMetricAggCtrl {
       $scope.updatePipelineAggOptions();
       $scope.updatePipelineAggOptions();
     };
     };
 
 
+    $scope.toggleVariables = () => {
+      $scope.showVariables = !$scope.showVariables;
+    };
+
     $scope.onChangeInternal = () => {
     $scope.onChangeInternal = () => {
       $scope.onChange();
       $scope.onChange();
     };
     };
@@ -152,6 +165,7 @@ export class ElasticMetricAggCtrl {
         $scope.target.bucketAggs = [queryDef.defaultBucketAgg()];
         $scope.target.bucketAggs = [queryDef.defaultBucketAgg()];
       }
       }
 
 
+      $scope.showVariables = queryDef.isPipelineAggWithMultipleBucketPaths($scope.agg.type);
       $scope.updatePipelineAggOptions();
       $scope.updatePipelineAggOptions();
       $scope.onChange();
       $scope.onChange();
     };
     };

+ 26 - 2
public/app/plugins/datasource/elasticsearch/partials/metric_agg.html

@@ -13,7 +13,17 @@
 	<div class="gf-form">
 	<div class="gf-form">
 		<metric-segment-model property="agg.type" options="metricAggTypes" on-change="onTypeChange()" custom="false" css-class="width-10"></metric-segment-model>
 		<metric-segment-model property="agg.type" options="metricAggTypes" on-change="onTypeChange()" custom="false" css-class="width-10"></metric-segment-model>
 		<metric-segment-model ng-if="aggDef.requiresField" property="agg.field" get-options="getFieldsInternal()" on-change="onChange()" css-class="width-12"></metric-segment-model>
 		<metric-segment-model ng-if="aggDef.requiresField" property="agg.field" get-options="getFieldsInternal()" on-change="onChange()" css-class="width-12"></metric-segment-model>
-		<metric-segment-model ng-if="aggDef.isPipelineAgg" property="agg.pipelineAgg" options="pipelineAggOptions" on-change="onChangeInternal()" custom="false" css-class="width-12"></metric-segment-model>
+		<metric-segment-model ng-if="aggDef.isPipelineAgg && !aggDef.supportsMultipleBucketPaths" property="agg.pipelineAgg" options="pipelineAggOptions" on-change="onChangeInternal()" custom="false" css-class="width-12"></metric-segment-model>
+	</div>
+
+  <div class="gf-form gf-form--grow" ng-if="aggDef.isPipelineAgg && aggDef.supportsMultipleBucketPaths">
+		<label class="gf-form-label gf-form-label--grow">
+			<a ng-click="toggleVariables()">
+				<i class="fa fa-caret-down" ng-show="showVariables"></i>
+				<i class="fa fa-caret-right" ng-hide="showVariables"></i>
+        {{variablesLinkText}}
+			</a>
+		</label>
 	</div>
 	</div>
 
 
 	<div class="gf-form gf-form--grow">
 	<div class="gf-form gf-form--grow">
@@ -36,6 +46,20 @@
 	</div>
 	</div>
 </div>
 </div>
 
 
+<div class="gf-form-group" ng-if="showVariables">
+	<elastic-pipeline-variables variables="agg.pipelineVariables" options="pipelineAggOptions" on-change="onChangeInternal()" />
+  <div class="gf-form offset-width-7">
+    <label class="gf-form-label width-10">
+      Script
+      <info-popover mode="right-normal">
+        Elasticsearch v5.0 and above: Scripting language is Painless. Use <i>params.&lt;var&gt;</i> to reference a variable.<br/><br/>
+        Elasticsearch pre-v5.0: Scripting language is per default Groovy if not changed. For Groovy use <i>&lt;var&gt;</i> to reference a variable.
+      </info-popover>
+    </label>
+    <input type="text" class="gf-form-input max-width-24" empty-to-null ng-model="agg.settings.script" ng-blur="onChangeInternal()" spellcheck='false' placeholder="params.var1 / params.var2">
+  </div>
+</div>
+
 <div class="gf-form-group" ng-if="showOptions">
 <div class="gf-form-group" ng-if="showOptions">
 	<div class="gf-form offset-width-7" ng-if="agg.type === 'derivative'">
 	<div class="gf-form offset-width-7" ng-if="agg.type === 'derivative'">
 		<label class="gf-form-label width-10">Unit</label>
 		<label class="gf-form-label width-10">Unit</label>
@@ -103,5 +127,5 @@
 			<tip>The missing parameter defines how documents that are missing a value should be treated. By default they will be ignored but it is also possible to treat them as if they had a value</tip>
 			<tip>The missing parameter defines how documents that are missing a value should be treated. By default they will be ignored but it is also possible to treat them as if they had a value</tip>
 		</label>
 		</label>
 		<input type="number" class="gf-form-input max-width-12" empty-to-null ng-model="agg.settings.missing" ng-blur="onChangeInternal()" spellcheck='false'>
 		<input type="number" class="gf-form-input max-width-12" empty-to-null ng-model="agg.settings.missing" ng-blur="onChangeInternal()" spellcheck='false'>
-	</div>
+  </div>
 </div>
 </div>

+ 20 - 0
public/app/plugins/datasource/elasticsearch/partials/pipeline_variables.html

@@ -0,0 +1,20 @@
+<div ng-repeat="var in variables">
+  <div class="gf-form offset-width-7" ng-if="$index === 0">
+    <label class="gf-form-label width-10">Variables</label>
+    <input type="text" class="gf-form-input max-width-12" ng-model="var.name" placeholder="Variable name" ng-blur="onChangeInternal()" spellcheck='false'>
+    <metric-segment-model property="var.pipelineAgg" options="options" on-change="onChangeInternal()" custom="false" css-class="width-12"></metric-segment-model>
+    <label class="gf-form-label">
+      <a class="pointer" ng-click="remove($index)"><i class="fa fa-minus"></i></a>
+    </label>
+    <label class="gf-form-label">
+      <a class="pointer" ng-click="add()"><i class="fa fa-plus"></i></a>
+    </label>
+  </div>
+  <div class="gf-form offset-width-17" ng-if="$index !== 0">
+    <input type="text" class="gf-form-input max-width-12" ng-model="var.name" placeholder="Variable name" ng-blur="onChangeInternal()" spellcheck='false'>
+    <metric-segment-model property="var.pipelineAgg" options="options" on-change="onChangeInternal()" custom="false" css-class="width-12"></metric-segment-model>
+    <label class="gf-form-label">
+      <a class="pointer" ng-click="remove($index)"><i class="fa fa-minus"></i></a>
+    </label>
+  </div>
+</div>

+ 45 - 0
public/app/plugins/datasource/elasticsearch/pipeline_variables.ts

@@ -0,0 +1,45 @@
+import coreModule from 'app/core/core_module';
+import _ from 'lodash';
+
+export function elasticPipelineVariables() {
+  return {
+    templateUrl: 'public/app/plugins/datasource/elasticsearch/partials/pipeline_variables.html',
+    controller: 'ElasticPipelineVariablesCtrl',
+    restrict: 'E',
+    scope: {
+      onChange: '&',
+      variables: '=',
+      options: '=',
+    },
+  };
+}
+
+const newVariable = index => {
+  return {
+    name: 'var' + index,
+    pipelineAgg: 'select metric',
+  };
+};
+
+export class ElasticPipelineVariablesCtrl {
+  constructor($scope) {
+    $scope.variables = $scope.variables || [newVariable(1)];
+
+    $scope.onChangeInternal = () => {
+      $scope.onChange();
+    };
+
+    $scope.add = () => {
+      $scope.variables.push(newVariable($scope.variables.length + 1));
+      $scope.onChange();
+    };
+
+    $scope.remove = index => {
+      $scope.variables.splice(index, 1);
+      $scope.onChange();
+    };
+  }
+}
+
+coreModule.directive('elasticPipelineVariables', elasticPipelineVariables);
+coreModule.controller('ElasticPipelineVariablesCtrl', ElasticPipelineVariablesCtrl);

+ 34 - 9
public/app/plugins/datasource/elasticsearch/query_builder.ts

@@ -189,7 +189,7 @@ export class ElasticQueryBuilder {
     target.bucketAggs = target.bucketAggs || [queryDef.defaultBucketAgg()];
     target.bucketAggs = target.bucketAggs || [queryDef.defaultBucketAgg()];
     target.timeField = this.timeField;
     target.timeField = this.timeField;
 
 
-    let i, nestedAggs, metric;
+    let i, j, pv, nestedAggs, metric;
     const query = {
     const query = {
       size: 0,
       size: 0,
       query: {
       query: {
@@ -269,17 +269,42 @@ export class ElasticQueryBuilder {
       let metricAgg = null;
       let metricAgg = null;
 
 
       if (queryDef.isPipelineAgg(metric.type)) {
       if (queryDef.isPipelineAgg(metric.type)) {
-        if (metric.pipelineAgg && /^\d*$/.test(metric.pipelineAgg)) {
-          const appliedAgg = queryDef.findMetricById(target.metrics, metric.pipelineAgg);
-          if (appliedAgg) {
-            if (appliedAgg.type === 'count') {
-              metricAgg = { buckets_path: '_count' };
-            } else {
-              metricAgg = { buckets_path: metric.pipelineAgg };
+        if (queryDef.isPipelineAggWithMultipleBucketPaths(metric.type)) {
+          if (metric.pipelineVariables) {
+            metricAgg = {
+              buckets_path: {},
+            };
+
+            for (j = 0; j < metric.pipelineVariables.length; j++) {
+              pv = metric.pipelineVariables[j];
+
+              if (pv.name && pv.pipelineAgg && /^\d*$/.test(pv.pipelineAgg)) {
+                const appliedAgg = queryDef.findMetricById(target.metrics, pv.pipelineAgg);
+                if (appliedAgg) {
+                  if (appliedAgg.type === 'count') {
+                    metricAgg.buckets_path[pv.name] = '_count';
+                  } else {
+                    metricAgg.buckets_path[pv.name] = pv.pipelineAgg;
+                  }
+                }
+              }
             }
             }
+          } else {
+            continue;
           }
           }
         } else {
         } else {
-          continue;
+          if (metric.pipelineAgg && /^\d*$/.test(metric.pipelineAgg)) {
+            const appliedAgg = queryDef.findMetricById(target.metrics, metric.pipelineAgg);
+            if (appliedAgg) {
+              if (appliedAgg.type === 'count') {
+                metricAgg = { buckets_path: '_count' };
+              } else {
+                metricAgg = { buckets_path: metric.pipelineAgg };
+              }
+            }
+          } else {
+            continue;
+          }
         }
         }
       } else {
       } else {
         metricAgg = { field: metric.field };
         metricAgg = { field: metric.field };

+ 4 - 0
public/app/plugins/datasource/elasticsearch/query_ctrl.ts

@@ -1,5 +1,6 @@
 import './bucket_agg';
 import './bucket_agg';
 import './metric_agg';
 import './metric_agg';
+import './pipeline_variables';
 
 
 import angular from 'angular';
 import angular from 'angular';
 import _ from 'lodash';
 import _ from 'lodash';
@@ -70,6 +71,9 @@ export class ElasticQueryCtrl extends QueryCtrl {
       if (aggDef.requiresField) {
       if (aggDef.requiresField) {
         text += metric.field;
         text += metric.field;
       }
       }
+      if (aggDef.supportsMultipleBucketPaths) {
+        text += metric.settings.script.replace(new RegExp('params.', 'g'), '');
+      }
       text += '), ';
       text += '), ';
     });
     });
 
 

+ 17 - 0
public/app/plugins/datasource/elasticsearch/query_def.ts

@@ -64,6 +64,14 @@ export const metricAggTypes = [
     isPipelineAgg: true,
     isPipelineAgg: true,
     minVersion: 2,
     minVersion: 2,
   },
   },
+  {
+    text: 'Bucket Script',
+    value: 'bucket_script',
+    requiresField: false,
+    isPipelineAgg: true,
+    supportsMultipleBucketPaths: true,
+    minVersion: 2,
+  },
   { text: 'Raw Document', value: 'raw_document', requiresField: false },
   { text: 'Raw Document', value: 'raw_document', requiresField: false },
 ];
 ];
 
 
@@ -128,6 +136,7 @@ export const pipelineOptions = {
     { text: 'minimize', default: false },
     { text: 'minimize', default: false },
   ],
   ],
   derivative: [{ text: 'unit', default: undefined }],
   derivative: [{ text: 'unit', default: undefined }],
+  bucket_script: [],
 };
 };
 
 
 export const movingAvgModelSettings = {
 export const movingAvgModelSettings = {
@@ -171,6 +180,14 @@ export function isPipelineAgg(metricType) {
   return false;
   return false;
 }
 }
 
 
+export function isPipelineAggWithMultipleBucketPaths(metricType) {
+  if (metricType) {
+    return metricAggTypes.find(t => t.value === metricType && t.supportsMultipleBucketPaths) !== undefined;
+  }
+
+  return false;
+}
+
 export function getPipelineAggOptions(targets) {
 export function getPipelineAggOptions(targets) {
   const result = [];
   const result = [];
   _.each(targets.metrics, metric => {
   _.each(targets.metrics, metric => {

+ 66 - 0
public/app/plugins/datasource/elasticsearch/specs/elastic_response.test.ts

@@ -665,4 +665,70 @@ describe('ElasticResponse', () => {
       expect(result.data[0].datapoints[0].fieldProp).toBe('field');
       expect(result.data[0].datapoints[0].fieldProp).toBe('field');
     });
     });
   });
   });
+
+  describe('with bucket_script ', () => {
+    let result;
+
+    beforeEach(() => {
+      targets = [
+        {
+          refId: 'A',
+          metrics: [
+            { id: '1', type: 'sum', field: '@value' },
+            { id: '3', type: 'max', field: '@value' },
+            {
+              id: '4',
+              field: 'select field',
+              pipelineVariables: [{ name: 'var1', pipelineAgg: '1' }, { name: 'var2', pipelineAgg: '3' }],
+              settings: { script: 'params.var1 * params.var2' },
+              type: 'bucket_script',
+            },
+          ],
+          bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '2' }],
+        },
+      ];
+      response = {
+        responses: [
+          {
+            aggregations: {
+              '2': {
+                buckets: [
+                  {
+                    1: { value: 2 },
+                    3: { value: 3 },
+                    4: { value: 6 },
+                    doc_count: 60,
+                    key: 1000,
+                  },
+                  {
+                    1: { value: 3 },
+                    3: { value: 4 },
+                    4: { value: 12 },
+                    doc_count: 60,
+                    key: 2000,
+                  },
+                ],
+              },
+            },
+          },
+        ],
+      };
+
+      result = new ElasticResponse(targets, response).getTimeSeries();
+    });
+
+    it('should return 3 series', () => {
+      expect(result.data.length).toBe(3);
+      expect(result.data[0].datapoints.length).toBe(2);
+      expect(result.data[0].target).toBe('Sum @value');
+      expect(result.data[1].target).toBe('Max @value');
+      expect(result.data[2].target).toBe('Sum @value * Max @value');
+      expect(result.data[0].datapoints[0][0]).toBe(2);
+      expect(result.data[1].datapoints[0][0]).toBe(3);
+      expect(result.data[2].datapoints[0][0]).toBe(6);
+      expect(result.data[0].datapoints[1][0]).toBe(3);
+      expect(result.data[1].datapoints[1][0]).toBe(4);
+      expect(result.data[2].datapoints[1][0]).toBe(12);
+    });
+  });
 });
 });

+ 77 - 0
public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts

@@ -353,6 +353,83 @@ describe('ElasticQueryBuilder', () => {
     expect(firstLevel.aggs['2'].derivative.buckets_path).toBe('_count');
     expect(firstLevel.aggs['2'].derivative.buckets_path).toBe('_count');
   });
   });
 
 
+  it('with bucket_script', () => {
+    const query = builder.build({
+      metrics: [
+        {
+          id: '1',
+          type: 'sum',
+          field: '@value',
+        },
+        {
+          id: '3',
+          type: 'max',
+          field: '@value',
+        },
+        {
+          field: 'select field',
+          id: '4',
+          meta: {},
+          pipelineVariables: [
+            {
+              name: 'var1',
+              pipelineAgg: '1',
+            },
+            {
+              name: 'var2',
+              pipelineAgg: '3',
+            },
+          ],
+          settings: {
+            script: 'params.var1 * params.var2',
+          },
+          type: 'bucket_script',
+        },
+      ],
+      bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '2' }],
+    });
+
+    const firstLevel = query.aggs['2'];
+
+    expect(firstLevel.aggs['4']).not.toBe(undefined);
+    expect(firstLevel.aggs['4'].bucket_script).not.toBe(undefined);
+    expect(firstLevel.aggs['4'].bucket_script.buckets_path).toMatchObject({ var1: '1', var2: '3' });
+  });
+
+  it('with bucket_script doc count', () => {
+    const query = builder.build({
+      metrics: [
+        {
+          id: '3',
+          type: 'count',
+          field: 'select field',
+        },
+        {
+          field: 'select field',
+          id: '4',
+          meta: {},
+          pipelineVariables: [
+            {
+              name: 'var1',
+              pipelineAgg: '3',
+            },
+          ],
+          settings: {
+            script: 'params.var1 * 1000',
+          },
+          type: 'bucket_script',
+        },
+      ],
+      bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '2' }],
+    });
+
+    const firstLevel = query.aggs['2'];
+
+    expect(firstLevel.aggs['4']).not.toBe(undefined);
+    expect(firstLevel.aggs['4'].bucket_script).not.toBe(undefined);
+    expect(firstLevel.aggs['4'].bucket_script.buckets_path).toMatchObject({ var1: '_count' });
+  });
+
   it('with histogram', () => {
   it('with histogram', () => {
     const query = builder.build({
     const query = builder.build({
       metrics: [{ id: '1', type: 'count' }],
       metrics: [{ id: '1', type: 'count' }],

+ 20 - 2
public/app/plugins/datasource/elasticsearch/specs/query_def.test.ts

@@ -65,6 +65,24 @@ describe('ElasticQueryDef', () => {
     });
     });
   });
   });
 
 
+  describe('isPipelineAggWithMultipleBucketPaths', () => {
+    describe('bucket_script', () => {
+      const result = queryDef.isPipelineAggWithMultipleBucketPaths('bucket_script');
+
+      test('should have multiple bucket paths support', () => {
+        expect(result).toBe(true);
+      });
+    });
+
+    describe('moving_avg', () => {
+      const result = queryDef.isPipelineAggWithMultipleBucketPaths('moving_avg');
+
+      test('should not have multiple bucket paths support', () => {
+        expect(result).toBe(false);
+      });
+    });
+  });
+
   describe('pipeline aggs depending on esverison', () => {
   describe('pipeline aggs depending on esverison', () => {
     describe('using esversion undefined', () => {
     describe('using esversion undefined', () => {
       test('should not get pipeline aggs', () => {
       test('should not get pipeline aggs', () => {
@@ -80,13 +98,13 @@ describe('ElasticQueryDef', () => {
 
 
     describe('using esversion 2', () => {
     describe('using esversion 2', () => {
       test('should get pipeline aggs', () => {
       test('should get pipeline aggs', () => {
-        expect(queryDef.getMetricAggTypes(2).length).toBe(11);
+        expect(queryDef.getMetricAggTypes(2).length).toBe(12);
       });
       });
     });
     });
 
 
     describe('using esversion 5', () => {
     describe('using esversion 5', () => {
       test('should get pipeline aggs', () => {
       test('should get pipeline aggs', () => {
-        expect(queryDef.getMetricAggTypes(5).length).toBe(11);
+        expect(queryDef.getMetricAggTypes(5).length).toBe(12);
       });
       });
     });
     });
   });
   });

+ 6 - 4
public/app/plugins/datasource/prometheus/datasource.ts

@@ -219,8 +219,9 @@ export class PrometheusDatasource {
     };
     };
     const range = Math.ceil(end - start);
     const range = Math.ceil(end - start);
 
 
+    // options.interval is the dynamically calculated interval
     let interval = kbn.interval_to_seconds(options.interval);
     let interval = kbn.interval_to_seconds(options.interval);
-    // Minimum interval ("Min step"), if specified for the query. or same as interval otherwise
+    // Minimum interval ("Min step"), if specified for the query or datasource. or same as interval otherwise
     const minInterval = kbn.interval_to_seconds(
     const minInterval = kbn.interval_to_seconds(
       this.templateSrv.replace(target.interval, options.scopedVars) || options.interval
       this.templateSrv.replace(target.interval, options.scopedVars) || options.interval
     );
     );
@@ -366,12 +367,13 @@ export class PrometheusDatasource {
     const step = annotation.step || '60s';
     const step = annotation.step || '60s';
     const start = this.getPrometheusTime(options.range.from, false);
     const start = this.getPrometheusTime(options.range.from, false);
     const end = this.getPrometheusTime(options.range.to, true);
     const end = this.getPrometheusTime(options.range.to, true);
-    // Unsetting min interval
     const queryOptions = {
     const queryOptions = {
       ...options,
       ...options,
-      interval: '0s',
+      interval: step,
     };
     };
-    const query = this.createQuery({ expr, interval: step }, queryOptions, start, end);
+    // Unsetting min interval for accurate event resolution
+    const minStep = '1s';
+    const query = this.createQuery({ expr, interval: minStep }, queryOptions, start, end);
 
 
     const self = this;
     const self = this;
     return this.performTimeSeriesQuery(query, query.start, query.end).then(results => {
     return this.performTimeSeriesQuery(query, query.start, query.end).then(results => {

+ 73 - 0
public/app/plugins/datasource/prometheus/specs/datasource.test.ts

@@ -577,6 +577,79 @@ describe('PrometheusDatasource', () => {
         expect(results[0].time).toEqual(1);
         expect(results[0].time).toEqual(1);
       });
       });
     });
     });
+
+    describe('step parameter', () => {
+      beforeEach(() => {
+        backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response));
+        ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv);
+      });
+
+      it('should use default step for short range if no interval is given', () => {
+        const query = {
+          ...options,
+          range: {
+            from: time({ seconds: 63 }),
+            to: time({ seconds: 123 }),
+          },
+        };
+        ctx.ds.annotationQuery(query);
+        const req = backendSrv.datasourceRequest.mock.calls[0][0];
+        expect(req.url).toContain('step=60');
+      });
+
+      it('should use custom step for short range', () => {
+        const annotation = {
+          ...options.annotation,
+          step: '10s',
+        };
+        const query = {
+          ...options,
+          annotation,
+          range: {
+            from: time({ seconds: 63 }),
+            to: time({ seconds: 123 }),
+          },
+        };
+        ctx.ds.annotationQuery(query);
+        const req = backendSrv.datasourceRequest.mock.calls[0][0];
+        expect(req.url).toContain('step=10');
+      });
+
+      it('should use custom step for short range', () => {
+        const annotation = {
+          ...options.annotation,
+          step: '10s',
+        };
+        const query = {
+          ...options,
+          annotation,
+          range: {
+            from: time({ seconds: 63 }),
+            to: time({ seconds: 123 }),
+          },
+        };
+        ctx.ds.annotationQuery(query);
+        const req = backendSrv.datasourceRequest.mock.calls[0][0];
+        expect(req.url).toContain('step=10');
+      });
+
+      it('should use dynamic step on long ranges if no option was given', () => {
+        const query = {
+          ...options,
+          range: {
+            from: time({ seconds: 63 }),
+            to: time({ hours: 24 * 30, seconds: 63 }),
+          },
+        };
+        ctx.ds.annotationQuery(query);
+        const req = backendSrv.datasourceRequest.mock.calls[0][0];
+        // Range in seconds: (to - from) / 1000
+        // Max_datapoints: 11000
+        // Step: range / max_datapoints
+        const step = 236;
+        expect(req.url).toContain(`step=${step}`);
+      });
+    });
   });
   });
 
 
   describe('When resultFormat is table and instant = true', () => {
   describe('When resultFormat is table and instant = true', () => {

+ 9 - 22
public/app/plugins/datasource/stackdriver/annotations_query_ctrl.ts

@@ -1,31 +1,18 @@
-import _ from 'lodash';
-import './query_filter_ctrl';
+import { TemplateSrv } from 'app/features/templating/template_srv';
 
 
 export class StackdriverAnnotationsQueryCtrl {
 export class StackdriverAnnotationsQueryCtrl {
   static templateUrl = 'partials/annotations.editor.html';
   static templateUrl = 'partials/annotations.editor.html';
   annotation: any;
   annotation: any;
-  datasource: any;
-
-  defaultDropdownValue = 'Select Metric';
-  defaultServiceValue = 'All Services';
-
-  defaults = {
-    project: {
-      id: 'default',
-      name: 'loading project...',
-    },
-    metricType: this.defaultDropdownValue,
-    service: this.defaultServiceValue,
-    metric: '',
-    filters: [],
-    metricKind: '',
-    valueType: '',
-  };
+  templateSrv: TemplateSrv;
 
 
   /** @ngInject */
   /** @ngInject */
-  constructor() {
+  constructor(templateSrv) {
+    this.templateSrv = templateSrv;
     this.annotation.target = this.annotation.target || {};
     this.annotation.target = this.annotation.target || {};
-    this.annotation.target.refId = 'annotationQuery';
-    _.defaultsDeep(this.annotation.target, this.defaults);
+    this.onQueryChange = this.onQueryChange.bind(this);
+  }
+
+  onQueryChange(target) {
+    Object.assign(this.annotation.target, target);
   }
   }
 }
 }

+ 57 - 0
public/app/plugins/datasource/stackdriver/components/Aggregations.test.tsx

@@ -0,0 +1,57 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { Aggregations, Props } from './Aggregations';
+import { shallow } from 'enzyme';
+import { ValueTypes, MetricKind } from '../constants';
+import { TemplateSrvStub } from 'test/specs/helpers';
+
+const props: Props = {
+  onChange: () => {},
+  templateSrv: new TemplateSrvStub(),
+  metricDescriptor: {
+    valueType: '',
+    metricKind: '',
+  },
+  crossSeriesReducer: '',
+  groupBys: [],
+  children: renderProps => <div />,
+};
+
+describe('Aggregations', () => {
+  let wrapper;
+  it('renders correctly', () => {
+    const tree = renderer.create(<Aggregations {...props} />).toJSON();
+    expect(tree).toMatchSnapshot();
+  });
+
+  describe('options', () => {
+    describe('when DOUBLE and DELTA is passed as props', () => {
+      beforeEach(() => {
+        const newProps = { ...props, metricDescriptor: { valueType: ValueTypes.DOUBLE, metricKind: MetricKind.GAUGE } };
+        wrapper = shallow(<Aggregations {...newProps} />);
+      });
+      it('', () => {
+        const options = wrapper.state().aggOptions[0].options;
+        expect(options.length).toEqual(11);
+        expect(options.map(o => o.value)).toEqual(
+          expect.not.arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE'])
+        );
+      });
+    });
+
+    describe('when MONEY and CUMULATIVE is passed as props', () => {
+      beforeEach(() => {
+        const newProps = {
+          ...props,
+          metricDescriptor: { valueType: ValueTypes.MONEY, metricKind: MetricKind.CUMULATIVE },
+        };
+        wrapper = shallow(<Aggregations {...newProps} />);
+      });
+      it('', () => {
+        const options = wrapper.state().aggOptions[0].options;
+        expect(options.length).toEqual(5);
+        expect(options.map(o => o.value)).toEqual(expect.arrayContaining(['REDUCE_NONE']));
+      });
+    });
+  });
+});

+ 94 - 0
public/app/plugins/datasource/stackdriver/components/Aggregations.tsx

@@ -0,0 +1,94 @@
+import React from 'react';
+import _ from 'lodash';
+
+import { MetricSelect } from 'app/core/components/Select/MetricSelect';
+import { getAggregationOptionsByMetric } from '../functions';
+import { TemplateSrv } from 'app/features/templating/template_srv';
+
+export interface Props {
+  onChange: (metricDescriptor) => void;
+  templateSrv: TemplateSrv;
+  metricDescriptor: {
+    valueType: string;
+    metricKind: string;
+  };
+  crossSeriesReducer: string;
+  groupBys: string[];
+  children?: (renderProps: any) => JSX.Element;
+}
+
+export interface State {
+  aggOptions: any[];
+  displayAdvancedOptions: boolean;
+}
+
+export class Aggregations extends React.Component<Props, State> {
+  state: State = {
+    aggOptions: [],
+    displayAdvancedOptions: false,
+  };
+
+  componentDidMount() {
+    this.setAggOptions(this.props);
+  }
+
+  componentWillReceiveProps(nextProps: Props) {
+    this.setAggOptions(nextProps);
+  }
+
+  setAggOptions({ metricDescriptor }: Props) {
+    let aggOptions = [];
+    if (metricDescriptor) {
+      aggOptions = [
+        {
+          label: 'Aggregations',
+          expanded: true,
+          options: getAggregationOptionsByMetric(metricDescriptor.valueType, metricDescriptor.metricKind).map(a => ({
+            ...a,
+            label: a.text,
+          })),
+        },
+      ];
+    }
+    this.setState({ aggOptions });
+  }
+
+  onToggleDisplayAdvanced = () => {
+    this.setState(state => ({
+      displayAdvancedOptions: !state.displayAdvancedOptions,
+    }));
+  };
+
+  render() {
+    const { displayAdvancedOptions, aggOptions } = this.state;
+    const { templateSrv, onChange, crossSeriesReducer } = this.props;
+
+    return (
+      <>
+        <div className="gf-form-inline">
+          <div className="gf-form">
+            <label className="gf-form-label query-keyword width-9">Aggregation</label>
+            <MetricSelect
+              onChange={onChange}
+              value={crossSeriesReducer}
+              variables={templateSrv.variables}
+              options={aggOptions}
+              placeholder="Select Aggregation"
+              className="width-15"
+            />
+          </div>
+          <div className="gf-form gf-form--grow">
+            <label className="gf-form-label gf-form-label--grow">
+              <a onClick={this.onToggleDisplayAdvanced}>
+                <>
+                  <i className={`fa fa-caret-${displayAdvancedOptions ? 'down' : 'right'}`} /> Advanced Options
+                </>
+              </a>
+            </label>
+          </div>
+        </div>
+        {this.props.children(this.state.displayAdvancedOptions)}
+      </>
+    );
+  }
+}

+ 52 - 0
public/app/plugins/datasource/stackdriver/components/AliasBy.tsx

@@ -0,0 +1,52 @@
+import React, { Component } from 'react';
+import { debounce } from 'lodash';
+
+export interface Props {
+  onChange: (alignmentPeriod) => void;
+  value: string;
+}
+
+export interface State {
+  value: string;
+}
+
+export class AliasBy extends Component<Props, State> {
+  propagateOnChange: (value) => void;
+
+  constructor(props) {
+    super(props);
+    this.propagateOnChange = debounce(this.props.onChange, 500);
+    this.state = { value: '' };
+  }
+
+  componentDidMount() {
+    this.setState({ value: this.props.value });
+  }
+
+  componentWillReceiveProps(nextProps: Props) {
+    if (nextProps.value !== this.props.value) {
+      this.setState({ value: nextProps.value });
+    }
+  }
+
+  onChange = e => {
+    this.setState({ value: e.target.value });
+    this.propagateOnChange(e.target.value);
+  };
+
+  render() {
+    return (
+      <>
+        <div className="gf-form-inline">
+          <div className="gf-form">
+            <label className="gf-form-label query-keyword width-9">Alias By</label>
+            <input type="text" className="gf-form-input width-24" value={this.state.value} onChange={this.onChange} />
+          </div>
+          <div className="gf-form gf-form--grow">
+            <div className="gf-form-label gf-form-label--grow" />
+          </div>
+        </div>
+      </>
+    );
+  }
+}

+ 56 - 0
public/app/plugins/datasource/stackdriver/components/AlignmentPeriods.tsx

@@ -0,0 +1,56 @@
+import React, { SFC } from 'react';
+import _ from 'lodash';
+
+import kbn from 'app/core/utils/kbn';
+import { MetricSelect } from 'app/core/components/Select/MetricSelect';
+import { alignmentPeriods, alignOptions } from '../constants';
+import { TemplateSrv } from 'app/features/templating/template_srv';
+
+export interface Props {
+  onChange: (alignmentPeriod) => void;
+  templateSrv: TemplateSrv;
+  alignmentPeriod: string;
+  perSeriesAligner: string;
+  usedAlignmentPeriod: string;
+}
+
+export const AlignmentPeriods: SFC<Props> = ({
+  alignmentPeriod,
+  templateSrv,
+  onChange,
+  perSeriesAligner,
+  usedAlignmentPeriod,
+}) => {
+  const alignment = alignOptions.find(ap => ap.value === templateSrv.replace(perSeriesAligner));
+  const formatAlignmentText = `${kbn.secondsToHms(usedAlignmentPeriod)} interval (${alignment ? alignment.text : ''})`;
+
+  return (
+    <>
+      <div className="gf-form-inline">
+        <div className="gf-form">
+          <label className="gf-form-label query-keyword width-9">Alignment Period</label>
+          <MetricSelect
+            onChange={onChange}
+            value={alignmentPeriod}
+            variables={templateSrv.variables}
+            options={[
+              {
+                label: 'Alignment options',
+                expanded: true,
+                options: alignmentPeriods.map(ap => ({
+                  ...ap,
+                  label: ap.text,
+                })),
+              },
+            ]}
+            placeholder="Select Alignment"
+            className="width-15"
+          />
+        </div>
+        <div className="gf-form gf-form--grow">
+          {usedAlignmentPeriod && <label className="gf-form-label gf-form-label--grow">{formatAlignmentText}</label>}
+        </div>
+      </div>
+    </>
+  );
+};

+ 33 - 0
public/app/plugins/datasource/stackdriver/components/Alignments.tsx

@@ -0,0 +1,33 @@
+import React, { SFC } from 'react';
+import _ from 'lodash';
+
+import { MetricSelect } from 'app/core/components/Select/MetricSelect';
+import { TemplateSrv } from 'app/features/templating/template_srv';
+import { SelectOptionItem } from '@grafana/ui';
+
+export interface Props {
+  onChange: (perSeriesAligner) => void;
+  templateSrv: TemplateSrv;
+  alignOptions: SelectOptionItem[];
+  perSeriesAligner: string;
+}
+
+export const Alignments: SFC<Props> = ({ perSeriesAligner, templateSrv, onChange, alignOptions }) => {
+  return (
+    <>
+      <div className="gf-form-group">
+        <div className="gf-form offset-width-9">
+          <label className="gf-form-label query-keyword width-15">Aligner</label>
+          <MetricSelect
+            onChange={onChange}
+            value={perSeriesAligner}
+            variables={templateSrv.variables}
+            options={alignOptions}
+            placeholder="Select Alignment"
+            className="width-15"
+          />
+        </div>
+      </div>
+    </>
+  );
+};

+ 119 - 0
public/app/plugins/datasource/stackdriver/components/AnnotationQueryEditor.tsx

@@ -0,0 +1,119 @@
+import React from 'react';
+import _ from 'lodash';
+
+import { TemplateSrv } from 'app/features/templating/template_srv';
+
+import StackdriverDatasource from '../datasource';
+import { Metrics } from './Metrics';
+import { Filter } from './Filter';
+import { AnnotationTarget } from '../types';
+import { AnnotationsHelp } from './AnnotationsHelp';
+
+export interface Props {
+  onQueryChange: (target: AnnotationTarget) => void;
+  target: AnnotationTarget;
+  datasource: StackdriverDatasource;
+  templateSrv: TemplateSrv;
+}
+
+interface State extends AnnotationTarget {
+  [key: string]: any;
+}
+
+const DefaultTarget: State = {
+  defaultProject: 'loading project...',
+  metricType: '',
+  filters: [],
+  metricKind: '',
+  valueType: '',
+  refId: 'annotationQuery',
+  title: '',
+  text: '',
+};
+
+export class AnnotationQueryEditor extends React.Component<Props, State> {
+  state: State = DefaultTarget;
+
+  componentDidMount() {
+    this.setState({
+      ...this.props.target,
+    });
+  }
+
+  onMetricTypeChange = ({ valueType, metricKind, type, unit }) => {
+    const { onQueryChange } = this.props;
+    this.setState(
+      {
+        metricType: type,
+        unit,
+        valueType,
+        metricKind,
+      },
+      () => {
+        onQueryChange(this.state);
+      }
+    );
+  };
+
+  onChange(prop, value) {
+    this.setState({ [prop]: value }, () => {
+      this.props.onQueryChange(this.state);
+    });
+  }
+
+  render() {
+    const { defaultProject, metricType, filters, refId, title, text } = this.state;
+    const { datasource, templateSrv } = this.props;
+
+    return (
+      <>
+        <Metrics
+          defaultProject={defaultProject}
+          metricType={metricType}
+          templateSrv={templateSrv}
+          datasource={datasource}
+          onChange={this.onMetricTypeChange}
+        >
+          {metric => (
+            <>
+              <Filter
+                filtersChanged={value => this.onChange('filters', value)}
+                filters={filters}
+                refId={refId}
+                hideGroupBys={true}
+                templateSrv={templateSrv}
+                datasource={datasource}
+                metricType={metric ? metric.type : ''}
+              />
+            </>
+          )}
+        </Metrics>
+        <div className="gf-form gf-form-inline">
+          <div className="gf-form">
+            <span className="gf-form-label query-keyword width-9">Title</span>
+            <input
+              type="text"
+              className="gf-form-input width-20"
+              value={title}
+              onChange={e => this.onChange('title', e.target.value)}
+            />
+          </div>
+          <div className="gf-form">
+            <span className="gf-form-label query-keyword width-9">Text</span>
+            <input
+              type="text"
+              className="gf-form-input width-20"
+              value={text}
+              onChange={e => this.onChange('text', e.target.value)}
+            />
+          </div>
+          <div className="gf-form gf-form--grow">
+            <div className="gf-form-label gf-form-label--grow" />
+          </div>
+        </div>
+
+        <AnnotationsHelp />
+      </>
+    );
+  }
+}

+ 44 - 0
public/app/plugins/datasource/stackdriver/components/AnnotationsHelp.tsx

@@ -0,0 +1,44 @@
+import React, { SFC } from 'react';
+
+export const AnnotationsHelp: SFC = () => {
+  return (
+    <div className="gf-form grafana-info-box" style={{ padding: 0 }}>
+      <pre className="gf-form-pre alert alert-info" style={{ marginRight: 0 }}>
+        <h5>Annotation Query Format</h5>
+        <p>
+          An annotation is an event that is overlaid on top of graphs. Annotation rendering is expensive so it is
+          important to limit the number of rows returned.{' '}
+        </p>
+        <p>
+          The Title and Text fields support templating and can use data returned from the query. For example, the Title
+          field could have the following text:
+        </p>
+        <code>
+          {`${'{{metric.type}}'}`} has value: {`${'{{metric.value}}'}`}
+        </code>
+        <p>
+          Example Result: <code>monitoring.googleapis.com/uptime_check/http_status has this value: 502</code>
+        </p>
+        <label>Patterns:</label>
+        <p>
+          <code>{`${'{{metric.value}}'}`}</code> = value of the metric/point
+        </p>
+        <p>
+          <code>{`${'{{metric.type}}'}`}</code> = metric type e.g. compute.googleapis.com/instance/cpu/usage_time
+        </p>
+        <p>
+          <code>{`${'{{metric.name}}'}`}</code> = name part of metric e.g. instance/cpu/usage_time
+        </p>
+        <p>
+          <code>{`${'{{metric.service}}'}`}</code> = service part of metric e.g. compute
+        </p>
+        <p>
+          <code>{`${'{{metric.label.label_name}}'}`}</code> = Metric label metadata e.g. metric.label.instance_name
+        </p>
+        <p>
+          <code>{`${'{{resource.label.label_name}}'}`}</code> = Resource label metadata e.g. resource.label.zone
+        </p>
+      </pre>
+    </div>
+  );
+};

+ 115 - 0
public/app/plugins/datasource/stackdriver/components/Filter.tsx

@@ -0,0 +1,115 @@
+import React from 'react';
+import _ from 'lodash';
+import appEvents from 'app/core/app_events';
+
+import { QueryMeta } from '../types';
+import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
+import { TemplateSrv } from 'app/features/templating/template_srv';
+import StackdriverDatasource from '../datasource';
+import '../query_filter_ctrl';
+
+export interface Props {
+  filtersChanged: (filters: string[]) => void;
+  groupBysChanged?: (groupBys: string[]) => void;
+  metricType: string;
+  templateSrv: TemplateSrv;
+  groupBys?: string[];
+  filters: string[];
+  datasource: StackdriverDatasource;
+  refId: string;
+  hideGroupBys: boolean;
+}
+
+interface State {
+  labelData: QueryMeta;
+  loading: Promise<any>;
+}
+
+const labelData = {
+  metricLabels: {},
+  resourceLabels: {},
+  resourceTypes: [],
+};
+
+export class Filter extends React.Component<Props, State> {
+  element: any;
+  component: AngularComponent;
+
+  async componentDidMount() {
+    if (!this.element) {
+      return;
+    }
+
+    const { groupBys, filters, hideGroupBys } = this.props;
+    const loader = getAngularLoader();
+
+    const filtersChanged = filters => {
+      this.props.filtersChanged(filters);
+    };
+
+    const groupBysChanged = groupBys => {
+      this.props.groupBysChanged(groupBys);
+    };
+
+    const scopeProps = {
+      loading: null,
+      labelData,
+      groupBys,
+      filters,
+      filtersChanged,
+      groupBysChanged,
+      hideGroupBys,
+    };
+    const loading = this.loadLabels(scopeProps);
+    scopeProps.loading = loading;
+    const template = `<stackdriver-filter
+                        filters="filters"
+                        group-bys="groupBys"
+                        label-data="labelData"
+                        loading="loading"
+                        filters-changed="filtersChanged(filters)"
+                        group-bys-changed="groupBysChanged(groupBys)"
+                        hide-group-bys="hideGroupBys"/>`;
+    this.component = loader.load(this.element, scopeProps, template);
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (!this.element) {
+      return;
+    }
+    const scope = this.component.getScope();
+    if (prevProps.metricType !== this.props.metricType) {
+      scope.loading = this.loadLabels(scope);
+    }
+    scope.filters = this.props.filters;
+    scope.groupBys = this.props.groupBys;
+  }
+
+  componentWillUnmount() {
+    if (this.component) {
+      this.component.destroy();
+    }
+  }
+
+  async loadLabels(scope) {
+    return new Promise(async resolve => {
+      try {
+        if (!this.props.metricType) {
+          scope.labelData = labelData;
+        } else {
+          const { meta } = await this.props.datasource.getLabels(this.props.metricType, this.props.refId);
+          scope.labelData = meta;
+        }
+        resolve();
+      } catch (error) {
+        appEvents.emit('alert-error', ['Error', 'Error loading metric labels for ' + this.props.metricType]);
+        scope.labelData = labelData;
+        resolve();
+      }
+    });
+  }
+
+  render() {
+    return <div ref={element => (this.element = element)} style={{ width: '100%' }} />;
+  }
+}

+ 115 - 0
public/app/plugins/datasource/stackdriver/components/Help.tsx

@@ -0,0 +1,115 @@
+import React from 'react';
+import { Project } from './Project';
+import StackdriverDatasource from '../datasource';
+
+export interface Props {
+  datasource: StackdriverDatasource;
+  rawQuery: string;
+  lastQueryError: string;
+}
+
+interface State {
+  displayHelp: boolean;
+  displaRawQuery: boolean;
+}
+
+export class Help extends React.Component<Props, State> {
+  state: State = {
+    displayHelp: false,
+    displaRawQuery: false,
+  };
+
+  onHelpClicked = () => {
+    this.setState({ displayHelp: !this.state.displayHelp });
+  };
+
+  onRawQueryClicked = () => {
+    this.setState({ displaRawQuery: !this.state.displaRawQuery });
+  };
+
+  shouldComponentUpdate(nextProps) {
+    return nextProps.metricDescriptor !== null;
+  }
+
+  render() {
+    const { displayHelp, displaRawQuery } = this.state;
+    const { datasource, rawQuery, lastQueryError } = this.props;
+
+    return (
+      <>
+        <div className="gf-form-inline">
+          <Project datasource={datasource} />
+          <div className="gf-form" onClick={this.onHelpClicked}>
+            <label className="gf-form-label query-keyword pointer">
+              Show Help <i className={`fa fa-caret-${displayHelp ? 'down' : 'right'}`} />
+            </label>
+          </div>
+
+          {rawQuery && (
+            <div className="gf-form" onClick={this.onRawQueryClicked}>
+              <label className="gf-form-label query-keyword">
+                Raw Query <i className={`fa fa-caret-${displaRawQuery ? 'down' : 'right'}`} ng-show="ctrl.showHelp" />
+              </label>
+            </div>
+          )}
+
+          <div className="gf-form gf-form--grow">
+            <div className="gf-form-label gf-form-label--grow" />
+          </div>
+        </div>
+        {rawQuery &&
+          displaRawQuery && (
+            <div className="gf-form">
+              <pre className="gf-form-pre">{rawQuery}</pre>
+            </div>
+          )}
+
+        {displayHelp && (
+          <div className="gf-form grafana-info-box" style={{ padding: 0 }}>
+            <pre className="gf-form-pre alert alert-info" style={{ marginRight: 0 }}>
+              <h5>Alias Patterns</h5>Format the legend keys any way you want by using alias patterns. Format the legend
+              keys any way you want by using alias patterns.<br /> <br />
+              Example:
+              <code>{`${'{{metricDescriptor.name}} - {{metricDescriptor.label.instance_name}}'}`}</code>
+              <br />
+              Result: &nbsp;&nbsp;<code>cpu/usage_time - server1-europe-west-1</code>
+              <br />
+              <br />
+              <strong>Patterns</strong>
+              <br />
+              <ul>
+                <li>
+                  <code>{`${'{{metricDescriptor.type}}'}`}</code> = metric type e.g.
+                  compute.googleapis.com/instance/cpu/usage_time
+                </li>
+                <li>
+                  <code>{`${'{{metricDescriptor.name}}'}`}</code> = name part of metric e.g. instance/cpu/usage_time
+                </li>
+                <li>
+                  <code>{`${'{{metricDescriptor.service}}'}`}</code> = service part of metric e.g. compute
+                </li>
+                <li>
+                  <code>{`${'{{metricDescriptor.label.label_name}}'}`}</code> = Metric label metadata e.g.
+                  metricDescriptor.label.instance_name
+                </li>
+                <li>
+                  <code>{`${'{{resource.label.label_name}}'}`}</code> = Resource label metadata e.g. resource.label.zone
+                </li>
+                <li>
+                  <code>{`${'{{bucket}}'}`}</code> = bucket boundary for distribution metrics when using a heatmap in
+                  Grafana
+                </li>
+              </ul>
+            </pre>
+          </div>
+        )}
+
+        {lastQueryError && (
+          <div className="gf-form">
+            <pre className="gf-form-pre alert alert-error">{lastQueryError}</pre>
+          </div>
+        )}
+      </>
+    );
+  }
+}

+ 195 - 0
public/app/plugins/datasource/stackdriver/components/Metrics.tsx

@@ -0,0 +1,195 @@
+import React from 'react';
+import _ from 'lodash';
+
+import StackdriverDatasource from '../datasource';
+import appEvents from 'app/core/app_events';
+import { MetricDescriptor } from '../types';
+import { MetricSelect } from 'app/core/components/Select/MetricSelect';
+import { TemplateSrv } from 'app/features/templating/template_srv';
+
+export interface Props {
+  onChange: (metricDescriptor: MetricDescriptor) => void;
+  templateSrv: TemplateSrv;
+  datasource: StackdriverDatasource;
+  defaultProject: string;
+  metricType: string;
+  children?: (renderProps: any) => JSX.Element;
+}
+
+interface State {
+  metricDescriptors: MetricDescriptor[];
+  metrics: any[];
+  services: any[];
+  service: string;
+  metric: string;
+  metricDescriptor: MetricDescriptor;
+  defaultProject: string;
+}
+
+export class Metrics extends React.Component<Props, State> {
+  state: State = {
+    metricDescriptors: [],
+    metrics: [],
+    services: [],
+    service: '',
+    metric: '',
+    metricDescriptor: null,
+    defaultProject: '',
+  };
+
+  constructor(props) {
+    super(props);
+  }
+
+  componentDidMount() {
+    this.setState({ defaultProject: this.props.defaultProject }, () => {
+      this.getCurrentProject()
+        .then(this.loadMetricDescriptors.bind(this))
+        .then(this.initializeServiceAndMetrics.bind(this));
+    });
+  }
+
+  async getCurrentProject() {
+    return new Promise(async (resolve, reject) => {
+      try {
+        if (!this.state.defaultProject || this.state.defaultProject === 'loading project...') {
+          const defaultProject = await this.props.datasource.getDefaultProject();
+          this.setState({ defaultProject });
+        }
+        resolve(this.state.defaultProject);
+      } catch (error) {
+        appEvents.emit('ds-request-error', error);
+        reject();
+      }
+    });
+  }
+
+  async loadMetricDescriptors() {
+    if (this.state.defaultProject !== 'loading project...') {
+      const metricDescriptors = await this.props.datasource.getMetricTypes(this.state.defaultProject);
+      this.setState({ metricDescriptors });
+      return metricDescriptors;
+    } else {
+      return [];
+    }
+  }
+
+  async initializeServiceAndMetrics() {
+    const { metricDescriptors } = this.state;
+    const services = this.getServicesList(metricDescriptors);
+    const metrics = this.getMetricsList(metricDescriptors);
+    const service = metrics.length > 0 ? metrics[0].service : '';
+    const metricDescriptor = this.getSelectedMetricDescriptor(this.props.metricType);
+    this.setState({ metricDescriptors, services, metrics, service: service, metricDescriptor });
+  }
+
+  getSelectedMetricDescriptor(metricType) {
+    return this.state.metricDescriptors.find(md => md.type === this.props.templateSrv.replace(metricType));
+  }
+
+  getMetricsList(metricDescriptors: MetricDescriptor[]) {
+    const selectedMetricDescriptor = this.getSelectedMetricDescriptor(this.props.metricType);
+    if (!selectedMetricDescriptor) {
+      return [];
+    }
+    const metricsByService = metricDescriptors.filter(m => m.service === selectedMetricDescriptor.service).map(m => ({
+      service: m.service,
+      value: m.type,
+      label: m.displayName,
+      description: m.description,
+    }));
+    return metricsByService;
+  }
+
+  onServiceChange = service => {
+    const { metricDescriptors } = this.state;
+    const { templateSrv, metricType } = this.props;
+
+    const metrics = metricDescriptors.filter(m => m.service === templateSrv.replace(service)).map(m => ({
+      service: m.service,
+      value: m.type,
+      label: m.displayName,
+      description: m.description,
+    }));
+
+    this.setState({ service, metrics });
+
+    if (metrics.length > 0 && !metrics.some(m => m.value === templateSrv.replace(metricType))) {
+      this.onMetricTypeChange(metrics[0].value);
+    }
+  };
+
+  onMetricTypeChange = value => {
+    const metricDescriptor = this.getSelectedMetricDescriptor(value);
+    this.setState({ metricDescriptor });
+    this.props.onChange({ ...metricDescriptor, type: value });
+  };
+
+  getServicesList(metricDescriptors: MetricDescriptor[]) {
+    const services = metricDescriptors.map(m => ({
+      value: m.service,
+      label: _.startCase(m.serviceShortName),
+    }));
+
+    return services.length > 0 ? _.uniqBy(services, s => s.value) : [];
+  }
+
+  getTemplateVariablesGroup() {
+    return {
+      label: 'Template Variables',
+      options: this.props.templateSrv.variables.map(v => ({
+        label: `$${v.name}`,
+        value: `$${v.name}`,
+      })),
+    };
+  }
+
+  render() {
+    const { services, service, metrics } = this.state;
+    const { metricType, templateSrv } = this.props;
+
+    return (
+      <>
+        <div className="gf-form-inline">
+          <div className="gf-form">
+            <span className="gf-form-label width-9 query-keyword">Service</span>
+            <MetricSelect
+              onChange={this.onServiceChange}
+              value={service}
+              options={services}
+              isSearchable={false}
+              placeholder="Select Services"
+              className="width-15"
+            />
+          </div>
+          <div className="gf-form gf-form--grow">
+            <div className="gf-form-label gf-form-label--grow" />
+          </div>
+        </div>
+        <div className="gf-form-inline">
+          <div className="gf-form">
+            <span className="gf-form-label width-9 query-keyword">Metric</span>
+            <MetricSelect
+              onChange={this.onMetricTypeChange}
+              value={metricType}
+              variables={templateSrv.variables}
+              options={[
+                {
+                  label: 'Metrics',
+                  expanded: true,
+                  options: metrics,
+                },
+              ]}
+              placeholder="Select Metric"
+              className="width-15"
+            />
+          </div>
+          <div className="gf-form gf-form--grow">
+            <div className="gf-form-label gf-form-label--grow" />
+          </div>
+        </div>
+        {this.props.children(this.state.metricDescriptor)}
+      </>
+    );
+  }
+}

+ 31 - 0
public/app/plugins/datasource/stackdriver/components/Project.tsx

@@ -0,0 +1,31 @@
+import React from 'react';
+import StackdriverDatasource from '../datasource';
+
+export interface Props {
+  datasource: StackdriverDatasource;
+}
+
+interface State {
+  projectName: string;
+}
+
+export class Project extends React.Component<Props, State> {
+  state: State = {
+    projectName: 'Loading project...',
+  };
+
+  async componentDidMount() {
+    const projectName = await this.props.datasource.getDefaultProject();
+    this.setState({ projectName });
+  }
+
+  render() {
+    const { projectName } = this.state;
+    return (
+      <div className="gf-form">
+        <span className="gf-form-label width-9 query-keyword">Project</span>
+        <input className="gf-form-input width-15" disabled type="text" value={projectName} />
+      </div>
+    );
+  }
+}

+ 23 - 0
public/app/plugins/datasource/stackdriver/components/QueryEditor.test.tsx

@@ -0,0 +1,23 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { QueryEditor, Props, DefaultTarget } from './QueryEditor';
+import { TemplateSrv } from 'app/features/templating/template_srv';
+
+const props: Props = {
+  onQueryChange: target => {},
+  onExecuteQuery: () => {},
+  target: DefaultTarget,
+  events: { on: () => {} },
+  datasource: {
+    getDefaultProject: () => Promise.resolve('project'),
+    getMetricTypes: () => Promise.resolve([]),
+  } as any,
+  templateSrv: new TemplateSrv(),
+};
+
+describe('QueryEditor', () => {
+  it('renders correctly', () => {
+    const tree = renderer.create(<QueryEditor {...props} />).toJSON();
+    expect(tree).toMatchSnapshot();
+  });
+});

+ 206 - 0
public/app/plugins/datasource/stackdriver/components/QueryEditor.tsx

@@ -0,0 +1,206 @@
+import React from 'react';
+import _ from 'lodash';
+
+import { TemplateSrv } from 'app/features/templating/template_srv';
+
+import { Metrics } from './Metrics';
+import { Filter } from './Filter';
+import { Aggregations } from './Aggregations';
+import { Alignments } from './Alignments';
+import { AlignmentPeriods } from './AlignmentPeriods';
+import { AliasBy } from './AliasBy';
+import { Help } from './Help';
+import { Target, MetricDescriptor } from '../types';
+import { getAlignmentPickerData } from '../functions';
+import StackdriverDatasource from '../datasource';
+import { SelectOptionItem } from '@grafana/ui';
+
+export interface Props {
+  onQueryChange: (target: Target) => void;
+  onExecuteQuery: () => void;
+  target: Target;
+  events: any;
+  datasource: StackdriverDatasource;
+  templateSrv: TemplateSrv;
+}
+
+interface State extends Target {
+  alignOptions: SelectOptionItem[];
+  lastQuery: string;
+  lastQueryError: string;
+  [key: string]: any;
+}
+
+export const DefaultTarget: State = {
+  defaultProject: 'loading project...',
+  metricType: '',
+  metricKind: '',
+  valueType: '',
+  refId: '',
+  service: '',
+  unit: '',
+  crossSeriesReducer: 'REDUCE_MEAN',
+  alignmentPeriod: 'stackdriver-auto',
+  perSeriesAligner: 'ALIGN_MEAN',
+  groupBys: [],
+  filters: [],
+  aliasBy: '',
+  alignOptions: [],
+  lastQuery: '',
+  lastQueryError: '',
+  usedAlignmentPeriod: '',
+};
+
+export class QueryEditor extends React.Component<Props, State> {
+  state: State = DefaultTarget;
+
+  componentDidMount() {
+    const { events, target, templateSrv } = this.props;
+    events.on('data-received', this.onDataReceived.bind(this));
+    events.on('data-error', this.onDataError.bind(this));
+    const { perSeriesAligner, alignOptions } = getAlignmentPickerData(target, templateSrv);
+    this.setState({
+      ...this.props.target,
+      alignOptions,
+      perSeriesAligner,
+    });
+  }
+
+  componentWillUnmount() {
+    this.props.events.off('data-received', this.onDataReceived);
+    this.props.events.off('data-error', this.onDataError);
+  }
+
+  onDataReceived(dataList) {
+    const series = dataList.find(item => item.refId === this.props.target.refId);
+    if (series) {
+      this.setState({
+        lastQuery: decodeURIComponent(series.meta.rawQuery),
+        lastQueryError: '',
+        usedAlignmentPeriod: series.meta.alignmentPeriod,
+      });
+    }
+  }
+
+  onDataError(err) {
+    let lastQuery;
+    let lastQueryError;
+    if (err.data && err.data.error) {
+      lastQueryError = this.props.datasource.formatStackdriverError(err);
+    } else if (err.data && err.data.results) {
+      const queryRes = err.data.results[this.props.target.refId];
+      lastQuery = decodeURIComponent(queryRes.meta.rawQuery);
+      if (queryRes && queryRes.error) {
+        try {
+          lastQueryError = JSON.parse(queryRes.error).error.message;
+        } catch {
+          lastQueryError = queryRes.error;
+        }
+      }
+    }
+    this.setState({ lastQuery, lastQueryError });
+  }
+
+  onMetricTypeChange = ({ valueType, metricKind, type, unit }: MetricDescriptor) => {
+    const { templateSrv, onQueryChange, onExecuteQuery } = this.props;
+    const { perSeriesAligner, alignOptions } = getAlignmentPickerData(
+      { valueType, metricKind, perSeriesAligner: this.state.perSeriesAligner },
+      templateSrv
+    );
+    this.setState(
+      {
+        alignOptions,
+        perSeriesAligner,
+        metricType: type,
+        unit,
+        valueType,
+        metricKind,
+      },
+      () => {
+        onQueryChange(this.state);
+        onExecuteQuery();
+      }
+    );
+  };
+
+  onPropertyChange(prop, value) {
+    this.setState({ [prop]: value }, () => {
+      this.props.onQueryChange(this.state);
+      this.props.onExecuteQuery();
+    });
+  }
+
+  render() {
+    const {
+      usedAlignmentPeriod,
+      defaultProject,
+      metricType,
+      crossSeriesReducer,
+      groupBys,
+      filters,
+      perSeriesAligner,
+      alignOptions,
+      alignmentPeriod,
+      aliasBy,
+      lastQuery,
+      lastQueryError,
+      refId,
+    } = this.state;
+    const { datasource, templateSrv } = this.props;
+
+    return (
+      <>
+        <Metrics
+          defaultProject={defaultProject}
+          metricType={metricType}
+          templateSrv={templateSrv}
+          datasource={datasource}
+          onChange={this.onMetricTypeChange}
+        >
+          {metric => (
+            <>
+              <Filter
+                filtersChanged={value => this.onPropertyChange('filters', value)}
+                groupBysChanged={value => this.onPropertyChange('groupBys', value)}
+                filters={filters}
+                groupBys={groupBys}
+                refId={refId}
+                hideGroupBys={false}
+                templateSrv={templateSrv}
+                datasource={datasource}
+                metricType={metric ? metric.type : ''}
+              />
+              <Aggregations
+                metricDescriptor={metric}
+                templateSrv={templateSrv}
+                crossSeriesReducer={crossSeriesReducer}
+                groupBys={groupBys}
+                onChange={value => this.onPropertyChange('crossSeriesReducer', value)}
+              >
+                {displayAdvancedOptions =>
+                  displayAdvancedOptions && (
+                    <Alignments
+                      alignOptions={alignOptions}
+                      templateSrv={templateSrv}
+                      perSeriesAligner={perSeriesAligner}
+                      onChange={value => this.onPropertyChange('perSeriesAligner', value)}
+                    />
+                  )
+                }
+              </Aggregations>
+              <AlignmentPeriods
+                templateSrv={templateSrv}
+                alignmentPeriod={alignmentPeriod}
+                perSeriesAligner={perSeriesAligner}
+                usedAlignmentPeriod={usedAlignmentPeriod}
+                onChange={value => this.onPropertyChange('alignmentPeriod', value)}
+              />
+              <AliasBy value={aliasBy} onChange={value => this.onPropertyChange('aliasBy', value)} />
+              <Help datasource={datasource} rawQuery={lastQuery} lastQueryError={lastQueryError} />
+            </>
+          )}
+        </Metrics>
+      </>
+    );
+  }
+}

+ 8 - 8
public/app/plugins/datasource/stackdriver/components/VariableQueryEditor.tsx

@@ -63,7 +63,7 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
     this.setState(state);
     this.setState(state);
   }
   }
 
 
-  async handleQueryTypeChange(event) {
+  async onQueryTypeChange(event) {
     const state: any = {
     const state: any = {
       selectedQueryType: event.target.value,
       selectedQueryType: event.target.value,
       ...await this.getLabels(this.state.selectedMetricType, event.target.value),
       ...await this.getLabels(this.state.selectedMetricType, event.target.value),
@@ -134,7 +134,7 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
       case MetricFindQueryTypes.LabelValues:
       case MetricFindQueryTypes.LabelValues:
       case MetricFindQueryTypes.ResourceTypes:
       case MetricFindQueryTypes.ResourceTypes:
         return (
         return (
-          <React.Fragment>
+          <>
             <SimpleSelect
             <SimpleSelect
               value={this.state.selectedService}
               value={this.state.selectedService}
               options={this.insertTemplateVariables(this.state.services)}
               options={this.insertTemplateVariables(this.state.services)}
@@ -155,12 +155,12 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
                 label="Label Key"
                 label="Label Key"
               />
               />
             )}
             )}
-          </React.Fragment>
+          </>
         );
         );
       case MetricFindQueryTypes.Aligners:
       case MetricFindQueryTypes.Aligners:
       case MetricFindQueryTypes.Aggregations:
       case MetricFindQueryTypes.Aggregations:
         return (
         return (
-          <React.Fragment>
+          <>
             <SimpleSelect
             <SimpleSelect
               value={this.state.selectedService}
               value={this.state.selectedService}
               options={this.insertTemplateVariables(this.state.services)}
               options={this.insertTemplateVariables(this.state.services)}
@@ -173,7 +173,7 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
               onValueChange={e => this.onMetricTypeChange(e)}
               onValueChange={e => this.onMetricTypeChange(e)}
               label="Metric Type"
               label="Metric Type"
             />
             />
-          </React.Fragment>
+          </>
         );
         );
       default:
       default:
         return '';
         return '';
@@ -182,15 +182,15 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
 
 
   render() {
   render() {
     return (
     return (
-      <React.Fragment>
+      <>
         <SimpleSelect
         <SimpleSelect
           value={this.state.selectedQueryType}
           value={this.state.selectedQueryType}
           options={this.queryTypes}
           options={this.queryTypes}
-          onValueChange={e => this.handleQueryTypeChange(e)}
+          onValueChange={e => this.onQueryTypeChange(e)}
           label="Query Type"
           label="Query Type"
         />
         />
         {this.renderQueryTypeSwitch(this.state.selectedQueryType)}
         {this.renderQueryTypeSwitch(this.state.selectedQueryType)}
-      </React.Fragment>
+      </>
     );
     );
   }
   }
 }
 }

+ 119 - 0
public/app/plugins/datasource/stackdriver/components/__snapshots__/Aggregations.test.tsx.snap

@@ -0,0 +1,119 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Aggregations renders correctly 1`] = `
+Array [
+  <div
+    className="gf-form-inline"
+  >
+    <div
+      className="gf-form"
+    >
+      <label
+        className="gf-form-label query-keyword width-9"
+      >
+        Aggregation
+      </label>
+      <div
+        className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
+        onKeyDown={[Function]}
+      >
+        <div
+          className="css-0 gf-form-select-box__control"
+          onMouseDown={[Function]}
+          onTouchEnd={[Function]}
+        >
+          <div
+            className="css-0 gf-form-select-box__value-container"
+          >
+            <div
+              className="css-0 gf-form-select-box__placeholder"
+            >
+              Select Aggregation
+            </div>
+            <div
+              className="css-0"
+            >
+              <div
+                className="gf-form-select-box__input"
+                style={
+                  Object {
+                    "display": "inline-block",
+                  }
+                }
+              >
+                <input
+                  aria-autocomplete="list"
+                  autoCapitalize="none"
+                  autoComplete="off"
+                  autoCorrect="off"
+                  disabled={false}
+                  id="react-select-2-input"
+                  onBlur={[Function]}
+                  onChange={[Function]}
+                  onFocus={[Function]}
+                  spellCheck="false"
+                  style={
+                    Object {
+                      "background": 0,
+                      "border": 0,
+                      "boxSizing": "content-box",
+                      "color": "inherit",
+                      "fontSize": "inherit",
+                      "opacity": 1,
+                      "outline": 0,
+                      "padding": 0,
+                      "width": "1px",
+                    }
+                  }
+                  tabIndex="0"
+                  type="text"
+                  value=""
+                />
+                <div
+                  style={
+                    Object {
+                      "height": 0,
+                      "left": 0,
+                      "overflow": "scroll",
+                      "position": "absolute",
+                      "top": 0,
+                      "visibility": "hidden",
+                      "whiteSpace": "pre",
+                    }
+                  }
+                >
+                  
+                </div>
+              </div>
+            </div>
+          </div>
+          <div
+            className="css-0 gf-form-select-box__indicators"
+          >
+            <span
+              className="gf-form-select-box__select-arrow "
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+    <div
+      className="gf-form gf-form--grow"
+    >
+      <label
+        className="gf-form-label gf-form-label--grow"
+      >
+        <a
+          onClick={[Function]}
+        >
+          <i
+            className="fa fa-caret-right"
+          />
+           Advanced Options
+        </a>
+      </label>
+    </div>
+  </div>,
+  <div />,
+]
+`;

+ 459 - 0
public/app/plugins/datasource/stackdriver/components/__snapshots__/QueryEditor.test.tsx.snap

@@ -0,0 +1,459 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`QueryEditor renders correctly 1`] = `
+Array [
+  <div
+    className="gf-form-inline"
+  >
+    <div
+      className="gf-form"
+    >
+      <span
+        className="gf-form-label width-9 query-keyword"
+      >
+        Service
+      </span>
+      <div
+        className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
+        onKeyDown={[Function]}
+      >
+        <div
+          className="css-0 gf-form-select-box__control"
+          onMouseDown={[Function]}
+          onTouchEnd={[Function]}
+        >
+          <div
+            className="css-0 gf-form-select-box__value-container"
+          >
+            <div
+              className="css-0 gf-form-select-box__placeholder"
+            >
+              Select Services
+            </div>
+            <input
+              className="css-14uuagi"
+              disabled={false}
+              id="react-select-2-input"
+              onBlur={[Function]}
+              onChange={[Function]}
+              onFocus={[Function]}
+              readOnly={true}
+              tabIndex="0"
+              value=""
+            />
+          </div>
+          <div
+            className="css-0 gf-form-select-box__indicators"
+          >
+            <span
+              className="gf-form-select-box__select-arrow "
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+    <div
+      className="gf-form gf-form--grow"
+    >
+      <div
+        className="gf-form-label gf-form-label--grow"
+      />
+    </div>
+  </div>,
+  <div
+    className="gf-form-inline"
+  >
+    <div
+      className="gf-form"
+    >
+      <span
+        className="gf-form-label width-9 query-keyword"
+      >
+        Metric
+      </span>
+      <div
+        className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
+        onKeyDown={[Function]}
+      >
+        <div
+          className="css-0 gf-form-select-box__control"
+          onMouseDown={[Function]}
+          onTouchEnd={[Function]}
+        >
+          <div
+            className="css-0 gf-form-select-box__value-container"
+          >
+            <div
+              className="css-0 gf-form-select-box__placeholder"
+            >
+              Select Metric
+            </div>
+            <div
+              className="css-0"
+            >
+              <div
+                className="gf-form-select-box__input"
+                style={
+                  Object {
+                    "display": "inline-block",
+                  }
+                }
+              >
+                <input
+                  aria-autocomplete="list"
+                  autoCapitalize="none"
+                  autoComplete="off"
+                  autoCorrect="off"
+                  disabled={false}
+                  id="react-select-3-input"
+                  onBlur={[Function]}
+                  onChange={[Function]}
+                  onFocus={[Function]}
+                  spellCheck="false"
+                  style={
+                    Object {
+                      "background": 0,
+                      "border": 0,
+                      "boxSizing": "content-box",
+                      "color": "inherit",
+                      "fontSize": "inherit",
+                      "opacity": 1,
+                      "outline": 0,
+                      "padding": 0,
+                      "width": "1px",
+                    }
+                  }
+                  tabIndex="0"
+                  type="text"
+                  value=""
+                />
+                <div
+                  style={
+                    Object {
+                      "height": 0,
+                      "left": 0,
+                      "overflow": "scroll",
+                      "position": "absolute",
+                      "top": 0,
+                      "visibility": "hidden",
+                      "whiteSpace": "pre",
+                    }
+                  }
+                >
+                  
+                </div>
+              </div>
+            </div>
+          </div>
+          <div
+            className="css-0 gf-form-select-box__indicators"
+          >
+            <span
+              className="gf-form-select-box__select-arrow "
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+    <div
+      className="gf-form gf-form--grow"
+    >
+      <div
+        className="gf-form-label gf-form-label--grow"
+      />
+    </div>
+  </div>,
+  <div
+    style={
+      Object {
+        "width": "100%",
+      }
+    }
+  />,
+  <div
+    className="gf-form-inline"
+  >
+    <div
+      className="gf-form"
+    >
+      <label
+        className="gf-form-label query-keyword width-9"
+      >
+        Aggregation
+      </label>
+      <div
+        className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
+        onKeyDown={[Function]}
+      >
+        <div
+          className="css-0 gf-form-select-box__control"
+          onMouseDown={[Function]}
+          onTouchEnd={[Function]}
+        >
+          <div
+            className="css-0 gf-form-select-box__value-container"
+          >
+            <div
+              className="css-0 gf-form-select-box__placeholder"
+            >
+              Select Aggregation
+            </div>
+            <div
+              className="css-0"
+            >
+              <div
+                className="gf-form-select-box__input"
+                style={
+                  Object {
+                    "display": "inline-block",
+                  }
+                }
+              >
+                <input
+                  aria-autocomplete="list"
+                  autoCapitalize="none"
+                  autoComplete="off"
+                  autoCorrect="off"
+                  disabled={false}
+                  id="react-select-4-input"
+                  onBlur={[Function]}
+                  onChange={[Function]}
+                  onFocus={[Function]}
+                  spellCheck="false"
+                  style={
+                    Object {
+                      "background": 0,
+                      "border": 0,
+                      "boxSizing": "content-box",
+                      "color": "inherit",
+                      "fontSize": "inherit",
+                      "opacity": 1,
+                      "outline": 0,
+                      "padding": 0,
+                      "width": "1px",
+                    }
+                  }
+                  tabIndex="0"
+                  type="text"
+                  value=""
+                />
+                <div
+                  style={
+                    Object {
+                      "height": 0,
+                      "left": 0,
+                      "overflow": "scroll",
+                      "position": "absolute",
+                      "top": 0,
+                      "visibility": "hidden",
+                      "whiteSpace": "pre",
+                    }
+                  }
+                >
+                  
+                </div>
+              </div>
+            </div>
+          </div>
+          <div
+            className="css-0 gf-form-select-box__indicators"
+          >
+            <span
+              className="gf-form-select-box__select-arrow "
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+    <div
+      className="gf-form gf-form--grow"
+    >
+      <label
+        className="gf-form-label gf-form-label--grow"
+      >
+        <a
+          onClick={[Function]}
+        >
+          <i
+            className="fa fa-caret-right"
+          />
+           Advanced Options
+        </a>
+      </label>
+    </div>
+  </div>,
+  <div
+    className="gf-form-inline"
+  >
+    <div
+      className="gf-form"
+    >
+      <label
+        className="gf-form-label query-keyword width-9"
+      >
+        Alignment Period
+      </label>
+      <div
+        className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
+        onKeyDown={[Function]}
+      >
+        <div
+          className="css-0 gf-form-select-box__control"
+          onMouseDown={[Function]}
+          onTouchEnd={[Function]}
+        >
+          <div
+            className="css-0 gf-form-select-box__value-container gf-form-select-box__value-container--has-value"
+          >
+            <div
+              className="css-0 gf-form-select-box__single-value"
+            >
+              <div
+                className="gf-form-select-box__img-value"
+              >
+                stackdriver auto
+              </div>
+            </div>
+            <div
+              className="css-0"
+            >
+              <div
+                className="gf-form-select-box__input"
+                style={
+                  Object {
+                    "display": "inline-block",
+                  }
+                }
+              >
+                <input
+                  aria-autocomplete="list"
+                  autoCapitalize="none"
+                  autoComplete="off"
+                  autoCorrect="off"
+                  disabled={false}
+                  id="react-select-5-input"
+                  onBlur={[Function]}
+                  onChange={[Function]}
+                  onFocus={[Function]}
+                  spellCheck="false"
+                  style={
+                    Object {
+                      "background": 0,
+                      "border": 0,
+                      "boxSizing": "content-box",
+                      "color": "inherit",
+                      "fontSize": "inherit",
+                      "opacity": 1,
+                      "outline": 0,
+                      "padding": 0,
+                      "width": "1px",
+                    }
+                  }
+                  tabIndex="0"
+                  type="text"
+                  value=""
+                />
+                <div
+                  style={
+                    Object {
+                      "height": 0,
+                      "left": 0,
+                      "overflow": "scroll",
+                      "position": "absolute",
+                      "top": 0,
+                      "visibility": "hidden",
+                      "whiteSpace": "pre",
+                    }
+                  }
+                >
+                  
+                </div>
+              </div>
+            </div>
+          </div>
+          <div
+            className="css-0 gf-form-select-box__indicators"
+          >
+            <span
+              className="gf-form-select-box__select-arrow "
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+    <div
+      className="gf-form gf-form--grow"
+    >
+      
+    </div>
+  </div>,
+  <div
+    className="gf-form-inline"
+  >
+    <div
+      className="gf-form"
+    >
+      <label
+        className="gf-form-label query-keyword width-9"
+      >
+        Alias By
+      </label>
+      <input
+        className="gf-form-input width-24"
+        onChange={[Function]}
+        type="text"
+        value=""
+      />
+    </div>
+    <div
+      className="gf-form gf-form--grow"
+    >
+      <div
+        className="gf-form-label gf-form-label--grow"
+      />
+    </div>
+  </div>,
+  <div
+    className="gf-form-inline"
+  >
+    <div
+      className="gf-form"
+    >
+      <span
+        className="gf-form-label width-9 query-keyword"
+      >
+        Project
+      </span>
+      <input
+        className="gf-form-input width-15"
+        disabled={true}
+        type="text"
+        value="Loading project..."
+      />
+    </div>
+    <div
+      className="gf-form"
+      onClick={[Function]}
+    >
+      <label
+        className="gf-form-label query-keyword pointer"
+      >
+        Show Help 
+        <i
+          className="fa fa-caret-right"
+        />
+      </label>
+    </div>
+    
+    <div
+      className="gf-form gf-form--grow"
+    >
+      <div
+        className="gf-form-label gf-form-label--grow"
+      />
+    </div>
+  </div>,
+  "",
+  "",
+]
+`;

+ 8 - 14
public/app/plugins/datasource/stackdriver/datasource.ts

@@ -2,6 +2,7 @@ import { stackdriverUnitMappings } from './constants';
 import appEvents from 'app/core/app_events';
 import appEvents from 'app/core/app_events';
 import _ from 'lodash';
 import _ from 'lodash';
 import StackdriverMetricFindQuery from './StackdriverMetricFindQuery';
 import StackdriverMetricFindQuery from './StackdriverMetricFindQuery';
+import { MetricDescriptor } from './types';
 
 
 export default class StackdriverDatasource {
 export default class StackdriverDatasource {
   id: number;
   id: number;
@@ -28,21 +29,15 @@ export default class StackdriverDatasource {
         return !target.hide && target.metricType;
         return !target.hide && target.metricType;
       })
       })
       .map(t => {
       .map(t => {
-        if (!t.hasOwnProperty('aggregation')) {
-          t.aggregation = {
-            crossSeriesReducer: 'REDUCE_MEAN',
-            groupBys: [],
-          };
-        }
         return {
         return {
           refId: t.refId,
           refId: t.refId,
           intervalMs: options.intervalMs,
           intervalMs: options.intervalMs,
           datasourceId: this.id,
           datasourceId: this.id,
           metricType: this.templateSrv.replace(t.metricType, options.scopedVars || {}),
           metricType: this.templateSrv.replace(t.metricType, options.scopedVars || {}),
-          primaryAggregation: this.templateSrv.replace(t.aggregation.crossSeriesReducer, options.scopedVars || {}),
-          perSeriesAligner: this.templateSrv.replace(t.aggregation.perSeriesAligner, options.scopedVars || {}),
-          alignmentPeriod: this.templateSrv.replace(t.aggregation.alignmentPeriod, options.scopedVars || {}),
-          groupBys: this.interpolateGroupBys(t.aggregation.groupBys, options.scopedVars),
+          primaryAggregation: this.templateSrv.replace(t.crossSeriesReducer || 'REDUCE_MEAN', options.scopedVars || {}),
+          perSeriesAligner: this.templateSrv.replace(t.perSeriesAligner, options.scopedVars || {}),
+          alignmentPeriod: this.templateSrv.replace(t.alignmentPeriod, options.scopedVars || {}),
+          groupBys: this.interpolateGroupBys(t.groupBys, options.scopedVars),
           view: t.view || 'FULL',
           view: t.view || 'FULL',
           filters: (t.filters || []).map(f => {
           filters: (t.filters || []).map(f => {
             return this.templateSrv.replace(f, options.scopedVars || {});
             return this.templateSrv.replace(f, options.scopedVars || {});
@@ -75,9 +70,7 @@ export default class StackdriverDatasource {
           refId: refId,
           refId: refId,
           datasourceId: this.id,
           datasourceId: this.id,
           metricType: this.templateSrv.replace(metricType),
           metricType: this.templateSrv.replace(metricType),
-          aggregation: {
-            crossSeriesReducer: 'REDUCE_NONE',
-          },
+          crossSeriesReducer: 'REDUCE_NONE',
           view: 'HEADERS',
           view: 'HEADERS',
         },
         },
       ],
       ],
@@ -261,7 +254,7 @@ export default class StackdriverDatasource {
     }
     }
   }
   }
 
 
-  async getMetricTypes(projectName: string) {
+  async getMetricTypes(projectName: string): Promise<MetricDescriptor[]> {
     try {
     try {
       if (this.metricTypes.length === 0) {
       if (this.metricTypes.length === 0) {
         const metricsApiPath = `v3/projects/${projectName}/metricDescriptors`;
         const metricsApiPath = `v3/projects/${projectName}/metricDescriptors`;
@@ -273,6 +266,7 @@ export default class StackdriverDatasource {
           m.service = service;
           m.service = service;
           m.serviceShortName = serviceShortName;
           m.serviceShortName = serviceShortName;
           m.displayName = m.displayName || m.type;
           m.displayName = m.displayName || m.type;
+
           return m;
           return m;
         });
         });
       }
       }

+ 2 - 2
public/app/plugins/datasource/stackdriver/filter_segments.ts

@@ -5,13 +5,13 @@ export class FilterSegments {
   filterSegments: any[];
   filterSegments: any[];
   removeSegment: any;
   removeSegment: any;
 
 
-  constructor(private uiSegmentSrv, private target, private getFilterKeysFunc, private getFilterValuesFunc) {}
+  constructor(private uiSegmentSrv, private filters, private getFilterKeysFunc, private getFilterValuesFunc) {}
 
 
   buildSegmentModel() {
   buildSegmentModel() {
     this.removeSegment = this.uiSegmentSrv.newSegment({ fake: true, value: DefaultRemoveFilterValue });
     this.removeSegment = this.uiSegmentSrv.newSegment({ fake: true, value: DefaultRemoveFilterValue });
 
 
     this.filterSegments = [];
     this.filterSegments = [];
-    this.target.filters.forEach((f, index) => {
+    this.filters.forEach((f, index) => {
       switch (index % 4) {
       switch (index % 4) {
         case 0:
         case 0:
           this.filterSegments.push(this.uiSegmentSrv.newKey(f));
           this.filterSegments.push(this.uiSegmentSrv.newKey(f));

+ 38 - 0
public/app/plugins/datasource/stackdriver/functions.test.ts

@@ -0,0 +1,38 @@
+import { getAlignmentOptionsByMetric } from './functions';
+import { ValueTypes, MetricKind } from './constants';
+
+describe('functions', () => {
+  let result;
+  describe('getAlignmentOptionsByMetric', () => {
+    describe('when double and gauge is passed', () => {
+      beforeEach(() => {
+        result = getAlignmentOptionsByMetric(ValueTypes.DOUBLE, MetricKind.GAUGE);
+      });
+
+      it('should return all alignment options except two', () => {
+        expect(result.length).toBe(9);
+        expect(result.map(o => o.value)).toEqual(
+          expect.not.arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE'])
+        );
+      });
+    });
+
+    describe('when double and delta is passed', () => {
+      beforeEach(() => {
+        result = getAlignmentOptionsByMetric(ValueTypes.DOUBLE, MetricKind.DELTA);
+      });
+
+      it('should return all alignment options except four', () => {
+        expect(result.length).toBe(9);
+        expect(result.map(o => o.value)).toEqual(
+          expect.not.arrayContaining([
+            'ALIGN_COUNT_TRUE',
+            'ALIGN_COUNT_FALSE',
+            'ALIGN_FRACTION_TRUE',
+            'ALIGN_INTERPOLATE',
+          ])
+        );
+      });
+    });
+  });
+});

+ 18 - 0
public/app/plugins/datasource/stackdriver/functions.ts

@@ -46,3 +46,21 @@ export const getLabelKeys = async (datasource, selectedMetricType) => {
     : [];
     : [];
   return labelKeys;
   return labelKeys;
 };
 };
+
+export const getAlignmentPickerData = ({ valueType, metricKind, perSeriesAligner }, templateSrv) => {
+  const options = getAlignmentOptionsByMetric(valueType, metricKind).map(option => ({
+    ...option,
+    label: option.text,
+  }));
+  const alignOptions = [
+    {
+      label: 'Alignment options',
+      expanded: true,
+      options,
+    },
+  ];
+  if (!options.some(o => o.value === templateSrv.replace(perSeriesAligner))) {
+    perSeriesAligner = options.length > 0 ? options[0].value : '';
+  }
+  return { alignOptions, perSeriesAligner };
+};

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików