Browse Source

Merge branch 'master' into 14812/formgroup-component

Peter Holmberg 7 years ago
parent
commit
219da09f0e
65 changed files with 8037 additions and 1104 deletions
  1. 2 2
      .circleci/config.yml
  2. 10 1
      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. 6 1
      docs/sources/features/explore/index.md
  8. 2 2
      latest.json
  9. 1 1
      packages/grafana-ui/src/components/ColorPicker/SpectrumPicker.tsx
  10. 0 0
      packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss
  11. 1 4
      packages/grafana-ui/src/components/Portal/Portal.tsx
  12. 1 0
      packages/grafana-ui/src/components/Select/SelectOption.test.tsx
  13. 9 3
      packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx
  14. 1 0
      packages/grafana-ui/src/components/Select/_Select.scss
  15. 1 0
      packages/grafana-ui/src/components/index.scss
  16. 2 0
      packages/grafana-ui/src/index.scss
  17. 0 0
      packages/grafana-ui/src/vendor/spectrum.css
  18. 0 0
      packages/grafana-ui/src/vendor/spectrum.js
  19. 2 1
      pkg/services/provisioning/dashboards/config_reader.go
  20. 2 1
      pkg/services/provisioning/dashboards/file_reader.go
  21. 45 4
      pkg/services/provisioning/dashboards/file_reader_test.go
  22. 1 1
      pkg/services/sqlstore/dashboard_provisioning.go
  23. 27 0
      public/app/core/angular_wrappers.ts
  24. 90 0
      public/app/core/components/Select/MetricSelect.tsx
  25. 1 0
      public/app/features/all.ts
  26. 0 0
      public/app/features/datasources/partials/http_settings.html
  27. 26 0
      public/app/features/datasources/settings/HttpSettingsCtrl.ts
  28. 6 4
      public/app/plugins/datasource/prometheus/datasource.ts
  29. 73 0
      public/app/plugins/datasource/prometheus/specs/datasource.test.ts
  30. 9 22
      public/app/plugins/datasource/stackdriver/annotations_query_ctrl.ts
  31. 57 0
      public/app/plugins/datasource/stackdriver/components/Aggregations.test.tsx
  32. 94 0
      public/app/plugins/datasource/stackdriver/components/Aggregations.tsx
  33. 52 0
      public/app/plugins/datasource/stackdriver/components/AliasBy.tsx
  34. 56 0
      public/app/plugins/datasource/stackdriver/components/AlignmentPeriods.tsx
  35. 33 0
      public/app/plugins/datasource/stackdriver/components/Alignments.tsx
  36. 119 0
      public/app/plugins/datasource/stackdriver/components/AnnotationQueryEditor.tsx
  37. 44 0
      public/app/plugins/datasource/stackdriver/components/AnnotationsHelp.tsx
  38. 115 0
      public/app/plugins/datasource/stackdriver/components/Filter.tsx
  39. 115 0
      public/app/plugins/datasource/stackdriver/components/Help.tsx
  40. 195 0
      public/app/plugins/datasource/stackdriver/components/Metrics.tsx
  41. 31 0
      public/app/plugins/datasource/stackdriver/components/Project.tsx
  42. 23 0
      public/app/plugins/datasource/stackdriver/components/QueryEditor.test.tsx
  43. 206 0
      public/app/plugins/datasource/stackdriver/components/QueryEditor.tsx
  44. 8 8
      public/app/plugins/datasource/stackdriver/components/VariableQueryEditor.tsx
  45. 119 0
      public/app/plugins/datasource/stackdriver/components/__snapshots__/Aggregations.test.tsx.snap
  46. 459 0
      public/app/plugins/datasource/stackdriver/components/__snapshots__/QueryEditor.test.tsx.snap
  47. 8 14
      public/app/plugins/datasource/stackdriver/datasource.ts
  48. 2 2
      public/app/plugins/datasource/stackdriver/filter_segments.ts
  49. 38 0
      public/app/plugins/datasource/stackdriver/functions.test.ts
  50. 18 0
      public/app/plugins/datasource/stackdriver/functions.ts
  51. 6 37
      public/app/plugins/datasource/stackdriver/partials/annotations.editor.html
  52. 0 46
      public/app/plugins/datasource/stackdriver/partials/query.aggregation.html
  53. 9 72
      public/app/plugins/datasource/stackdriver/partials/query.editor.html
  54. 1 27
      public/app/plugins/datasource/stackdriver/partials/query.filter.html
  55. 0 80
      public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts
  56. 12 83
      public/app/plugins/datasource/stackdriver/query_ctrl.ts
  57. 48 176
      public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts
  58. 0 1
      public/app/plugins/datasource/stackdriver/specs/datasource.test.ts
  59. 0 74
      public/app/plugins/datasource/stackdriver/specs/query_aggregation_ctrl.test.ts
  60. 31 59
      public/app/plugins/datasource/stackdriver/specs/query_filter_ctrl.test.ts
  61. 47 0
      public/app/plugins/datasource/stackdriver/types.ts
  62. 5 0
      public/app/types/templates.ts
  63. 0 2
      public/sass/_grafana.scss
  64. 0 0
      scripts/build/load-signing-key.sh
  65. 241 375
      yarn.lock

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

