Ver Fonte

Merge branch 'grafana-ui/move-spectrum' into tooling/storybook-poc

Dominik Prokop há 7 anos atrás
pai
commit
2991b64b60
100 ficheiros alterados com 8893 adições e 1161 exclusões
  1. 2 2
      .circleci/config.yml
  2. 18 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. 1 4
      packages/grafana-ui/src/components/Portal/Portal.tsx
  14. 1 0
      packages/grafana-ui/src/components/Select/SelectOption.test.tsx
  15. 9 3
      packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx
  16. 1 0
      packages/grafana-ui/src/components/Select/_Select.scss
  17. 2 0
      packages/grafana-ui/src/components/index.scss
  18. 2 0
      packages/grafana-ui/src/index.scss
  19. 0 0
      packages/grafana-ui/src/vendor/spectrum.css
  20. 0 0
      packages/grafana-ui/src/vendor/spectrum.js
  21. 2 1
      pkg/services/provisioning/dashboards/config_reader.go
  22. 2 1
      pkg/services/provisioning/dashboards/file_reader.go
  23. 45 4
      pkg/services/provisioning/dashboards/file_reader_test.go
  24. 1 1
      pkg/services/sqlstore/dashboard_provisioning.go
  25. 1 1
      pkg/tsdb/elasticsearch/client/models.go
  26. 2 2
      pkg/tsdb/elasticsearch/client/search_request.go
  27. 23 9
      pkg/tsdb/elasticsearch/models.go
  28. 31 8
      pkg/tsdb/elasticsearch/response_parser.go
  29. 78 0
      pkg/tsdb/elasticsearch/response_parser_test.go
  30. 53 15
      pkg/tsdb/elasticsearch/time_series_query.go
  31. 71 0
      pkg/tsdb/elasticsearch/time_series_query_test.go
  32. 27 0
      public/app/core/angular_wrappers.ts
  33. 90 0
      public/app/core/components/Select/MetricSelect.tsx
  34. 1 1
      public/app/features/alerting/AlertTab.tsx
  35. 1 0
      public/app/features/all.ts
  36. 1 1
      public/app/features/dashboard/dashgrid/DashboardPanel.tsx
  37. 0 71
      public/app/features/dashboard/dashgrid/KeyboardNavigation.tsx
  38. 4 2
      public/app/features/dashboard/dashgrid/PanelChrome.tsx
  39. 0 31
      public/app/features/dashboard/dashgrid/PanelLoader.ts
  40. 0 0
      public/app/features/dashboard/panel_editor/DataSourceOption.tsx
  41. 0 0
      public/app/features/dashboard/panel_editor/EditorTabBody.tsx
  42. 0 0
      public/app/features/dashboard/panel_editor/GeneralTab.tsx
  43. 0 0
      public/app/features/dashboard/panel_editor/PanelEditor.tsx
  44. 0 0
      public/app/features/dashboard/panel_editor/QueriesTab.tsx
  45. 0 0
      public/app/features/dashboard/panel_editor/QueryInspector.tsx
  46. 0 0
      public/app/features/dashboard/panel_editor/QueryOptions.tsx
  47. 0 0
      public/app/features/dashboard/panel_editor/VisualizationTab.tsx
  48. 0 0
      public/app/features/dashboard/panel_editor/VizTypePicker.tsx
  49. 0 0
      public/app/features/dashboard/panel_editor/VizTypePickerPlugin.tsx
  50. 0 0
      public/app/features/datasources/partials/http_settings.html
  51. 26 0
      public/app/features/datasources/settings/HttpSettingsCtrl.ts
  52. 1 2
      public/app/features/plugins/all.ts
  53. 0 42
      public/app/features/plugins/ds_dashboards_ctrl.ts
  54. 0 223
      public/app/features/plugins/ds_edit_ctrl.ts
  55. 0 0
      public/app/features/plugins/variableQueryEditorLoader.tsx
  56. 22 5
      public/app/plugins/datasource/elasticsearch/elastic_response.ts
  57. 16 2
      public/app/plugins/datasource/elasticsearch/metric_agg.ts
  58. 26 2
      public/app/plugins/datasource/elasticsearch/partials/metric_agg.html
  59. 20 0
      public/app/plugins/datasource/elasticsearch/partials/pipeline_variables.html
  60. 45 0
      public/app/plugins/datasource/elasticsearch/pipeline_variables.ts
  61. 34 9
      public/app/plugins/datasource/elasticsearch/query_builder.ts
  62. 4 0
      public/app/plugins/datasource/elasticsearch/query_ctrl.ts
  63. 17 0
      public/app/plugins/datasource/elasticsearch/query_def.ts
  64. 66 0
      public/app/plugins/datasource/elasticsearch/specs/elastic_response.test.ts
  65. 77 0
      public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts
  66. 20 2
      public/app/plugins/datasource/elasticsearch/specs/query_def.test.ts
  67. 9 22
      public/app/plugins/datasource/stackdriver/annotations_query_ctrl.ts
  68. 57 0
      public/app/plugins/datasource/stackdriver/components/Aggregations.test.tsx
  69. 94 0
      public/app/plugins/datasource/stackdriver/components/Aggregations.tsx
  70. 52 0
      public/app/plugins/datasource/stackdriver/components/AliasBy.tsx
  71. 56 0
      public/app/plugins/datasource/stackdriver/components/AlignmentPeriods.tsx
  72. 33 0
      public/app/plugins/datasource/stackdriver/components/Alignments.tsx
  73. 119 0
      public/app/plugins/datasource/stackdriver/components/AnnotationQueryEditor.tsx
  74. 44 0
      public/app/plugins/datasource/stackdriver/components/AnnotationsHelp.tsx
  75. 115 0
      public/app/plugins/datasource/stackdriver/components/Filter.tsx
  76. 115 0
      public/app/plugins/datasource/stackdriver/components/Help.tsx
  77. 195 0
      public/app/plugins/datasource/stackdriver/components/Metrics.tsx
  78. 31 0
      public/app/plugins/datasource/stackdriver/components/Project.tsx
  79. 23 0
      public/app/plugins/datasource/stackdriver/components/QueryEditor.test.tsx
  80. 206 0
      public/app/plugins/datasource/stackdriver/components/QueryEditor.tsx
  81. 8 8
      public/app/plugins/datasource/stackdriver/components/VariableQueryEditor.tsx
  82. 119 0
      public/app/plugins/datasource/stackdriver/components/__snapshots__/Aggregations.test.tsx.snap
  83. 459 0
      public/app/plugins/datasource/stackdriver/components/__snapshots__/QueryEditor.test.tsx.snap
  84. 8 14
      public/app/plugins/datasource/stackdriver/datasource.ts
  85. 2 2
      public/app/plugins/datasource/stackdriver/filter_segments.ts
  86. 38 0
      public/app/plugins/datasource/stackdriver/functions.test.ts
  87. 18 0
      public/app/plugins/datasource/stackdriver/functions.ts
  88. 6 37
      public/app/plugins/datasource/stackdriver/partials/annotations.editor.html
  89. 0 46
      public/app/plugins/datasource/stackdriver/partials/query.aggregation.html
  90. 9 72
      public/app/plugins/datasource/stackdriver/partials/query.editor.html
  91. 1 27
      public/app/plugins/datasource/stackdriver/partials/query.filter.html
  92. 0 80
      public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts
  93. 12 83
      public/app/plugins/datasource/stackdriver/query_ctrl.ts
  94. 48 176
      public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts
  95. 0 1
      public/app/plugins/datasource/stackdriver/specs/datasource.test.ts
  96. 0 74
      public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts
  97. 31 59
      public/app/plugins/datasource/stackdriver/specs/query_filter_ctrl.test.ts
  98. 47 0
      public/app/plugins/datasource/stackdriver/types.ts
  99. 8 1
      public/app/plugins/panel/gauge/GaugePanel.tsx
  100. 5 0
      public/app/types/templates.ts

+ 2 - 2
.circleci/config.yml

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

+ 18 - 2
CHANGELOG.md