+ 10 - 1
CHANGELOG.md

@@ -19,17 +19,26 @@
 * **OAuth**: Support OAuth providers that are not RFC6749 compliant [#14562](https://github.com/grafana/grafana/issues/14562), thx [@tdabasinskas](https://github.com/tdabasinskas)
 * **Units**: Add blood glucose level units mg/dL and mmol/L [#14519](https://github.com/grafana/grafana/issues/14519), thx [@kjedamzik](https://github.com/kjedamzik)
 * **Stackdriver**: Aggregating series returns more than one series [#14581](https://github.com/grafana/grafana/issues/14581) and [#13914](https://github.com/grafana/grafana/issues/13914), thx [@kinok](https://github.com/kinok)
+* **Provisioning**: Fixes bug causing infinite growth in dashboard_version table. [#12864](https://github.com/grafana/grafana/issues/12864)
 
 ### Bug fixes
 * **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)
+* **Prometheus**: Query for annotation always uses 60s step regardless of dashboard range, fixes [#14795](https://github.com/grafana/grafana/issues/14795)
 
 # 5.4.3 (2019-01-14)
+
+### Tech
+
 * **Docker**: Build and publish docker images for armv7 and arm64 [#14617](https://github.com/grafana/grafana/pull/14617), thx [@johanneswuerbach](https://github.com/johanneswuerbach)
+* **Backend**: Upgrade to golang 1.11.4 [#14580](https://github.com/grafana/grafana/issues/14580)
+* **MySQL** only update session in mysql database when required [#14540](https://github.com/grafana/grafana/pull/14540)
 
 ### Bug fixes
 * **Alerting** Invalid frequency causes division by zero in alert scheduler [#14810](https://github.com/grafana/grafana/issues/14810)
 * **Dashboard** Dashboard links do not update when time range changes [#14493](https://github.com/grafana/grafana/issues/14493)
-
+* **Limits** Support more than 1000 datasources per org [#13883](https://github.com/grafana/grafana/issues/13883)
+* **Backend** fix signed in user for orgId=0 result should return active org id [#14574](https://github.com/grafana/grafana/pull/14574)
+* **Provisioning** Adds orgId to user dto for provisioned dashboards [#14678](https://github.com/grafana/grafana/pull/14678)
 
 # 5.4.2 (2018-12-13)
 

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

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

+ 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

@@ -4,6 +4,7 @@ import SelectOption from './SelectOption';
 import { OptionProps } from 'react-select/lib/components/Option';
 
 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 {

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

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

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

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

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

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

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

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

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

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

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

+ 0 - 2
public/sass/_grafana.scss

@@ -3,7 +3,6 @@
 
 // VENDOR
 @import '../vendor/css/timepicker.css';
-@import '../vendor/css/spectrum.css';
 @import '../vendor/css/rc-cascader.scss';
 
 // MIXINS
@@ -76,7 +75,6 @@
 @import 'components/typeahead';
 @import 'components/modals';
 @import 'components/dropdown';
-@import 'components/color_picker';
 @import 'components/footer';
 @import 'components/infobox';
 @import 'components/shortcuts';

+ 0 - 0
scripts/build/load-signing-key.sh


File diff suppressed because it is too large
+ 241 - 375
yarn.lock


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