@@ -2,6 +2,7 @@
 
 ### New Features
 * **Alerting**: Adds support for Google Hangouts Chat notifications [#11221](https://github.com/grafana/grafana/issues/11221), thx [@PatrickSchuster](https://github.com/PatrickSchuster)
+* **Elasticsearch**: Support bucket script pipeline aggregations [#5968](https://github.com/grafana/grafana/issues/5968)
 * **Snapshots**: Enable deletion of public snapshot [#14109](https://github.com/grafana/grafana/issues/14109)
 
 ### Minor
@@ -11,18 +12,33 @@
 * **Auth**: Prevent password reset when login form is disabled or either LDAP or Auth Proxy is enabled [#14246](https://github.com/grafana/grafana/issues/14246), thx [@SilverFire](https://github.com/SilverFire)
 * **Dataproxy**: Override incoming Authorization header [#13815](https://github.com/grafana/grafana/issues/13815), thx [@kornholi](https://github.com/kornholi)
 * **Admin**: Fix prevent removing last grafana admin permissions [#11067](https://github.com/grafana/grafana/issues/11067), thx [@danielbh](https://github.com/danielbh)
-* **Templating**: Escaping "Custom" template variables [#13754](https://github.com/grafana/grafana/issues/13754), thx [@IntegersOfK](https://github.com/IntegersOfK)
+* **Templating**: Escaping "Custom" template variables [#13754](https://github.com/grafana/grafana/issues/13754), thx [@IntegersOfK]req(https://github.com/IntegersOfK)
 * **Admin**: When multiple user invitations, all links are the same as the first user who was invited [#14483](https://github.com/grafana/grafana/issues/14483)
 * **LDAP**: Upgrade go-ldap to v3 [#14548](https://github.com/grafana/grafana/issues/14548)
 * **Proxy whitelist**: Add CIDR capability to auth_proxy whitelist [#14546](https://github.com/grafana/grafana/issues/14546), thx [@jacobrichard](https://github.com/jacobrichard)
 * **OAuth**: Support OAuth providers that are not RFC6749 compliant [#14562](https://github.com/grafana/grafana/issues/14562), thx [@tdabasinskas](https://github.com/tdabasinskas)
 * **Units**: Add blood glucose level units mg/dL and mmol/L [#14519](https://github.com/grafana/grafana/issues/14519), thx [@kjedamzik](https://github.com/kjedamzik)
 * **Stackdriver**: Aggregating series returns more than one series [#14581](https://github.com/grafana/grafana/issues/14581) and [#13914](https://github.com/grafana/grafana/issues/13914), thx [@kinok](https://github.com/kinok)
-* **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
 * **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)
 
+# 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)
 
 * **Datasource admin**: Fix for issue creating new data source when same name exists [#14467](https://github.com/grafana/grafana/issues/14467)

+ 1 - 1
devenv/dashboards.yaml

@@ -4,6 +4,6 @@ providers:
  - name: 'gdev dashboards'
    folder: 'gdev dashboards'
    type: file
+   updateIntervalSeconds: 15
    options:
      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,
   "gnetId": null,
   "graphTooltip": 0,
-  "iteration": 1542304484522,
+  "iteration": 1545263815779,
   "links": [
     {
       "icon": "external link",
@@ -66,6 +66,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -168,6 +169,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -270,6 +272,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -372,6 +375,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -474,6 +478,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -576,6 +581,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -2249,6 +2255,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -2366,6 +2373,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -2483,6 +2491,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -2600,6 +2609,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -2717,6 +2727,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -2834,6 +2845,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -2951,6 +2963,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -3068,6 +3081,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -3185,6 +3199,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -3302,6 +3317,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -3419,6 +3435,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -3536,6 +3553,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -3667,6 +3685,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -3780,6 +3799,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -3893,6 +3913,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -4006,6 +4027,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -4119,6 +4141,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -4232,6 +4255,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -4345,6 +4369,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -4458,6 +4483,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -4571,6 +4597,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -4684,6 +4711,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -4797,6 +4825,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -4910,6 +4939,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -5008,6 +5038,512 @@
         "x": 0,
         "y": 4
       },
+      "id": 60,
+      "panels": [
+        {
+          "aliasColors": {},
+          "bars": false,
+          "dashLength": 10,
+          "dashes": false,
+          "datasource": "$version_one",
+          "fill": 1,
+          "gridPos": {
+            "h": 8,
+            "w": 12,
+            "x": 0,
+            "y": 5
+          },
+          "id": 63,
+          "legend": {
+            "avg": false,
+            "current": false,
+            "max": false,
+            "min": false,
+            "show": true,
+            "total": false,
+            "values": false
+          },
+          "lines": true,
+          "linewidth": 1,
+          "links": [],
+          "nullPointMode": "null",
+          "paceLength": 10,
+          "percentage": false,
+          "pointradius": 2,
+          "points": false,
+          "renderer": "flot",
+          "seriesOverrides": [],
+          "stack": false,
+          "steppedLine": false,
+          "targets": [
+            {
+              "bucketAggs": [
+                {
+                  "field": "@timestamp",
+                  "id": "2",
+                  "settings": {
+                    "interval": "auto",
+                    "min_doc_count": 0,
+                    "trimEdges": 0
+                  },
+                  "type": "date_histogram"
+                }
+              ],
+              "metrics": [
+                {
+                  "field": "select field",
+                  "hide": true,
+                  "id": "1",
+                  "type": "count"
+                },
+                {
+                  "field": "select field",
+                  "id": "3",
+                  "meta": {},
+                  "pipelineVariables": [
+                    {
+                      "name": "var1",
+                      "pipelineAgg": "1"
+                    }
+                  ],
+                  "settings": {
+                    "script": "params.var1 * 1000"
+                  },
+                  "type": "bucket_script"
+                }
+              ],
+              "refId": "A",
+              "timeField": "@timestamp"
+            }
+          ],
+          "thresholds": [],
+          "timeFrom": null,
+          "timeRegions": [],
+          "timeShift": null,
+          "title": "count * 1000 (version one) - interval auto",
+          "tooltip": {
+            "shared": true,
+            "sort": 0,
+            "value_type": "individual"
+          },
+          "type": "graph",
+          "xaxis": {
+            "buckets": null,
+            "mode": "time",
+            "name": null,
+            "show": true,
+            "values": []
+          },
+          "yaxes": [
+            {
+              "format": "short",
+              "label": null,
+              "logBase": 1,
+              "max": null,
+              "min": null,
+              "show": true
+            },
+            {
+              "format": "short",
+              "label": null,
+              "logBase": 1,
+              "max": null,
+              "min": null,
+              "show": true
+            }
+          ],
+          "yaxis": {
+            "align": false,
+            "alignLevel": null
+          }
+        },
+        {
+          "aliasColors": {},
+          "bars": false,
+          "dashLength": 10,
+          "dashes": false,
+          "datasource": "$version_two",
+          "fill": 1,
+          "gridPos": {
+            "h": 8,
+            "w": 12,
+            "x": 12,
+            "y": 5
+          },
+          "id": 64,
+          "legend": {
+            "avg": false,
+            "current": false,
+            "max": false,
+            "min": false,
+            "show": true,
+            "total": false,
+            "values": false
+          },
+          "lines": true,
+          "linewidth": 1,
+          "links": [],
+          "nullPointMode": "null",
+          "paceLength": 10,
+          "percentage": false,
+          "pointradius": 2,
+          "points": false,
+          "renderer": "flot",
+          "seriesOverrides": [],
+          "stack": false,
+          "steppedLine": false,
+          "targets": [
+            {
+              "bucketAggs": [
+                {
+                  "field": "@timestamp",
+                  "id": "2",
+                  "settings": {
+                    "interval": "auto",
+                    "min_doc_count": 0,
+                    "trimEdges": 0
+                  },
+                  "type": "date_histogram"
+                }
+              ],
+              "metrics": [
+                {
+                  "field": "select field",
+                  "hide": true,
+                  "id": "1",
+                  "type": "count"
+                },
+                {
+                  "field": "select field",
+                  "id": "3",
+                  "meta": {},
+                  "pipelineVariables": [
+                    {
+                      "name": "var1",
+                      "pipelineAgg": "1"
+                    }
+                  ],
+                  "settings": {
+                    "script": "params.var1 * 1000"
+                  },
+                  "type": "bucket_script"
+                }
+              ],
+              "refId": "A",
+              "timeField": "@timestamp"
+            }
+          ],
+          "thresholds": [],
+          "timeFrom": null,
+          "timeRegions": [],
+          "timeShift": null,
+          "title": "count * 1000 (version two) - interval auto",
+          "tooltip": {
+            "shared": true,
+            "sort": 0,
+            "value_type": "individual"
+          },
+          "type": "graph",
+          "xaxis": {
+            "buckets": null,
+            "mode": "time",
+            "name": null,
+            "show": true,
+            "values": []
+          },
+          "yaxes": [
+            {
+              "format": "short",
+              "label": null,
+              "logBase": 1,
+              "max": null,
+              "min": null,
+              "show": true
+            },
+            {
+              "format": "short",
+              "label": null,
+              "logBase": 1,
+              "max": null,
+              "min": null,
+              "show": true
+            }
+          ],
+          "yaxis": {
+            "align": false,
+            "alignLevel": null
+          }
+        },
+        {
+          "aliasColors": {},
+          "bars": false,
+          "dashLength": 10,
+          "dashes": false,
+          "datasource": "$version_one",
+          "fill": 1,
+          "gridPos": {
+            "h": 8,
+            "w": 12,
+            "x": 0,
+            "y": 13
+          },
+          "id": 65,
+          "legend": {
+            "avg": false,
+            "current": false,
+            "max": false,
+            "min": false,
+            "show": true,
+            "total": false,
+            "values": false
+          },
+          "lines": true,
+          "linewidth": 1,
+          "links": [],
+          "nullPointMode": "null",
+          "paceLength": 10,
+          "percentage": false,
+          "pointradius": 2,
+          "points": false,
+          "renderer": "flot",
+          "seriesOverrides": [],
+          "stack": false,
+          "steppedLine": false,
+          "targets": [
+            {
+              "bucketAggs": [
+                {
+                  "field": "@timestamp",
+                  "id": "2",
+                  "settings": {
+                    "interval": "auto",
+                    "min_doc_count": 0,
+                    "trimEdges": 0
+                  },
+                  "type": "date_histogram"
+                }
+              ],
+              "metrics": [
+                {
+                  "field": "select field",
+                  "hide": true,
+                  "id": "1",
+                  "type": "count"
+                },
+                {
+                  "field": "@value",
+                  "hide": true,
+                  "id": "3",
+                  "meta": {},
+                  "settings": {},
+                  "type": "avg"
+                },
+                {
+                  "field": "select field",
+                  "id": "4",
+                  "meta": {},
+                  "pipelineVariables": [
+                    {
+                      "name": "var1",
+                      "pipelineAgg": "1"
+                    },
+                    {
+                      "name": "var2",
+                      "pipelineAgg": "3"
+                    }
+                  ],
+                  "settings": {
+                    "script": "params.var1 * params.var2"
+                  },
+                  "type": "bucket_script"
+                }
+              ],
+              "refId": "A",
+              "timeField": "@timestamp"
+            }
+          ],
+          "thresholds": [],
+          "timeFrom": null,
+          "timeRegions": [],
+          "timeShift": null,
+          "title": "count * avg (version one) - interval auto",
+          "tooltip": {
+            "shared": true,
+            "sort": 0,
+            "value_type": "individual"
+          },
+          "type": "graph",
+          "xaxis": {
+            "buckets": null,
+            "mode": "time",
+            "name": null,
+            "show": true,
+            "values": []
+          },
+          "yaxes": [
+            {
+              "format": "short",
+              "label": null,
+              "logBase": 1,
+              "max": null,
+              "min": null,
+              "show": true
+            },
+            {
+              "format": "short",
+              "label": null,
+              "logBase": 1,
+              "max": null,
+              "min": null,
+              "show": true
+            }
+          ],
+          "yaxis": {
+            "align": false,
+            "alignLevel": null
+          }
+        },
+        {
+          "aliasColors": {},
+          "bars": false,
+          "dashLength": 10,
+          "dashes": false,
+          "datasource": "$version_two",
+          "fill": 1,
+          "gridPos": {
+            "h": 8,
+            "w": 12,
+            "x": 12,
+            "y": 13
+          },
+          "id": 66,
+          "legend": {
+            "avg": false,
+            "current": false,
+            "max": false,
+            "min": false,
+            "show": true,
+            "total": false,
+            "values": false
+          },
+          "lines": true,
+          "linewidth": 1,
+          "links": [],
+          "nullPointMode": "null",
+          "paceLength": 10,
+          "percentage": false,
+          "pointradius": 2,
+          "points": false,
+          "renderer": "flot",
+          "seriesOverrides": [],
+          "stack": false,
+          "steppedLine": false,
+          "targets": [
+            {
+              "bucketAggs": [
+                {
+                  "field": "@timestamp",
+                  "id": "2",
+                  "settings": {
+                    "interval": "auto",
+                    "min_doc_count": 0,
+                    "trimEdges": 0
+                  },
+                  "type": "date_histogram"
+                }
+              ],
+              "metrics": [
+                {
+                  "field": "select field",
+                  "hide": true,
+                  "id": "1",
+                  "type": "count"
+                },
+                {
+                  "field": "@value",
+                  "hide": true,
+                  "id": "3",
+                  "meta": {},
+                  "settings": {},
+                  "type": "avg"
+                },
+                {
+                  "field": "select field",
+                  "id": "4",
+                  "meta": {},
+                  "pipelineVariables": [
+                    {
+                      "name": "var1",
+                      "pipelineAgg": "1"
+                    },
+                    {
+                      "name": "var2",
+                      "pipelineAgg": "3"
+                    }
+                  ],
+                  "settings": {
+                    "script": "params.var1 * params.var2"
+                  },
+                  "type": "bucket_script"
+                }
+              ],
+              "refId": "A",
+              "timeField": "@timestamp"
+            }
+          ],
+          "thresholds": [],
+          "timeFrom": null,
+          "timeRegions": [],
+          "timeShift": null,
+          "title": "count * avg (version two) - interval auto",
+          "tooltip": {
+            "shared": true,
+            "sort": 0,
+            "value_type": "individual"
+          },
+          "type": "graph",
+          "xaxis": {
+            "buckets": null,
+            "mode": "time",
+            "name": null,
+            "show": true,
+            "values": []
+          },
+          "yaxes": [
+            {
+              "format": "short",
+              "label": null,
+              "logBase": 1,
+              "max": null,
+              "min": null,
+              "show": true
+            },
+            {
+              "format": "short",
+              "label": null,
+              "logBase": 1,
+              "max": null,
+              "min": null,
+              "show": true
+            }
+          ],
+          "yaxis": {
+            "align": false,
+            "alignLevel": null
+          }
+        }
+      ],
+      "title": "Basic date histogram with bucket script aggregation",
+      "type": "row"
+    },
+    {
+      "collapsed": true,
+      "gridPos": {
+        "h": 1,
+        "w": 24,
+        "x": 0,
+        "y": 5
+      },
       "id": 54,
       "panels": [
         {
@@ -5042,6 +5578,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -5193,6 +5730,7 @@
           "linewidth": 1,
           "links": [],
           "nullPointMode": "null",
+          "paceLength": 10,
           "percentage": false,
           "pointradius": 5,
           "points": false,
@@ -5328,8 +5866,8 @@
     "list": [
       {
         "current": {
-          "text": "gdev-elasticsearch-v2-metrics",
-          "value": "gdev-elasticsearch-v2-metrics"
+          "text": "gdev-elasticsearch-v5-metrics",
+          "value": "gdev-elasticsearch-v5-metrics"
         },
         "hide": 0,
         "label": "Version One",
@@ -5343,8 +5881,8 @@
       },
       {
         "current": {
-          "text": "gdev-elasticsearch-v5-metrics",
-          "value": "gdev-elasticsearch-v5-metrics"
+          "text": "gdev-elasticsearch-v6-metrics",
+          "value": "gdev-elasticsearch-v6-metrics"
         },
         "hide": 0,
         "label": "Version Two",
@@ -5359,7 +5897,7 @@
     ]
   },
   "time": {
-    "from": "now-3h",
+    "from": "now-1h",
     "to": "now"
   },
   "timepicker": {
@@ -5390,5 +5928,5 @@
   "timezone": "",
   "title": "Datasource tests - Elasticsearch comparison",
   "uid": "fuFWehBmk",
-  "version": 10
+  "version": 4
 }

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

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

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

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

+ 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 _ from 'lodash';
 import $ from 'jquery';
-import 'vendor/spectrum';
+import '../../vendor/spectrum';
 
 export interface Props {
   color: string;

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


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

@@ -12,10 +12,7 @@ export class Portal extends PureComponent<Props> {
 
   constructor(props: Props) {
     super(props);
-    const {
-      className,
-      root = document.body
-    } = this.props;
+    const { className, root = document.body } = this.props;
 
     if (className) {
       this.node.classList.add(className);

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

@@ -5,6 +5,7 @@ import { OptionProps } from 'react-select/lib/components/Option';
 
 // @ts-ignore
 const model: OptionProps<any> = {
+  data: jest.fn(),
   cx: jest.fn(),
   clearValue: 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';
 
 interface ExtendedGroupProps extends GroupProps<any> {
-  data: any;
+  data: {
+    label: string;
+    expanded: boolean;
+    options: any[];
+  };
 }
 
 interface State {
@@ -15,8 +19,10 @@ export default class SelectOptionGroup extends PureComponent<ExtendedGroupProps,
   };
 
   componentDidMount() {
-    if (this.props.selectProps) {
-      const value = this.props.selectProps.value[this.props.selectProps.value.length - 1];
+    if (this.props.data.expanded) {
+      this.setState({ expanded: true });
+    } else if (this.props.selectProps && this.props.selectProps.value) {
+      const { value } = this.props.selectProps.value;
 
       if (value && this.props.options.some(option => option.value === value)) {
         this.setState({ expanded: true });

+ 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 {
   overflow-y: auto;
   max-height: 300px;
+  max-width: 600px;
 }
 
 .tag-filter .gf-form-select-box__menu {

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

@@ -5,3 +5,5 @@
 @import 'Select/Select';
 @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';
+

+ 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


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

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

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

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

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

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

+ 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 {
 	result := &models.DashboardProvisioning{}
 
-	exist, err := sess.Where("dashboard_id=?", dashboard.Id).Get(result)
+	exist, err := sess.Where("dashboard_id=? AND name = ?", dashboard.Id, cmd.Name).Get(result)
 	if err != nil {
 		return err
 	}

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

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

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

+ 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
 type MetricAgg struct {
-	Field             string           `json:"field"`
-	Hide              bool             `json:"hide"`
-	ID                string           `json:"id"`
-	PipelineAggregate string           `json:"pipelineAgg"`
-	Settings          *simplejson.Json `json:"settings"`
-	Meta              *simplejson.Json `json:"meta"`
-	Type              string           `json:"type"`
+	Field             string            `json:"field"`
+	Hide              bool              `json:"hide"`
+	ID                string            `json:"id"`
+	PipelineAggregate string            `json:"pipelineAgg"`
+	PipelineVariables map[string]string `json:"pipelineVariables"`
+	Settings          *simplejson.Json  `json:"settings"`
+	Meta              *simplejson.Json  `json:"meta"`
+	Type              string            `json:"type"`
 }
 
 var metricAggType = map[string]string{
@@ -45,6 +46,7 @@ var metricAggType = map[string]string{
 	"cardinality":    "Unique Count",
 	"moving_avg":     "Moving Average",
 	"derivative":     "Derivative",
+	"bucket_script":  "Bucket Script",
 	"raw_document":   "Raw Document",
 }
 
@@ -60,8 +62,13 @@ var extendedStats = map[string]string{
 }
 
 var pipelineAggType = map[string]string{
-	"moving_avg": "moving_avg",
-	"derivative": "derivative",
+	"moving_avg":    "moving_avg",
+	"derivative":    "derivative",
+	"bucket_script": "bucket_script",
+}
+
+var pipelineAggWithMultipleBucketPathsType = map[string]string{
+	"bucket_script": "bucket_script",
 }
 
 func isPipelineAgg(metricType string) bool {
@@ -71,6 +78,13 @@ func isPipelineAgg(metricType string) bool {
 	return false
 }
 
+func isPipelineAggWithMultipleBucketPaths(metricType string) bool {
+	if _, ok := pipelineAggWithMultipleBucketPathsType[metricType]; ok {
+		return true
+	}
+	return false
+}
+
 func describeMetric(metricType, field string) string {
 	text := metricAggType[metricType]
 	if metricType == countType {

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

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

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

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

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

+ 1 - 1
public/app/features/alerting/AlertTab.tsx

@@ -6,7 +6,7 @@ import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoa
 import appEvents from 'app/core/app_events';
 
 // Components
-import { EditorTabBody, EditorToolbarView } from '../dashboard/dashgrid/EditorTabBody';
+import { EditorTabBody, EditorToolbarView } from '../dashboard/panel_editor/EditorTabBody';
 import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
 import StateHistory from './StateHistory';
 import 'app/features/alerting/AlertTabCtrl';

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

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

+ 1 - 1
public/app/features/dashboard/dashgrid/DashboardPanel.tsx

@@ -9,7 +9,7 @@ import { AddPanelPanel } from './AddPanelPanel';
 import { getPanelPluginNotFound } from './PanelPluginNotFound';
 import { DashboardRow } from './DashboardRow';
 import { PanelChrome } from './PanelChrome';
-import { PanelEditor } from './PanelEditor';
+import { PanelEditor } from '../panel_editor/PanelEditor';
 
 import { PanelModel } from '../panel_model';
 import { DashboardModel } from '../dashboard_model';

+ 0 - 71
public/app/features/dashboard/dashgrid/KeyboardNavigation.tsx

@@ -1,71 +0,0 @@
-import React, { KeyboardEvent, Component } from 'react';
-
-interface State {
-  selected: number;
-}
-
-export interface KeyboardNavigationProps {
-  onKeyDown: (evt: KeyboardEvent<EventTarget>, maxSelectedIndex: number, onEnterAction: () => void) => void;
-  onMouseEnter: (select: number) => void;
-  selected: number;
-}
-
-interface Props {
-  render: (injectProps: any) => void;
-}
-
-class KeyboardNavigation extends Component<Props, State> {
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      selected: 0,
-    };
-  }
-
-  goToNext = (maxSelectedIndex: number) => {
-    const nextIndex = this.state.selected >= maxSelectedIndex ? 0 : this.state.selected + 1;
-    this.setState({
-      selected: nextIndex,
-    });
-  };
-
-  goToPrev = (maxSelectedIndex: number) => {
-    const nextIndex = this.state.selected <= 0 ? maxSelectedIndex : this.state.selected - 1;
-    this.setState({
-      selected: nextIndex,
-    });
-  };
-
-  onKeyDown = (evt: KeyboardEvent, maxSelectedIndex: number, onEnterAction: any) => {
-    if (evt.key === 'ArrowDown') {
-      evt.preventDefault();
-      this.goToNext(maxSelectedIndex);
-    }
-    if (evt.key === 'ArrowUp') {
-      evt.preventDefault();
-      this.goToPrev(maxSelectedIndex);
-    }
-    if (evt.key === 'Enter' && onEnterAction) {
-      onEnterAction();
-    }
-  };
-
-  onMouseEnter = (mouseEnterIndex: number) => {
-    this.setState({
-      selected: mouseEnterIndex,
-    });
-  };
-
-  render() {
-    const injectProps = {
-      onKeyDown: this.onKeyDown,
-      onMouseEnter: this.onMouseEnter,
-      selected: this.state.selected,
-    };
-
-    return <>{this.props.render({ ...injectProps })}</>;
-  }
-}
-
-export default KeyboardNavigation;

+ 4 - 2
public/app/features/dashboard/dashgrid/PanelChrome.tsx

@@ -19,6 +19,8 @@ import { DashboardModel } from '../dashboard_model';
 import { PanelPlugin } from 'app/types';
 import { TimeRange } from '@grafana/ui';
 
+import variables from 'sass/_variables.scss';
+
 export interface Props {
   panel: PanelModel;
   dashboard: DashboardModel;
@@ -122,8 +124,8 @@ export class PanelChrome extends PureComponent<Props, State> {
                         timeSeries={timeSeries}
                         timeRange={timeRange}
                         options={panel.getOptions(plugin.exports.PanelDefaults)}
-                        width={width}
-                        height={height - PANEL_HEADER_HEIGHT}
+                        width={width - 2 * variables.panelHorizontalPadding }
+                        height={height - PANEL_HEADER_HEIGHT - variables.panelVerticalPadding}
                         renderCounter={renderCounter}
                       />
                     </div>

+ 0 - 31
public/app/features/dashboard/dashgrid/PanelLoader.ts

@@ -1,31 +0,0 @@
-import angular from 'angular';
-import coreModule from 'app/core/core_module';
-
-export interface AttachedPanel {
-  destroy();
-}
-
-export class PanelLoader {
-  /** @ngInject */
-  constructor(private $compile, private $rootScope) {}
-
-  load(elem, panel, dashboard): AttachedPanel {
-    const template = '<plugin-component type="panel" class="panel-height-helper"></plugin-component>';
-    const panelScope = this.$rootScope.$new();
-    panelScope.panel = panel;
-    panelScope.dashboard = dashboard;
-
-    const compiledElem = this.$compile(template)(panelScope);
-    const rootNode = angular.element(elem);
-    rootNode.append(compiledElem);
-
-    return {
-      destroy: () => {
-        panelScope.$destroy();
-        compiledElem.remove();
-      },
-    };
-  }
-}
-
-coreModule.service('panelLoader', PanelLoader);

+ 0 - 0
public/app/features/dashboard/dashgrid/DataSourceOption.tsx → public/app/features/dashboard/panel_editor/DataSourceOption.tsx


+ 0 - 0
public/app/features/dashboard/dashgrid/EditorTabBody.tsx → public/app/features/dashboard/panel_editor/EditorTabBody.tsx


+ 0 - 0
public/app/features/dashboard/dashgrid/GeneralTab.tsx → public/app/features/dashboard/panel_editor/GeneralTab.tsx


+ 0 - 0
public/app/features/dashboard/dashgrid/PanelEditor.tsx → public/app/features/dashboard/panel_editor/PanelEditor.tsx


+ 0 - 0
public/app/features/dashboard/dashgrid/QueriesTab.tsx → public/app/features/dashboard/panel_editor/QueriesTab.tsx


+ 0 - 0
public/app/features/dashboard/dashgrid/QueryInspector.tsx → public/app/features/dashboard/panel_editor/QueryInspector.tsx


+ 0 - 0
public/app/features/dashboard/dashgrid/QueryOptions.tsx → public/app/features/dashboard/panel_editor/QueryOptions.tsx


+ 0 - 0
public/app/features/dashboard/dashgrid/VisualizationTab.tsx → public/app/features/dashboard/panel_editor/VisualizationTab.tsx


+ 0 - 0
public/app/features/dashboard/dashgrid/VizTypePicker.tsx → public/app/features/dashboard/panel_editor/VizTypePicker.tsx


+ 0 - 0
public/app/features/dashboard/dashgrid/VizTypePickerPlugin.tsx → public/app/features/dashboard/panel_editor/VizTypePickerPlugin.tsx


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

+ 1 - 2
public/app/features/plugins/all.ts

@@ -1,7 +1,6 @@
 import './plugin_edit_ctrl';
 import './plugin_page_ctrl';
 import './import_list/import_list';
-import './ds_edit_ctrl';
 import './datasource_srv';
 import './plugin_component';
-import './VariableQueryComponentLoader';
+import './variableQueryEditorLoader';

+ 0 - 42
public/app/features/plugins/ds_dashboards_ctrl.ts

@@ -1,42 +0,0 @@
-import { coreModule } from 'app/core/core';
-import { store } from 'app/store/store';
-import { getNavModel } from 'app/core/selectors/navModel';
-import { buildNavModel } from './state/navModel';
-
-export class DataSourceDashboardsCtrl {
-  datasourceMeta: any;
-  navModel: any;
-  current: any;
-
-  /** @ngInject */
-  constructor(private backendSrv, private $routeParams) {
-    const state = store.getState();
-    this.navModel = getNavModel(state.navIndex, 'datasources');
-
-    if (this.$routeParams.id) {
-      this.getDatasourceById(this.$routeParams.id);
-    }
-  }
-
-  getDatasourceById(id) {
-    this.backendSrv
-      .get('/api/datasources/' + id)
-      .then(ds => {
-        this.current = ds;
-      })
-      .then(this.getPluginInfo.bind(this));
-  }
-
-  updateNav() {
-    this.navModel = buildNavModel(this.current, this.datasourceMeta, 'datasource-dashboards');
-  }
-
-  getPluginInfo() {
-    return this.backendSrv.get('/api/plugins/' + this.current.type + '/settings').then(pluginInfo => {
-      this.datasourceMeta = pluginInfo;
-      this.updateNav();
-    });
-  }
-}
-
-coreModule.controller('DataSourceDashboardsCtrl', DataSourceDashboardsCtrl);

+ 0 - 223
public/app/features/plugins/ds_edit_ctrl.ts

@@ -1,223 +0,0 @@
-import _ from 'lodash';
-import config from 'app/core/config';
-import { coreModule, appEvents } from 'app/core/core';
-import { store } from 'app/store/store';
-import { getNavModel } from 'app/core/selectors/navModel';
-import { buildNavModel } from './state/navModel';
-
-let datasourceTypes = [];
-
-const defaults = {
-  name: '',
-  type: 'graphite',
-  url: '',
-  access: 'proxy',
-  jsonData: {},
-  secureJsonFields: {},
-  secureJsonData: {},
-};
-
-let datasourceCreated = false;
-
-export class DataSourceEditCtrl {
-  isNew: boolean;
-  datasources: any[];
-  current: any;
-  types: any;
-  testing: any;
-  datasourceMeta: any;
-  editForm: any;
-  gettingStarted: boolean;
-  navModel: any;
-
-  /** @ngInject */
-  constructor(private $q, private backendSrv, private $routeParams, private $location, private datasourceSrv) {
-    const state = store.getState();
-    this.navModel = getNavModel(state.navIndex, 'datasources');
-    this.datasources = [];
-
-    this.loadDatasourceTypes().then(() => {
-      if (this.$routeParams.id) {
-        this.getDatasourceById(this.$routeParams.id);
-      } else {
-        this.initNewDatasourceModel();
-      }
-    });
-  }
-
-  initNewDatasourceModel() {
-    this.isNew = true;
-    this.current = _.cloneDeep(defaults);
-
-    // We are coming from getting started
-    if (this.$location.search().gettingstarted) {
-      this.gettingStarted = true;
-      this.current.isDefault = true;
-    }
-
-    this.typeChanged();
-  }
-
-  loadDatasourceTypes() {
-    if (datasourceTypes.length > 0) {
-      this.types = datasourceTypes;
-      return this.$q.when(null);
-    }
-
-    return this.backendSrv.get('/api/plugins', { enabled: 1, type: 'datasource' }).then(plugins => {
-      datasourceTypes = plugins;
-      this.types = plugins;
-    });
-  }
-
-  getDatasourceById(id) {
-    this.backendSrv.get('/api/datasources/' + id).then(ds => {
-      this.isNew = false;
-      this.current = ds;
-
-      if (datasourceCreated) {
-        datasourceCreated = false;
-        this.testDatasource();
-      }
-
-      return this.typeChanged();
-    });
-  }
-
-  userChangedType() {
-    // reset model but keep name & default flag
-    this.current = _.defaults(
-      {
-        id: this.current.id,
-        name: this.current.name,
-        isDefault: this.current.isDefault,
-        type: this.current.type,
-      },
-      _.cloneDeep(defaults)
-    );
-    this.typeChanged();
-  }
-
-  updateNav() {
-    this.navModel = buildNavModel(this.current, this.datasourceMeta, 'datasource-settings');
-  }
-
-  typeChanged() {
-    return this.backendSrv.get('/api/plugins/' + this.current.type + '/settings').then(pluginInfo => {
-      this.datasourceMeta = pluginInfo;
-      this.updateNav();
-    });
-  }
-
-  updateFrontendSettings() {
-    return this.backendSrv.get('/api/frontend/settings').then(settings => {
-      config.datasources = settings.datasources;
-      config.defaultDatasource = settings.defaultDatasource;
-      this.datasourceSrv.init();
-    });
-  }
-
-  testDatasource() {
-    return this.datasourceSrv.get(this.current.name).then(datasource => {
-      if (!datasource.testDatasource) {
-        return;
-      }
-
-      this.testing = { done: false, status: 'error' };
-
-      // make test call in no backend cache context
-      return this.backendSrv
-        .withNoBackendCache(() => {
-          return datasource
-            .testDatasource()
-            .then(result => {
-              this.testing.message = result.message;
-              this.testing.status = result.status;
-            })
-            .catch(err => {
-              if (err.statusText) {
-                this.testing.message = 'HTTP Error ' + err.statusText;
-              } else {
-                this.testing.message = err.message;
-              }
-            });
-        })
-        .finally(() => {
-          this.testing.done = true;
-        });
-    });
-  }
-
-  saveChanges() {
-    if (!this.editForm.$valid) {
-      return;
-    }
-
-    if (this.current.readOnly) {
-      return;
-    }
-
-    if (this.current.id) {
-      return this.backendSrv.put('/api/datasources/' + this.current.id, this.current).then(result => {
-        this.current = result.datasource;
-        this.updateNav();
-        return this.updateFrontendSettings().then(() => {
-          return this.testDatasource();
-        });
-      });
-    } else {
-      return this.backendSrv.post('/api/datasources', this.current).then(result => {
-        this.current = result.datasource;
-        this.updateFrontendSettings();
-
-        datasourceCreated = true;
-        this.$location.path('datasources/edit/' + result.id);
-      });
-    }
-  }
-
-  confirmDelete() {
-    this.backendSrv.delete('/api/datasources/' + this.current.id).then(() => {
-      this.$location.path('datasources');
-    });
-  }
-
-  delete(s) {
-    appEvents.emit('confirm-modal', {
-      title: 'Delete',
-      text: 'Are you sure you want to delete this datasource?',
-      yesText: 'Delete',
-      icon: 'fa-trash',
-      onConfirm: () => {
-        this.confirmDelete();
-      },
-    });
-  }
-}
-
-coreModule.controller('DataSourceEditCtrl', DataSourceEditCtrl);
-
-coreModule.directive('datasourceHttpSettings', () => {
-  return {
-    scope: {
-      current: '=',
-      suggestUrl: '@',
-      noDirectAccess: '@',
-    },
-    templateUrl: 'public/app/features/plugins/partials/ds_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];
-        };
-      },
-    },
-  };
-});

+ 0 - 0
public/app/features/plugins/VariableQueryComponentLoader.tsx → public/app/features/plugins/variableQueryEditorLoader.tsx


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

@@ -88,6 +88,7 @@ export class ElasticResponse {
             datapoints: [],
             metric: metric.type,
             field: metric.field,
+            metricId: metric.id,
             props: props,
           };
           for (i = 0; i < esAgg.buckets.length; i++) {
@@ -240,7 +241,7 @@ export class ElasticResponse {
           return metricName;
         }
         if (group === 'field') {
-          return series.field;
+          return series.field || '';
         }
 
         return match;
@@ -248,11 +249,27 @@ export class ElasticResponse {
     }
 
     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 {
-        metricName = 'Unset';
+        const appliedAgg = _.find(target.metrics, { id: series.field });
+        if (appliedAgg) {
+          metricName += ' ' + queryDef.describeMetric(appliedAgg);
+        } else {
+          metricName = 'Unset';
+        }
       }
     } else if (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.isSingle = metricAggs.length === 1;
       $scope.settingsLinkText = '';
+      $scope.variablesLinkText = '';
       $scope.aggDef = _.find($scope.metricAggTypes, { value: $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);
         if (pipelineOptions.length > 0) {
@@ -119,6 +128,10 @@ export class ElasticMetricAggCtrl {
       $scope.updatePipelineAggOptions();
     };
 
+    $scope.toggleVariables = () => {
+      $scope.showVariables = !$scope.showVariables;
+    };
+
     $scope.onChangeInternal = () => {
       $scope.onChange();
     };
@@ -152,6 +165,7 @@ export class ElasticMetricAggCtrl {
         $scope.target.bucketAggs = [queryDef.defaultBucketAgg()];
       }
 
+      $scope.showVariables = queryDef.isPipelineAggWithMultipleBucketPaths($scope.agg.type);
       $scope.updatePipelineAggOptions();
       $scope.onChange();
     };

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

@@ -13,7 +13,17 @@
 	<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 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 class="gf-form gf-form--grow">
@@ -36,6 +46,20 @@
 	</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 offset-width-7" ng-if="agg.type === 'derivative'">
 		<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>
 		</label>
 		<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>

+ 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.timeField = this.timeField;
 
-    let i, nestedAggs, metric;
+    let i, j, pv, nestedAggs, metric;
     const query = {
       size: 0,
       query: {
@@ -269,17 +269,42 @@ export class ElasticQueryBuilder {
       let metricAgg = null;
 
       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 {
-          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 {
         metricAgg = { field: metric.field };

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

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

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

@@ -64,6 +64,14 @@ export const metricAggTypes = [
     isPipelineAgg: true,
     minVersion: 2,
   },
+  {
+    text: 'Bucket Script',
+    value: 'bucket_script',
+    requiresField: false,
+    isPipelineAgg: true,
+    supportsMultipleBucketPaths: true,
+    minVersion: 2,
+  },
   { text: 'Raw Document', value: 'raw_document', requiresField: false },
 ];
 
@@ -128,6 +136,7 @@ export const pipelineOptions = {
     { text: 'minimize', default: false },
   ],
   derivative: [{ text: 'unit', default: undefined }],
+  bucket_script: [],
 };
 
 export const movingAvgModelSettings = {
@@ -171,6 +180,14 @@ export function isPipelineAgg(metricType) {
   return false;
 }
 
+export function isPipelineAggWithMultipleBucketPaths(metricType) {
+  if (metricType) {
+    return metricAggTypes.find(t => t.value === metricType && t.supportsMultipleBucketPaths) !== undefined;
+  }
+
+  return false;
+}
+
 export function getPipelineAggOptions(targets) {
   const result = [];
   _.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');
     });
   });
+
+  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');
   });
 
+  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', () => {
     const query = builder.build({
       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('using esversion undefined', () => {
       test('should not get pipeline aggs', () => {
@@ -80,13 +98,13 @@ describe('ElasticQueryDef', () => {
 
     describe('using esversion 2', () => {
       test('should get pipeline aggs', () => {
-        expect(queryDef.getMetricAggTypes(2).length).toBe(11);
+        expect(queryDef.getMetricAggTypes(2).length).toBe(12);
       });
     });
 
     describe('using esversion 5', () => {
       test('should get pipeline aggs', () => {
-        expect(queryDef.getMetricAggTypes(5).length).toBe(11);
+        expect(queryDef.getMetricAggTypes(5).length).toBe(12);
       });
     });
   });

+ 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 {
   static templateUrl = 'partials/annotations.editor.html';
   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 */
-  constructor() {
+  constructor(templateSrv) {
+    this.templateSrv = templateSrv;
     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);
   }
 
-  async handleQueryTypeChange(event) {
+  async onQueryTypeChange(event) {
     const state: any = {
       selectedQueryType: 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.ResourceTypes:
         return (
-          <React.Fragment>
+          <>
             <SimpleSelect
               value={this.state.selectedService}
               options={this.insertTemplateVariables(this.state.services)}
@@ -155,12 +155,12 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
                 label="Label Key"
               />
             )}
-          </React.Fragment>
+          </>
         );
       case MetricFindQueryTypes.Aligners:
       case MetricFindQueryTypes.Aggregations:
         return (
-          <React.Fragment>
+          <>
             <SimpleSelect
               value={this.state.selectedService}
               options={this.insertTemplateVariables(this.state.services)}
@@ -173,7 +173,7 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
               onValueChange={e => this.onMetricTypeChange(e)}
               label="Metric Type"
             />
-          </React.Fragment>
+          </>
         );
       default:
         return '';
@@ -182,15 +182,15 @@ export class StackdriverVariableQueryEditor extends PureComponent<VariableQueryP
 
   render() {
     return (
-      <React.Fragment>
+      <>
         <SimpleSelect
           value={this.state.selectedQueryType}
           options={this.queryTypes}
-          onValueChange={e => this.handleQueryTypeChange(e)}
+          onValueChange={e => this.onQueryTypeChange(e)}
           label="Query Type"
         />
         {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 _ from 'lodash';
 import StackdriverMetricFindQuery from './StackdriverMetricFindQuery';
+import { MetricDescriptor } from './types';
 
 export default class StackdriverDatasource {
   id: number;
@@ -28,21 +29,15 @@ export default class StackdriverDatasource {
         return !target.hide && target.metricType;
       })
       .map(t => {
-        if (!t.hasOwnProperty('aggregation')) {
-          t.aggregation = {
-            crossSeriesReducer: 'REDUCE_MEAN',
-            groupBys: [],
-          };
-        }
         return {
           refId: t.refId,
           intervalMs: options.intervalMs,
           datasourceId: this.id,
           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',
           filters: (t.filters || []).map(f => {
             return this.templateSrv.replace(f, options.scopedVars || {});
@@ -75,9 +70,7 @@ export default class StackdriverDatasource {
           refId: refId,
           datasourceId: this.id,
           metricType: this.templateSrv.replace(metricType),
-          aggregation: {
-            crossSeriesReducer: 'REDUCE_NONE',
-          },
+          crossSeriesReducer: 'REDUCE_NONE',
           view: 'HEADERS',
         },
       ],
@@ -261,7 +254,7 @@ export default class StackdriverDatasource {
     }
   }
 
-  async getMetricTypes(projectName: string) {
+  async getMetricTypes(projectName: string): Promise<MetricDescriptor[]> {
     try {
       if (this.metricTypes.length === 0) {
         const metricsApiPath = `v3/projects/${projectName}/metricDescriptors`;
@@ -273,6 +266,7 @@ export default class StackdriverDatasource {
           m.service = service;
           m.serviceShortName = serviceShortName;
           m.displayName = m.displayName || m.type;
+
           return m;
         });
       }

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

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

+ 6 - 37
public/app/plugins/datasource/stackdriver/partials/annotations.editor.html

@@ -1,37 +1,6 @@
-<stackdriver-filter target="ctrl.annotation.target" refresh="ctrl.refresh()" datasource="ctrl.datasource"
-  default-dropdown-value="ctrl.defaultDropdownValue" default-service-value="ctrl.defaultServiceValue" hide-group-bys="true"></stackdriver-filter>
-
-<div class="gf-form gf-form-inline">
-  <div class="gf-form">
-    <span class="gf-form-label query-keyword width-9">Title</span>
-    <input type="text" class="gf-form-input width-20" ng-model="ctrl.annotation.target.title" />
-  </div>
-  <div class="gf-form">
-    <span class="gf-form-label query-keyword width-9">Text</span>
-    <input type="text" class="gf-form-input width-20" ng-model="ctrl.annotation.target.text" />
-  </div>
-  <div class="gf-form gf-form--grow">
-    <div class="gf-form-label gf-form-label--grow"></div>
-  </div>
-</div>
-
-<div class="gf-form grafana-info-box" style="padding: 0">
-  <pre class="gf-form-pre alert alert-info" style="margin-right: 0"><h5>Annotation Query Format</h5>
-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.
-
-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:
-
-<code ng-non-bindable>{{metric.type}} has value: {{metric.value}}</code>
-
-Example Result: <code ng-non-bindable>monitoring.googleapis.com/uptime_check/http_status has this value: 502</code>
-
-<label>Patterns:</label>
-<code ng-non-bindable>{{metric.value}}</code> = value of the metric/point
-<code ng-non-bindable>{{metric.type}}</code> = metric type e.g. compute.googleapis.com/instance/cpu/usage_time
-<code ng-non-bindable>{{metric.name}}</code> = name part of metric e.g. instance/cpu/usage_time
-<code ng-non-bindable>{{metric.service}}</code> = service part of metric e.g. compute
-
-<code ng-non-bindable>{{metric.label.label_name}}</code> = Metric label metadata e.g. metric.label.instance_name
-<code ng-non-bindable>{{resource.label.label_name}}</code> = Resource label metadata e.g. resource.label.zone
-</pre>
-</div>
+<stackdriver-annotation-query-editor
+  target="ctrl.annotation.target"
+  on-query-change="(ctrl.onQueryChange)"
+  datasource="ctrl.datasource"
+  template-srv="ctrl.templateSrv"
+></stackdriver-annotation-query-editor>

+ 0 - 46
public/app/plugins/datasource/stackdriver/partials/query.aggregation.html

@@ -1,46 +0,0 @@
-<div class="gf-form-inline">
-  <div class="gf-form">
-    <label class="gf-form-label query-keyword width-9">Aggregation</label>
-    <div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
-      <gf-form-dropdown model="ctrl.target.aggregation.crossSeriesReducer" get-options="ctrl.aggOptions" class="gf-form width-12"
-        disabled type="text" allow-custom="true" lookup-text="true" css-class="min-width-12" on-change="refresh()"></gf-form-dropdown>
-    </div>
-  </div>
-  <div class="gf-form gf-form--grow">
-    <label class="gf-form-label gf-form-label--grow">
-      <a ng-click="ctrl.target.showAggregationOptions = !ctrl.target.showAggregationOptions">
-        <i class="fa fa-caret-down" ng-show="ctrl.target.showAggregationOptions"></i>
-        <i class="fa fa-caret-right" ng-hide="ctrl.target.showAggregationOptions"></i>
-        Advanced Options
-      </a>
-    </label>
-  </div>
-</div>
-<div class="gf-form-group" ng-if="ctrl.target.showAggregationOptions">
-  <div class="gf-form offset-width-9">
-    <label class="gf-form-label query-keyword width-12">Aligner</label>
-    <div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
-      <gf-form-dropdown model="ctrl.target.aggregation.perSeriesAligner" get-options="ctrl.alignOptions" class="gf-form width-12"
-        disabled type="text" allow-custom="true" lookup-text="true" css-class="min-width-12" on-change="refresh()"></gf-form-dropdown>
-    </div>
-
-    <div class="gf-form gf-form--grow">
-      <div class="gf-form-label gf-form-label--grow"></div>
-    </div>
-  </div>
-</div>
-<div class="gf-form-inline">
-  <div class="gf-form">
-    <label class="gf-form-label query-keyword width-9">Alignment Period</label>
-    <div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent">
-      <gf-form-dropdown model="ctrl.target.aggregation.alignmentPeriod" get-options="ctrl.alignmentPeriods" class="gf-form width-12"
-        disabled type="text" allow-custom="true" lookup-text="true" css-class="min-width-12" on-change="refresh()"></gf-form-dropdown>
-    </div>
-  </div>
-
-  <div class="gf-form gf-form--grow">
-    <label ng-if="alignmentPeriod" class="gf-form-label gf-form-label--grow">
-      {{ctrl.formatAlignmentText()}}
-    </label>
-  </div>
-</div>

+ 9 - 72
public/app/plugins/datasource/stackdriver/partials/query.editor.html

@@ -1,73 +1,10 @@
 <query-editor-row query-ctrl="ctrl" has-text-edit-mode="false">
-  <stackdriver-filter target="ctrl.target" refresh="ctrl.refresh()" datasource="ctrl.datasource" default-dropdown-value="ctrl.defaultDropdownValue"
-    default-service-value="ctrl.defaultServiceValue"></stackdriver-filter>
-  <stackdriver-aggregation target="ctrl.target" alignment-period="ctrl.lastQueryMeta.alignmentPeriod" refresh="ctrl.refresh()"></stackdriver-aggregation>
-  <div class="gf-form-inline">
-    <div class="gf-form">
-      <span class="gf-form-label query-keyword width-9">Alias By</span>
-      <input type="text" class="gf-form-input width-30" ng-model="ctrl.target.aliasBy" ng-change="ctrl.refresh()"
-        ng-model-options="{ debounce: 500 }" />
-    </div>
-    <div class="gf-form gf-form--grow">
-      <div class="gf-form-label gf-form-label--grow"></div>
-    </div>
-  </div>
-  <div class="gf-form-inline">
-    <div class="gf-form">
-      <span class="gf-form-label width-9 query-keyword">Project</span>
-      <input class="gf-form-input" disabled type="text" ng-model='ctrl.target.defaultProject' css-class="min-width-12" />
-    </div>
-    <div class="gf-form">
-      <label class="gf-form-label query-keyword" ng-click="ctrl.showHelp = !ctrl.showHelp">
-        Show Help
-        <i class="fa fa-caret-down" ng-show="ctrl.showHelp"></i>
-        <i class="fa fa-caret-right" ng-hide="ctrl.showHelp"></i>
-      </label>
-    </div>
-    <div class="gf-form" ng-show="ctrl.lastQueryMeta">
-      <label class="gf-form-label query-keyword" ng-click="ctrl.showLastQuery = !ctrl.showLastQuery">
-        Raw Query
-        <i class="fa fa-caret-down" ng-show="ctrl.showLastQuery"></i>
-        <i class="fa fa-caret-right" ng-hide="ctrl.showLastQuery"></i>
-      </label>
-    </div>
-    <div class="gf-form gf-form--grow">
-      <div class="gf-form-label gf-form-label--grow"></div>
-    </div>
-  </div>
-
-  <div class="gf-form" ng-show="ctrl.showLastQuery">
-    <pre class="gf-form-pre">{{ctrl.lastQueryMeta.rawQueryString}}</pre>
-  </div>
-  <div class="gf-form grafana-info-box" style="padding: 0" ng-show="ctrl.showHelp">
-    <pre class="gf-form-pre alert alert-info" style="margin-right: 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 ng-non-bindable>{{metric.name}} - {{metric.label.instance_name}}</code><br />
-    Result: &nbsp;&nbsp;<code ng-non-bindable>cpu/usage_time - server1-europe-west-1</code><br /><br />
-
-    <strong>Patterns</strong><br />
-    <ul>
-      <li>
-        <code ng-non-bindable>{{metric.type}}</code> = metric type e.g. compute.googleapis.com/instance/cpu/usage_time
-      </li>
-      <li>
-        <code ng-non-bindable>{{metric.name}}</code> = name part of metric e.g. instance/cpu/usage_time
-      </li>
-      <li>
-        <code ng-non-bindable>{{metric.service}}</code> = service part of metric e.g. compute
-      </li>
-      <li>
-        <code ng-non-bindable>{{metric.label.label_name}}</code> = Metric label metadata e.g.
-        metric.label.instance_name
-      </li>
-      <li>
-        <code ng-non-bindable>{{resource.label.label_name}}</code> = Resource label metadata e.g. resource.label.zone
-      </li>
-    </ul>
-  </div>
-  <div class="gf-form" ng-show="ctrl.lastQueryError">
-    <pre class="gf-form-pre alert alert-error">{{ctrl.lastQueryError}}</pre>
-  </div>
-</query-editor-row>
+  <stackdriver-query-editor
+    target="ctrl.target"
+    events="ctrl.panelCtrl.events"
+    datasource="ctrl.datasource"
+    template-srv="ctrl.templateSrv"
+    on-query-change="(ctrl.onQueryChange)"
+    on-execute-query="(ctrl.onExecuteQuery)"
+  ></stackdriver-query-editor>
+</query-editor-row>

+ 1 - 27
public/app/plugins/datasource/stackdriver/partials/query.filter.html

@@ -1,29 +1,3 @@
-<div class="gf-form-inline">
-  <div class="gf-form">
-    <span class="gf-form-label width-9 query-keyword">Service</span>
-    <select
-      class="gf-form-input width-12"
-      ng-model="ctrl.service"
-      ng-options="f.value as f.text for f in ctrl.services"
-      ng-change="ctrl.onServiceChange(ctrl.service)"
-    ></select>
-  </div>
-  <div class="gf-form">
-    <span class="gf-form-label width-9 query-keyword">Metric</span>
-    <gf-form-dropdown
-      model="ctrl.metricType"
-      get-options="ctrl.metrics"
-      class="min-width-20"
-      disabled
-      type="text"
-      allow-custom="true"
-      lookup-text="true"
-      css-class="min-width-12"
-      on-change="ctrl.onMetricTypeChange()"
-    ></gf-form-dropdown>
-  </div>
-  <div class="gf-form gf-form--grow"><div class="gf-form-label gf-form-label--grow"></div></div>
-</div>
 <div class="gf-form-inline">
   <div class="gf-form">
     <span class="gf-form-label query-keyword width-9">Filter</span>
@@ -37,7 +11,7 @@
   </div>
   <div class="gf-form gf-form--grow"><div class="gf-form-label gf-form-label--grow"></div></div>
 </div>
-<div class="gf-form-inline" ng-hide="ctrl.$scope.hideGroupBys">
+<div class="gf-form-inline" ng-hide="ctrl.hideGroupBys">
   <div class="gf-form">
     <span class="gf-form-label query-keyword width-9">Group By</span>
     <div class="gf-form" ng-repeat="segment in ctrl.groupBySegments">

+ 0 - 80
public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts

@@ -1,80 +0,0 @@
-import coreModule from 'app/core/core_module';
-import _ from 'lodash';
-import * as options from './constants';
-import { getAlignmentOptionsByMetric, getAggregationOptionsByMetric } from './functions';
-import kbn from 'app/core/utils/kbn';
-
-export class StackdriverAggregation {
-  constructor() {
-    return {
-      templateUrl: 'public/app/plugins/datasource/stackdriver/partials/query.aggregation.html',
-      controller: 'StackdriverAggregationCtrl',
-      restrict: 'E',
-      scope: {
-        target: '=',
-        alignmentPeriod: '<',
-        refresh: '&',
-      },
-    };
-  }
-}
-
-export class StackdriverAggregationCtrl {
-  alignmentPeriods: any[];
-  aggOptions: any[];
-  alignOptions: any[];
-  target: any;
-
-  /** @ngInject */
-  constructor(private $scope, private templateSrv) {
-    this.$scope.ctrl = this;
-    this.target = $scope.target;
-    this.alignmentPeriods = options.alignmentPeriods;
-    this.aggOptions = options.aggOptions;
-    this.alignOptions = options.alignOptions;
-    this.setAggOptions();
-    this.setAlignOptions();
-    const self = this;
-    $scope.$on('metricTypeChanged', () => {
-      self.setAggOptions();
-      self.setAlignOptions();
-    });
-  }
-
-  setAlignOptions() {
-    this.alignOptions = getAlignmentOptionsByMetric(this.target.valueType, this.target.metricKind);
-    if (!this.alignOptions.find(o => o.value === this.templateSrv.replace(this.target.aggregation.perSeriesAligner))) {
-      this.target.aggregation.perSeriesAligner = this.alignOptions.length > 0 ? this.alignOptions[0].value : '';
-    }
-  }
-
-  setAggOptions() {
-    this.aggOptions = getAggregationOptionsByMetric(this.target.valueType, this.target.metricKind);
-
-    if (!this.aggOptions.find(o => o.value === this.templateSrv.replace(this.target.aggregation.crossSeriesReducer))) {
-      this.deselectAggregationOption('REDUCE_NONE');
-    }
-
-    if (this.target.aggregation.groupBys.length > 0) {
-      this.aggOptions = this.aggOptions.filter(o => o.value !== 'REDUCE_NONE');
-      this.deselectAggregationOption('REDUCE_NONE');
-    }
-  }
-
-  formatAlignmentText() {
-    const selectedAlignment = this.alignOptions.find(
-      ap => ap.value === this.templateSrv.replace(this.target.aggregation.perSeriesAligner)
-    );
-    return `${kbn.secondsToHms(this.$scope.alignmentPeriod)} interval (${
-      selectedAlignment ? selectedAlignment.text : ''
-    })`;
-  }
-
-  deselectAggregationOption(notValidOptionValue: string) {
-    const newValue = this.aggOptions.find(o => o.value !== notValidOptionValue);
-    this.target.aggregation.crossSeriesReducer = newValue ? newValue.value : '';
-  }
-}
-
-coreModule.directive('stackdriverAggregation', StackdriverAggregation);
-coreModule.controller('StackdriverAggregationCtrl', StackdriverAggregationCtrl);

+ 12 - 83
public/app/plugins/datasource/stackdriver/query_ctrl.ts

@@ -1,97 +1,26 @@
 import _ from 'lodash';
-import { QueryCtrl } from 'app/plugins/sdk';
-import './query_aggregation_ctrl';
-import './query_filter_ctrl';
 
-export interface QueryMeta {
-  alignmentPeriod: string;
-  rawQuery: string;
-  rawQueryString: string;
-  metricLabels: { [key: string]: string[] };
-  resourceLabels: { [key: string]: string[] };
-}
+import { QueryCtrl } from 'app/plugins/sdk';
+import { Target } from './types';
+import { TemplateSrv } from 'app/features/templating/template_srv';
 
 export class StackdriverQueryCtrl extends QueryCtrl {
   static templateUrl = 'partials/query.editor.html';
-  target: {
-    defaultProject: string;
-    unit: string;
-    metricType: string;
-    service: string;
-    refId: string;
-    aggregation: {
-      crossSeriesReducer: string;
-      alignmentPeriod: string;
-      perSeriesAligner: string;
-      groupBys: string[];
-    };
-    filters: string[];
-    aliasBy: string;
-    metricKind: any;
-    valueType: any;
-  };
-
-  defaultDropdownValue = 'Select Metric';
-  defaultServiceValue = 'All Services';
-
-  defaults = {
-    defaultProject: 'loading project...',
-    metricType: this.defaultDropdownValue,
-    service: this.defaultServiceValue,
-    metric: '',
-    unit: '',
-    aggregation: {
-      crossSeriesReducer: 'REDUCE_MEAN',
-      alignmentPeriod: 'stackdriver-auto',
-      perSeriesAligner: 'ALIGN_MEAN',
-      groupBys: [],
-    },
-    filters: [],
-    showAggregationOptions: false,
-    aliasBy: '',
-    metricKind: '',
-    valueType: '',
-  };
-
-  showHelp: boolean;
-  showLastQuery: boolean;
-  lastQueryMeta: QueryMeta;
-  lastQueryError?: string;
+  templateSrv: TemplateSrv;
 
   /** @ngInject */
-  constructor($scope, $injector) {
+  constructor($scope, $injector, templateSrv) {
     super($scope, $injector);
-    _.defaultsDeep(this.target, this.defaults);
-    this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope);
-    this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope);
+    this.templateSrv = templateSrv;
+    this.onQueryChange = this.onQueryChange.bind(this);
+    this.onExecuteQuery = this.onExecuteQuery.bind(this);
   }
 
-  onDataReceived(dataList) {
-    this.lastQueryError = null;
-    this.lastQueryMeta = null;
-
-    const anySeriesFromQuery: any = _.find(dataList, { refId: this.target.refId });
-    if (anySeriesFromQuery) {
-      this.lastQueryMeta = anySeriesFromQuery.meta;
-      this.lastQueryMeta.rawQueryString = decodeURIComponent(this.lastQueryMeta.rawQuery);
-    }
+  onQueryChange(target: Target) {
+    Object.assign(this.target, target);
   }
 
-  onDataError(err) {
-    if (err.data && err.data.results) {
-      const queryRes = err.data.results[this.target.refId];
-      if (queryRes && queryRes.error) {
-        this.lastQueryMeta = queryRes.meta;
-        this.lastQueryMeta.rawQueryString = decodeURIComponent(this.lastQueryMeta.rawQuery);
-
-        let jsonBody;
-        try {
-          jsonBody = JSON.parse(queryRes.error);
-        } catch {
-        }
-
-        this.lastQueryError = jsonBody.error.message;
-      }
-    }
+  onExecuteQuery() {
+    this.$scope.ctrl.refresh();
   }
 }

+ 48 - 176
public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts

@@ -1,7 +1,6 @@
 import coreModule from 'app/core/core_module';
 import _ from 'lodash';
-import { FilterSegments } from './filter_segments';
-import appEvents from 'app/core/app_events';
+import { FilterSegments, DefaultFilterValue } from './filter_segments';
 
 export class StackdriverFilter {
   /** @ngInject */
@@ -10,13 +9,15 @@ export class StackdriverFilter {
       templateUrl: 'public/app/plugins/datasource/stackdriver/partials/query.filter.html',
       controller: 'StackdriverFilterCtrl',
       controllerAs: 'ctrl',
+      bindToController: true,
       restrict: 'E',
       scope: {
-        target: '=',
-        datasource: '=',
-        refresh: '&',
-        defaultDropdownValue: '<',
-        defaultServiceValue: '<',
+        labelData: '<',
+        loading: '<',
+        groupBys: '<',
+        filters: '<',
+        filtersChanged: '&',
+        groupBysChanged: '&',
         hideGroupBys: '<',
       },
     };
@@ -24,46 +25,28 @@ export class StackdriverFilter {
 }
 
 export class StackdriverFilterCtrl {
-  metricLabels: { [key: string]: string[] };
-  resourceLabels: { [key: string]: string[] };
-  resourceTypes: string[];
-
   defaultRemoveGroupByValue = '-- remove group by --';
   resourceTypeValue = 'resource.type';
-  loadLabelsPromise: Promise<any>;
-
-  service: string;
-  metricType: string;
-  metricDescriptors: any[];
-  metrics: any[];
-  services: any[];
   groupBySegments: any[];
   filterSegments: FilterSegments;
   removeSegment: any;
-  target: any;
-  datasource: any;
+  filters: string[];
+  groupBys: string[];
+  hideGroupBys: boolean;
+  labelData: any;
+  loading: Promise<any>;
+  filtersChanged: (filters) => void;
+  groupBysChanged: (groupBys) => void;
 
   /** @ngInject */
-  constructor(private $scope, private uiSegmentSrv, private templateSrv, private $rootScope) {
-    this.datasource = $scope.datasource;
-    this.target = $scope.target;
-    this.metricType = $scope.defaultDropdownValue;
-    this.service = $scope.defaultServiceValue;
-
-    this.metricDescriptors = [];
-    this.metrics = [];
-    this.services = [];
-
-    this.getCurrentProject()
-      .then(this.loadMetricDescriptors.bind(this))
-      .then(this.getLabels.bind(this));
-
-    this.initSegments($scope.hideGroupBys);
+  constructor(private $scope, private uiSegmentSrv, private templateSrv) {
+    this.$scope.ctrl = this;
+    this.initSegments(this.hideGroupBys);
   }
 
   initSegments(hideGroupBys: boolean) {
     if (!hideGroupBys) {
-      this.groupBySegments = this.target.aggregation.groupBys.map(groupBy => {
+      this.groupBySegments = this.groupBys.map(groupBy => {
         return this.uiSegmentSrv.getSegmentForValue(groupBy);
       });
       this.ensurePlusButton(this.groupBySegments);
@@ -73,133 +56,17 @@ export class StackdriverFilterCtrl {
 
     this.filterSegments = new FilterSegments(
       this.uiSegmentSrv,
-      this.target,
+      this.filters,
       this.getFilterKeys.bind(this),
       this.getFilterValues.bind(this)
     );
     this.filterSegments.buildSegmentModel();
   }
 
-  async getCurrentProject() {
-    return new Promise(async (resolve, reject) => {
-      try {
-        if (!this.target.defaultProject || this.target.defaultProject === 'loading project...') {
-          this.target.defaultProject = await this.datasource.getDefaultProject();
-        }
-        resolve(this.target.defaultProject);
-      } catch (error) {
-        appEvents.emit('ds-request-error', error);
-        reject();
-      }
-    });
-  }
-
-  async loadMetricDescriptors() {
-    if (this.target.defaultProject !== 'loading project...') {
-      this.metricDescriptors = await this.datasource.getMetricTypes(this.target.defaultProject);
-      this.services = this.getServicesList();
-      this.metrics = this.getMetricsList();
-      return this.metricDescriptors;
-    } else {
-      return [];
-    }
-  }
-
-  getServicesList() {
-    const defaultValue = { value: this.$scope.defaultServiceValue, text: this.$scope.defaultServiceValue };
-    const services = this.metricDescriptors.map(m => {
-      return {
-        value: m.service,
-        text: m.serviceShortName,
-      };
-    });
-
-    if (services.find(m => m.value === this.target.service)) {
-      this.service = this.target.service;
-    }
-
-    return services.length > 0 ? [defaultValue, ..._.uniqBy(services, 'value')] : [];
-  }
-
-  getMetricsList() {
-    const metrics = this.metricDescriptors.map(m => {
-      return {
-        service: m.service,
-        value: m.type,
-        serviceShortName: m.serviceShortName,
-        text: m.displayName,
-        title: m.description,
-      };
-    });
-
-    let result;
-    if (this.target.service === this.$scope.defaultServiceValue) {
-      result = metrics.map(m => ({ ...m, text: `${m.service} - ${m.text}` }));
-    } else {
-      result = metrics.filter(m => m.service === this.target.service);
-    }
-
-    if (result.find(m => m.value === this.templateSrv.replace(this.target.metricType))) {
-      this.metricType = this.target.metricType;
-    } else if (result.length > 0) {
-      this.metricType = this.target.metricType = result[0].value;
-    }
-    return result;
-  }
-
-  async getLabels() {
-    this.loadLabelsPromise = new Promise(async resolve => {
-      try {
-        const { meta } = await this.datasource.getLabels(this.target.metricType, this.target.refId);
-        this.metricLabels = meta.metricLabels;
-        this.resourceLabels = meta.resourceLabels;
-        this.resourceTypes = meta.resourceTypes;
-        resolve();
-      } catch (error) {
-        if (error.data && error.data.message) {
-          console.log(error.data.message);
-        } else {
-          console.log(error);
-        }
-        appEvents.emit('alert-error', ['Error', 'Error loading metric labels for ' + this.target.metricType]);
-        resolve();
-      }
-    });
-  }
-
-  onServiceChange() {
-    this.target.service = this.service;
-    this.metrics = this.getMetricsList();
-    this.setMetricType();
-    this.getLabels();
-    if (!this.metrics.find(m => m.value === this.target.metricType)) {
-      this.target.metricType = this.$scope.defaultDropdownValue;
-    } else {
-      this.$scope.refresh();
-    }
-  }
-
-  async onMetricTypeChange() {
-    this.setMetricType();
-    this.$scope.refresh();
-    this.getLabels();
-  }
-
-  setMetricType() {
-    this.target.metricType = this.metricType;
-    const { valueType, metricKind, unit } = this.metricDescriptors.find(
-      m => m.type === this.templateSrv.replace(this.metricType)
-    );
-    this.target.unit = unit;
-    this.target.valueType = valueType;
-    this.target.metricKind = metricKind;
-    this.$rootScope.$broadcast('metricTypeChanged');
-  }
-
   async createLabelKeyElements() {
-    await this.loadLabelsPromise;
+    await this.loading;
 
-    let elements = Object.keys(this.metricLabels || {}).map(l => {
+    let elements = Object.keys(this.labelData.metricLabels || {}).map(l => {
       return this.uiSegmentSrv.newSegment({
         value: `metric.label.${l}`,
         expandable: false,
@@ -208,7 +75,7 @@ export class StackdriverFilterCtrl {
 
     elements = [
       ...elements,
-      ...Object.keys(this.resourceLabels || {}).map(l => {
+      ...Object.keys(this.labelData.resourceLabels || {}).map(l => {
         return this.uiSegmentSrv.newSegment({
           value: `resource.label.${l}`,
           expandable: false,
@@ -216,7 +83,7 @@ export class StackdriverFilterCtrl {
       }),
     ];
 
-    if (this.resourceTypes && this.resourceTypes.length > 0) {
+    if (this.labelData.resourceTypes && this.labelData.resourceTypes.length > 0) {
       elements = [
         ...elements,
         this.uiSegmentSrv.newSegment({
@@ -229,10 +96,10 @@ export class StackdriverFilterCtrl {
     return elements;
   }
 
-  async getFilterKeys(segment, removeText?: string) {
+  async getFilterKeys(segment, removeText: string) {
     let elements = await this.createLabelKeyElements();
 
-    if (this.target.filters.indexOf(this.resourceTypeValue) !== -1) {
+    if (this.filters.indexOf(this.resourceTypeValue) !== -1) {
       elements = elements.filter(e => e.value !== this.resourceTypeValue);
     }
 
@@ -241,21 +108,24 @@ export class StackdriverFilterCtrl {
       return [];
     }
 
-    this.removeSegment.value = removeText;
-    return [...elements, this.removeSegment];
+    return segment.type === 'plus-button'
+      ? elements
+      : [
+          ...elements,
+          this.uiSegmentSrv.newSegment({ fake: true, value: removeText || this.defaultRemoveGroupByValue }),
+        ];
   }
 
   async getGroupBys(segment) {
     let elements = await this.createLabelKeyElements();
-
-    elements = elements.filter(e => this.target.aggregation.groupBys.indexOf(e.value) === -1);
+    elements = elements.filter(e => this.groupBys.indexOf(e.value) === -1);
     const noValueOrPlusButton = !segment || segment.type === 'plus-button';
     if (noValueOrPlusButton && elements.length === 0) {
       return [];
     }
 
     this.removeSegment.value = this.defaultRemoveGroupByValue;
-    return [...elements, this.removeSegment];
+    return segment.type === 'plus-button' ? elements : [...elements, this.removeSegment];
   }
 
   groupByChanged(segment, index) {
@@ -272,43 +142,45 @@ export class StackdriverFilterCtrl {
       return memo;
     };
 
-    this.target.aggregation.groupBys = this.groupBySegments.reduce(reducer, []);
+    const groupBys = this.groupBySegments.reduce(reducer, []);
+    this.groupBysChanged({ groupBys });
     this.ensurePlusButton(this.groupBySegments);
-    this.$rootScope.$broadcast('metricTypeChanged');
-    this.$scope.refresh();
   }
 
   async getFilters(segment, index) {
-    const hasNoFilterKeys = this.metricLabels && Object.keys(this.metricLabels).length === 0;
+    await this.loading;
+    const hasNoFilterKeys = this.labelData.metricLabels && Object.keys(this.labelData.metricLabels).length === 0;
     return this.filterSegments.getFilters(segment, index, hasNoFilterKeys);
   }
 
   getFilterValues(index) {
     const filterKey = this.templateSrv.replace(this.filterSegments.filterSegments[index - 2].value);
-    if (!filterKey || !this.metricLabels || Object.keys(this.metricLabels).length === 0) {
+    if (!filterKey || !this.labelData.metricLabels || Object.keys(this.labelData.metricLabels).length === 0) {
       return [];
     }
 
     const shortKey = filterKey.substring(filterKey.indexOf('.label.') + 7);
 
-    if (filterKey.startsWith('metric.label.') && this.metricLabels.hasOwnProperty(shortKey)) {
-      return this.metricLabels[shortKey];
+    if (filterKey.startsWith('metric.label.') && this.labelData.metricLabels.hasOwnProperty(shortKey)) {
+      return this.labelData.metricLabels[shortKey];
     }
 
-    if (filterKey.startsWith('resource.label.') && this.resourceLabels.hasOwnProperty(shortKey)) {
-      return this.resourceLabels[shortKey];
+    if (filterKey.startsWith('resource.label.') && this.labelData.resourceLabels.hasOwnProperty(shortKey)) {
+      return this.labelData.resourceLabels[shortKey];
     }
 
     if (filterKey === this.resourceTypeValue) {
-      return this.resourceTypes;
+      return this.labelData.resourceTypes;
     }
 
     return [];
   }
 
   filterSegmentUpdated(segment, index) {
-    this.target.filters = this.filterSegments.filterSegmentUpdated(segment, index);
-    this.$scope.refresh();
+    const filters = this.filterSegments.filterSegmentUpdated(segment, index);
+    if (!filters.some(f => f === DefaultFilterValue)) {
+      this.filtersChanged({ filters });
+    }
   }
 
   ensurePlusButton(segments) {

+ 0 - 1
public/app/plugins/datasource/stackdriver/specs/datasource.test.ts

@@ -82,7 +82,6 @@ describe('StackdriverDataSource', () => {
       targets: [
         {
           refId: 'A',
-          aggregation: {},
         },
       ],
     };

+ 0 - 74
public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts

@@ -1,74 +0,0 @@
-import { StackdriverAggregationCtrl } from '../query_aggregation_ctrl';
-
-describe('StackdriverAggregationCtrl', () => {
-  let ctrl;
-  describe('aggregation and alignment options', () => {
-    describe('when new query result is returned from the server', () => {
-      describe('and result is double and gauge and no group by is used', () => {
-        beforeEach(async () => {
-          ctrl = new StackdriverAggregationCtrl(
-            {
-              $on: () => {},
-              target: {
-                valueType: 'DOUBLE',
-                metricKind: 'GAUGE',
-                aggregation: { crossSeriesReducer: '', groupBys: [] },
-              },
-            },
-            {
-              replace: s => s,
-            }
-          );
-        });
-
-        it('should populate all aggregate options except two', () => {
-          ctrl.setAggOptions();
-          expect(ctrl.aggOptions.length).toBe(11);
-          expect(ctrl.aggOptions.map(o => o.value)).toEqual(
-            expect['not'].arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE'])
-          );
-        });
-
-        it('should populate all alignment options except two', () => {
-          ctrl.setAlignOptions();
-          expect(ctrl.alignOptions.length).toBe(9);
-          expect(ctrl.alignOptions.map(o => o.value)).toEqual(
-            expect['not'].arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE'])
-          );
-        });
-      });
-
-      describe('and result is double and gauge and a group by is used', () => {
-        beforeEach(async () => {
-          ctrl = new StackdriverAggregationCtrl(
-            {
-              $on: () => {},
-              target: {
-                valueType: 'DOUBLE',
-                metricKind: 'GAUGE',
-                aggregation: { crossSeriesReducer: 'REDUCE_NONE', groupBys: ['resource.label.projectid'] },
-              },
-            },
-            {
-              replace: s => s,
-            }
-          );
-        });
-
-        it('should populate all aggregate options except three', () => {
-          ctrl.setAggOptions();
-          expect(ctrl.aggOptions.length).toBe(10);
-          expect(ctrl.aggOptions.map(o => o.value)).toEqual(
-            expect['not'].arrayContaining(['REDUCE_COUNT_TRUE', 'REDUCE_COUNT_FALSE', 'REDUCE_NONE'])
-          );
-        });
-
-        it('should select some other reducer than REDUCE_NONE', () => {
-          ctrl.setAggOptions();
-          expect(ctrl.target.aggregation.crossSeriesReducer).not.toBe('');
-          expect(ctrl.target.aggregation.crossSeriesReducer).not.toBe('REDUCE_NONE');
-        });
-      });
-    });
-  });
-});

+ 31 - 59
public/app/plugins/datasource/stackdriver/specs/query_filter_ctrl.test.ts

@@ -5,6 +5,7 @@ import { DefaultRemoveFilterValue, DefaultFilterValue } from '../filter_segments
 describe('StackdriverQueryFilterCtrl', () => {
   let ctrl;
   let result;
+  let groupByChangedMock;
 
   describe('when initializing query editor', () => {
     beforeEach(() => {
@@ -32,10 +33,10 @@ describe('StackdriverQueryFilterCtrl', () => {
 
     describe('when labels are fetched', () => {
       beforeEach(async () => {
-        ctrl.metricLabels = { 'metric-key-1': ['metric-value-1'] };
-        ctrl.resourceLabels = { 'resource-key-1': ['resource-value-1'] };
+        ctrl.labelData.metricLabels = { 'metric-key-1': ['metric-value-1'] };
+        ctrl.labelData.resourceLabels = { 'resource-key-1': ['resource-value-1'] };
 
-        result = await ctrl.getGroupBys();
+        result = await ctrl.getGroupBys({ type: '' });
       });
 
       it('should populate group bys segments', () => {
@@ -48,17 +49,17 @@ describe('StackdriverQueryFilterCtrl', () => {
 
     describe('when a group by label is selected', () => {
       beforeEach(async () => {
-        ctrl.metricLabels = {
+        ctrl.labelData.metricLabels = {
           'metric-key-1': ['metric-value-1'],
           'metric-key-2': ['metric-value-2'],
         };
-        ctrl.resourceLabels = {
+        ctrl.labelData.resourceLabels = {
           'resource-key-1': ['resource-value-1'],
           'resource-key-2': ['resource-value-2'],
         };
-        ctrl.target.aggregation.groupBys = ['metric.label.metric-key-1', 'resource.label.resource-key-1'];
+        ctrl.groupBys = ['metric.label.metric-key-1', 'resource.label.resource-key-1'];
 
-        result = await ctrl.getGroupBys();
+        result = await ctrl.getGroupBys({ type: '' });
       });
 
       it('should not be used to populate group bys segments', () => {
@@ -71,6 +72,8 @@ describe('StackdriverQueryFilterCtrl', () => {
 
     describe('when a group by is selected', () => {
       beforeEach(() => {
+        groupByChangedMock = jest.fn();
+        ctrl.groupBysChanged = groupByChangedMock;
         const removeSegment = { fake: true, value: '-- remove group by --' };
         const segment = { value: 'groupby1' };
         ctrl.groupBySegments = [segment, removeSegment];
@@ -78,12 +81,14 @@ describe('StackdriverQueryFilterCtrl', () => {
       });
 
       it('should be added to group bys list', () => {
-        expect(ctrl.target.aggregation.groupBys.length).toBe(1);
+        expect(groupByChangedMock).toHaveBeenCalledWith({ groupBys: ['groupby1'] });
       });
     });
 
     describe('when a selected group by is removed', () => {
       beforeEach(() => {
+        groupByChangedMock = jest.fn();
+        ctrl.groupBysChanged = groupByChangedMock;
         const removeSegment = { fake: true, value: '-- remove group by --' };
         const segment = { value: 'groupby1' };
         ctrl.groupBySegments = [segment, removeSegment];
@@ -91,7 +96,7 @@ describe('StackdriverQueryFilterCtrl', () => {
       });
 
       it('should be added to group bys list', () => {
-        expect(ctrl.target.aggregation.groupBys.length).toBe(0);
+        expect(groupByChangedMock).toHaveBeenCalledWith({ groupBys: [] });
       });
     });
   });
@@ -130,11 +135,11 @@ describe('StackdriverQueryFilterCtrl', () => {
 
     describe('when values for a key filter part are fetched', () => {
       beforeEach(async () => {
-        ctrl.metricLabels = {
+        ctrl.labelData.metricLabels = {
           'metric-key-1': ['metric-value-1'],
           'metric-key-2': ['metric-value-2'],
         };
-        ctrl.resourceLabels = {
+        ctrl.labelData.resourceLabels = {
           'resource-key-1': ['resource-value-1'],
           'resource-key-2': ['resource-value-2'],
         };
@@ -155,11 +160,11 @@ describe('StackdriverQueryFilterCtrl', () => {
 
     describe('when values for a value filter part are fetched', () => {
       beforeEach(async () => {
-        ctrl.metricLabels = {
+        ctrl.labelData.metricLabels = {
           'metric-key-1': ['metric-value-1'],
           'metric-key-2': ['metric-value-2'],
         };
-        ctrl.resourceLabels = {
+        ctrl.labelData.resourceLabels = {
           'resource-key-1': ['resource-value-1'],
           'resource-key-2': ['resource-value-2'],
         };
@@ -198,6 +203,7 @@ describe('StackdriverQueryFilterCtrl', () => {
         });
       });
     });
+
     describe('when has one existing filter', () => {
       describe('and user clicks on key segment', () => {
         beforeEach(() => {
@@ -213,7 +219,6 @@ describe('StackdriverQueryFilterCtrl', () => {
           ];
           ctrl.filterSegmentUpdated(existingKeySegment, 0);
         });
-
         it('should not add any new segments', () => {
           expect(ctrl.filterSegments.filterSegments.length).toBe(4);
           expect(ctrl.filterSegments.filterSegments[0].type).toBe('key');
@@ -229,7 +234,6 @@ describe('StackdriverQueryFilterCtrl', () => {
           ctrl.filterSegments.filterSegments = [existingKeySegment, existingOperatorSegment, existingValueSegment];
           ctrl.filterSegmentUpdated(existingValueSegment, 2);
         });
-
         it('should ensure that plus segment exists', () => {
           expect(ctrl.filterSegments.filterSegments.length).toBe(4);
           expect(ctrl.filterSegments.filterSegments[0].type).toBe('key');
@@ -238,7 +242,6 @@ describe('StackdriverQueryFilterCtrl', () => {
           expect(ctrl.filterSegments.filterSegments[3].type).toBe('plus-button');
         });
       });
-
       describe('and user clicks on value segment and value is equal to fake value', () => {
         beforeEach(() => {
           const existingKeySegment = { value: 'filterkey1', type: 'key' };
@@ -247,7 +250,6 @@ describe('StackdriverQueryFilterCtrl', () => {
           ctrl.filterSegments.filterSegments = [existingKeySegment, existingOperatorSegment, existingValueSegment];
           ctrl.filterSegmentUpdated(existingValueSegment, 2);
         });
-
         it('should not add plus segment', () => {
           expect(ctrl.filterSegments.filterSegments.length).toBe(3);
           expect(ctrl.filterSegments.filterSegments[0].type).toBe('key');
@@ -269,13 +271,11 @@ describe('StackdriverQueryFilterCtrl', () => {
           ];
           ctrl.filterSegmentUpdated(existingKeySegment, 0);
         });
-
         it('should remove filter segments', () => {
           expect(ctrl.filterSegments.filterSegments.length).toBe(1);
           expect(ctrl.filterSegments.filterSegments[0].type).toBe('plus-button');
         });
       });
-
       describe('and user removes key segment and there is a previous filter', () => {
         beforeEach(() => {
           const existingKeySegment1 = { value: DefaultRemoveFilterValue, type: 'key' };
@@ -296,7 +296,6 @@ describe('StackdriverQueryFilterCtrl', () => {
           ];
           ctrl.filterSegmentUpdated(existingKeySegment2, 4);
         });
-
         it('should remove filter segments and the condition segment', () => {
           expect(ctrl.filterSegments.filterSegments.length).toBe(4);
           expect(ctrl.filterSegments.filterSegments[0].type).toBe('key');
@@ -305,7 +304,6 @@ describe('StackdriverQueryFilterCtrl', () => {
           expect(ctrl.filterSegments.filterSegments[3].type).toBe('plus-button');
         });
       });
-
       describe('and user removes key segment and there is a filter after it', () => {
         beforeEach(() => {
           const existingKeySegment1 = { value: DefaultRemoveFilterValue, type: 'key' };
@@ -326,7 +324,6 @@ describe('StackdriverQueryFilterCtrl', () => {
           ];
           ctrl.filterSegmentUpdated(existingKeySegment1, 0);
         });
-
         it('should remove filter segments and the condition segment', () => {
           expect(ctrl.filterSegments.filterSegments.length).toBe(4);
           expect(ctrl.filterSegments.filterSegments[0].type).toBe('key');
@@ -335,7 +332,6 @@ describe('StackdriverQueryFilterCtrl', () => {
           expect(ctrl.filterSegments.filterSegments[3].type).toBe('plus-button');
         });
       });
-
       describe('and user clicks on plus button', () => {
         beforeEach(() => {
           const existingKeySegment = { value: 'filterkey1', type: 'key' };
@@ -350,7 +346,6 @@ describe('StackdriverQueryFilterCtrl', () => {
           ];
           ctrl.filterSegmentUpdated(plusSegment, 3);
         });
-
         it('should condition segment and new filter segments', () => {
           expect(ctrl.filterSegments.filterSegments.length).toBe(7);
           expect(ctrl.filterSegments.filterSegments[0].type).toBe('key');
@@ -367,13 +362,6 @@ describe('StackdriverQueryFilterCtrl', () => {
 });
 
 function createCtrlWithFakes(existingFilters?: string[]) {
-  StackdriverFilterCtrl.prototype.loadMetricDescriptors = () => {
-    return Promise.resolve([]);
-  };
-  StackdriverFilterCtrl.prototype.getLabels = () => {
-    return Promise.resolve();
-  };
-
   const fakeSegmentServer = {
     newKey: val => {
       return { value: val, type: 'key' };
@@ -403,40 +391,24 @@ function createCtrlWithFakes(existingFilters?: string[]) {
     },
   };
   const scope = {
-    target: createTarget(existingFilters),
+    hideGroupBys: false,
+    groupBys: [],
+    filters: existingFilters || [],
+    labelData: {
+      metricLabels: {},
+      resourceLabels: {},
+      resourceTypes: [],
+    },
+    filtersChanged: () => {},
+    groupBysChanged: () => {},
     datasource: {
       getDefaultProject: () => {
         return 'project';
       },
     },
-    defaultDropdownValue: 'Select Metric',
-    defaultServiceValue: 'All Services',
     refresh: () => {},
   };
 
-  return new StackdriverFilterCtrl(scope, fakeSegmentServer, new TemplateSrvStub(), { $broadcast: param => {} });
-}
-
-function createTarget(existingFilters?: string[]) {
-  return {
-    project: {
-      id: '',
-      name: '',
-    },
-    unit: '',
-    metricType: 'ametric',
-    service: '',
-    refId: 'A',
-    aggregation: {
-      crossSeriesReducer: '',
-      alignmentPeriod: '',
-      perSeriesAligner: '',
-      groupBys: [],
-    },
-    filters: existingFilters || [],
-    aliasBy: '',
-    metricService: '',
-    metricKind: '',
-    valueType: '',
-  };
+  Object.assign(StackdriverFilterCtrl.prototype, scope);
+  return new StackdriverFilterCtrl(scope, fakeSegmentServer, new TemplateSrvStub());
 }

+ 47 - 0
public/app/plugins/datasource/stackdriver/types.ts

@@ -19,3 +19,50 @@ export interface VariableQueryData {
   metricTypes: Array<{ value: string; name: string }>;
   services: Array<{ value: string; name: string }>;
 }
+
+export interface Target {
+  defaultProject: string;
+  unit: string;
+  metricType: string;
+  service: string;
+  refId: string;
+  crossSeriesReducer: string;
+  alignmentPeriod: string;
+  perSeriesAligner: string;
+  groupBys: string[];
+  filters: string[];
+  aliasBy: string;
+  metricKind: string;
+  valueType: string;
+}
+
+export interface AnnotationTarget {
+  defaultProject: string;
+  metricType: string;
+  refId: string;
+  filters: string[];
+  metricKind: string;
+  valueType: string;
+  title: string;
+  text: string;
+}
+
+export interface QueryMeta {
+  alignmentPeriod: string;
+  rawQuery: string;
+  rawQueryString: string;
+  metricLabels: { [key: string]: string[] };
+  resourceLabels: { [key: string]: string[] };
+  resourceTypes: string[];
+}
+
+export interface MetricDescriptor {
+  valueType: string;
+  metricKind: string;
+  type: string;
+  unit: string;
+  service: string;
+  serviceShortName: string;
+  displayName: string;
+  description: string;
+}

+ 8 - 1
public/app/plugins/panel/gauge/GaugePanel.tsx

@@ -15,6 +15,13 @@ export class GaugePanel extends PureComponent<Props> {
       nullValueMode: NullValueMode.Ignore,
     });
 
-    return <Gauge timeSeries={vmSeries} {...this.props.options} width={width} height={height} />;
+    return (
+      <Gauge
+        timeSeries={vmSeries}
+        {...this.props.options}
+        width={width}
+        height={height}
+      />
+    );
   }
 }

+ 5 - 0
public/app/types/templates.ts

@@ -0,0 +1,5 @@
+export interface Variable {
+  name: string;
+  type: string;
+  current: any;
+}

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff