Browse Source

Merge remote-tracking branch 'grafana/master'

* grafana/master: (51 commits)
  changing callback fn into arrow functions for correct usage of this (#12673)
  Fix requested changes
  Update CHANGELOG.md
  Add support for interval in query variable
  Change to arrow functions
  Add graph_ctrl jest
  changelog: add notes about closing #12691
  Update kbn.ts
  Add jest test file
  Id validation of CloudWatch GetMetricData
  changelog: adds note for #11487
  Datasource for Grafana logging platform
  fix: postgres/mysql engine cache was not being used, fixes #12636 (#12642)
  added: replaces added to grafana
  fix: datasource search was not working properly
  docs: minor docs fix
  Fix label suggestions in Explore query field
  pluginloader: expose flot gauge plugin
  alert: add missing test after refactor
  Handle query string in storage public_url (#9351) (#12555)
  ...
ryan 7 years ago
parent
commit
56927e55b6
100 changed files with 5238 additions and 3149 deletions
  1. 4 0
      CHANGELOG.md
  2. 12 3
      Gopkg.lock
  3. 1 1
      Gopkg.toml
  4. 1 0
      build.go
  5. 2 0
      conf/ldap.toml
  6. 10 5
      devenv/README.md
  7. 6 1
      devenv/datasources.yaml
  8. 0 592
      devenv/dev-dashboards/dashboard_with_rows.json
  9. 1558 0
      devenv/dev-dashboards/panel_tests_graph.json
  10. 574 0
      devenv/dev-dashboards/panel_tests_singlestat.json
  11. 453 0
      devenv/dev-dashboards/panel_tests_table.json
  12. 3 3
      devenv/dev-dashboards/testdata_alerts.json
  13. 13 7
      devenv/setup.sh
  14. 2 1
      docker/blocks/nginx_proxy/Dockerfile
  15. 3 0
      docker/blocks/nginx_proxy/htpasswd
  16. 20 1
      docker/blocks/nginx_proxy/nginx.conf
  17. 85 0
      docker/blocks/openldap/ldap_dev.toml
  18. 2 5
      docker/blocks/openldap/notes.md
  19. 286 0
      docs/sources/http_api/playlist.md
  20. 7 1
      docs/sources/installation/configuration.md
  21. 9 2
      docs/sources/installation/ldap.md
  22. 1 1
      docs/sources/reference/templating.md
  23. 1 2
      pkg/api/api.go
  24. 1 0
      pkg/api/playlist.go
  25. 12 3
      pkg/components/imguploader/webdavuploader.go
  26. 13 0
      pkg/components/imguploader/webdavuploader_test.go
  27. 7 0
      pkg/login/ext_user.go
  28. 4 3
      pkg/login/ldap.go
  29. 4 3
      pkg/login/ldap_settings.go
  30. 42 8
      pkg/login/ldap_test.go
  31. 8 0
      pkg/metrics/metrics.go
  32. 1 1
      pkg/models/playlist.go
  33. 9 8
      pkg/models/user_auth.go
  34. 3 0
      pkg/plugins/datasource_plugin.go
  35. 1 0
      pkg/services/sqlstore/alert.go
  36. 13 2
      pkg/services/sqlstore/alert_test.go
  37. 1 0
      pkg/services/sqlstore/dashboard_test.go
  38. 1 0
      pkg/services/sqlstore/org_test.go
  39. 11 4
      pkg/services/sqlstore/user.go
  40. 2 0
      pkg/setting/setting.go
  41. 191 26
      pkg/tsdb/cloudwatch/cloudwatch.go
  42. 4 0
      pkg/tsdb/cloudwatch/types.go
  43. 1 0
      pkg/tsdb/sql_engine.go
  44. 1 1
      pkg/tsdb/testdata/testdata.go
  45. 209 53
      public/app/containers/Explore/Explore.tsx
  46. 9 0
      public/app/containers/Explore/JSONViewer.tsx
  47. 66 0
      public/app/containers/Explore/Logs.tsx
  48. 4 2
      public/app/containers/Explore/QueryField.tsx
  49. 0 12
      public/app/containers/Explore/utils/query.ts
  50. 4 2
      public/app/core/components/scroll/page_scroll.ts
  51. 29 0
      public/app/core/logs_model.ts
  52. 1 1
      public/app/core/services/keybindingSrv.ts
  53. 2 0
      public/app/core/utils/kbn.ts
  54. 2 2
      public/app/features/panel/metrics_panel_ctrl.ts
  55. 4 5
      public/app/features/plugins/built_in_plugins.ts
  56. 14 5
      public/app/features/plugins/datasource_srv.ts
  57. 1 0
      public/app/features/plugins/ds_list_ctrl.ts
  58. 3 1
      public/app/features/plugins/plugin_loader.ts
  59. 27 1
      public/app/features/plugins/specs/datasource_srv.jest.ts
  60. 0 1448
      public/app/plugins/app/testdata/dashboards/graph_last_1h.json
  61. 0 34
      public/app/plugins/app/testdata/module.ts
  62. 0 32
      public/app/plugins/app/testdata/plugin.json
  63. 11 1
      public/app/plugins/datasource/cloudwatch/datasource.ts
  64. 31 14
      public/app/plugins/datasource/cloudwatch/partials/query.parameter.html
  65. 3 0
      public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts
  66. 3 0
      public/app/plugins/datasource/logging/README.md
  67. 38 0
      public/app/plugins/datasource/logging/datasource.jest.ts
  68. 134 0
      public/app/plugins/datasource/logging/datasource.ts
  69. 57 0
      public/app/plugins/datasource/logging/img/grafana_icon.svg
  70. 7 0
      public/app/plugins/datasource/logging/module.ts
  71. 2 0
      public/app/plugins/datasource/logging/partials/config.html
  72. 28 0
      public/app/plugins/datasource/logging/plugin.json
  73. 45 0
      public/app/plugins/datasource/logging/result_transformer.jest.ts
  74. 71 0
      public/app/plugins/datasource/logging/result_transformer.ts
  75. 3 3
      public/app/plugins/datasource/opentsdb/datasource.ts
  76. 28 5
      public/app/plugins/datasource/prometheus/datasource.ts
  77. 21 9
      public/app/plugins/datasource/prometheus/plugin.json
  78. 45 1
      public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
  79. 18 18
      public/app/plugins/datasource/prometheus/specs/datasource_specs.ts
  80. 0 0
      public/app/plugins/datasource/testdata/datasource.ts
  81. 0 0
      public/app/plugins/datasource/testdata/module.ts
  82. 0 1
      public/app/plugins/datasource/testdata/partials/query.editor.html
  83. 4 4
      public/app/plugins/datasource/testdata/plugin.json
  84. 0 0
      public/app/plugins/datasource/testdata/query_ctrl.ts
  85. 0 604
      public/app/plugins/panel/graph/jquery.flot.events.js
  86. 671 0
      public/app/plugins/panel/graph/jquery.flot.events.ts
  87. 94 0
      public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts
  88. 0 78
      public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts
  89. 2 2
      public/app/plugins/panel/singlestat/editor.html
  90. 1 1
      public/app/routes/routes.ts
  91. 0 36
      public/sass/_variables.dark.scss
  92. 0 31
      public/sass/_variables.light.scss
  93. 9 36
      public/sass/_variables.scss
  94. 7 3
      public/sass/base/_type.scss
  95. 1 0
      public/sass/pages/_dashboard.scss
  96. 41 0
      public/sass/pages/_explore.scss
  97. 2 2
      vendor/github.com/aws/aws-sdk-go/aws/client/client.go
  98. 87 15
      vendor/github.com/aws/aws-sdk-go/aws/client/logger.go
  99. 1 0
      vendor/github.com/aws/aws-sdk-go/aws/client/metadata/client_info.go
  100. 15 3
      vendor/github.com/aws/aws-sdk-go/aws/credentials/credentials.go

+ 4 - 0
CHANGELOG.md

@@ -2,6 +2,8 @@
 
 
 * **Dataproxy**: Pass configured/auth headers to a Datasource [#10971](https://github.com/grafana/grafana/issues/10971), thx [@mrsiano](https://github.com/mrsiano)
 * **Dataproxy**: Pass configured/auth headers to a Datasource [#10971](https://github.com/grafana/grafana/issues/10971), thx [@mrsiano](https://github.com/mrsiano)
 * **Cleanup**: Make temp file time to live configurable [#11607](https://github.com/grafana/grafana/issues/11607), thx [@xapon](https://github.com/xapon)
 * **Cleanup**: Make temp file time to live configurable [#11607](https://github.com/grafana/grafana/issues/11607), thx [@xapon](https://github.com/xapon)
+* **LDAP**: Define Grafana Admin permission in ldap group mappings [#2469](https://github.com/grafana/grafana/issues/2496), PR [#12622](https://github.com/grafana/grafana/issues/12622)
+* **Cloudwatch**: CloudWatch GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda)
 
 
 ### Minor
 ### Minor
 
 
@@ -11,11 +13,13 @@
 * **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2)
 * **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2)
 * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
 * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
 * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484)
 * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484)
+* **Prometheus**: Add $interval, $interval_ms, $range, and $range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597)
 * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
 * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
 * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)
 * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)
 * **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
 * **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
 * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)
 * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)
 * **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane)
 * **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane)
+* **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek)
 
 
 # 5.2.2 (unreleased)
 # 5.2.2 (unreleased)
 
 

+ 12 - 3
Gopkg.lock

@@ -32,6 +32,7 @@
     "aws/credentials/ec2rolecreds",
     "aws/credentials/ec2rolecreds",
     "aws/credentials/endpointcreds",
     "aws/credentials/endpointcreds",
     "aws/credentials/stscreds",
     "aws/credentials/stscreds",
+    "aws/csm",
     "aws/defaults",
     "aws/defaults",
     "aws/ec2metadata",
     "aws/ec2metadata",
     "aws/endpoints",
     "aws/endpoints",
@@ -43,6 +44,8 @@
     "internal/shareddefaults",
     "internal/shareddefaults",
     "private/protocol",
     "private/protocol",
     "private/protocol/ec2query",
     "private/protocol/ec2query",
+    "private/protocol/eventstream",
+    "private/protocol/eventstream/eventstreamapi",
     "private/protocol/query",
     "private/protocol/query",
     "private/protocol/query/queryutil",
     "private/protocol/query/queryutil",
     "private/protocol/rest",
     "private/protocol/rest",
@@ -54,8 +57,8 @@
     "service/s3",
     "service/s3",
     "service/sts"
     "service/sts"
   ]
   ]
-  revision = "c7cd1ebe87257cde9b65112fc876b0339ea0ac30"
-  version = "v1.13.49"
+  revision = "fde4ded7becdeae4d26bf1212916aabba79349b4"
+  version = "v1.14.12"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
@@ -424,6 +427,12 @@
   revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
   revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
   version = "v1.0.0"
   version = "v1.0.0"
 
 
+[[projects]]
+  branch = "master"
+  name = "github.com/shurcooL/sanitized_anchor_name"
+  packages = ["."]
+  revision = "86672fcb3f950f35f2e675df2240550f2a50762f"
+
 [[projects]]
 [[projects]]
   name = "github.com/smartystreets/assertions"
   name = "github.com/smartystreets/assertions"
   packages = [
   packages = [
@@ -670,6 +679,6 @@
 [solve-meta]
 [solve-meta]
   analyzer-name = "dep"
   analyzer-name = "dep"
   analyzer-version = 1
   analyzer-version = 1
-  inputs-digest = "85cc057e0cc074ab5b43bd620772d63d51e07b04e8782fcfe55e6929d2fc40f7"
+  inputs-digest = "cb8e7fd81f23ec987fc4d5dd9d31ae0f1164bc2f30cbea2fe86e0d97dd945beb"
   solver-name = "gps-cdcl"
   solver-name = "gps-cdcl"
   solver-version = 1
   solver-version = 1

+ 1 - 1
Gopkg.toml

@@ -36,7 +36,7 @@ ignored = [
 
 
 [[constraint]]
 [[constraint]]
   name = "github.com/aws/aws-sdk-go"
   name = "github.com/aws/aws-sdk-go"
-  version = "1.12.65"
+  version = "1.13.56"
 
 
 [[constraint]]
 [[constraint]]
   branch = "master"
   branch = "master"

+ 1 - 0
build.go

@@ -330,6 +330,7 @@ func createPackage(options linuxPackageOptions) {
 	name := "grafana"
 	name := "grafana"
 	if enterprise {
 	if enterprise {
 		name += "-enterprise"
 		name += "-enterprise"
+		args = append(args, "--replaces", "grafana")
 	}
 	}
 	args = append(args, "--name", name)
 	args = append(args, "--name", name)
 
 

+ 2 - 0
conf/ldap.toml

@@ -72,6 +72,8 @@ email =  "email"
 [[servers.group_mappings]]
 [[servers.group_mappings]]
 group_dn = "cn=admins,dc=grafana,dc=org"
 group_dn = "cn=admins,dc=grafana,dc=org"
 org_role = "Admin"
 org_role = "Admin"
+# To make user an instance admin  (Grafana Admin) uncomment line below
+# grafana_admin = true
 # The Grafana organization database id, optional, if left out the default org (id 1) will be used
 # The Grafana organization database id, optional, if left out the default org (id 1) will be used
 # org_id = 1
 # org_id = 1
 
 

+ 10 - 5
devenv/README.md

@@ -1,11 +1,16 @@
 This folder contains useful scripts and configuration for...
 This folder contains useful scripts and configuration for...
 
 
-* Configuring datasources in Grafana
-* Provision example dashboards in Grafana
-* Run preconfiured datasources as docker containers
-
-want to know more? run setup!
+* Configuring dev datasources in Grafana
+* Configuring dev & test scenarios dashboards.
 
 
 ```bash
 ```bash
 ./setup.sh
 ./setup.sh
 ```
 ```
+
+After restarting grafana server there should now be a number of datasources named `gdev-<type>` provisioned as well as a dashboard folder named `gdev dashboards`. This folder contains dashboard & panel features tests dashboards. 
+
+# Dev dashboards
+
+Please update these dashboards or make new ones as new panels & dashboards features are developed or new bugs are found. The dashboards are located in the `devenv/dev-dashboards` folder. 
+
+

+ 6 - 1
devenv/datasources.yaml

@@ -14,6 +14,9 @@ datasources:
     isDefault: true
     isDefault: true
     url: http://localhost:9090
     url: http://localhost:9090
 
 
+  - name: gdev-testdata
+    type: testdata
+
   - name: gdev-influxdb
   - name: gdev-influxdb
     type: influxdb
     type: influxdb
     access: proxy
     access: proxy
@@ -60,7 +63,8 @@ datasources:
     url: localhost:5432
     url: localhost:5432
     database: grafana
     database: grafana
     user: grafana
     user: grafana
-    password: password
+    secureJsonData:
+      password: password
     jsonData:
     jsonData:
       sslmode: "disable"
       sslmode: "disable"
 
 
@@ -71,3 +75,4 @@ datasources:
       authType: credentials
       authType: credentials
       defaultRegion: eu-west-2
       defaultRegion: eu-west-2
 
 
+

+ 0 - 592
devenv/dev-dashboards/dashboard_with_rows.json

@@ -1,592 +0,0 @@
-{
-  "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,
-  "id": 59,
-  "links": [],
-  "panels": [
-    {
-      "collapsed": false,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 0
-      },
-      "id": 9,
-      "panels": [],
-      "title": "Row title",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 0,
-        "y": 1
-      },
-      "id": 12,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "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": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 12,
-        "y": 1
-      },
-      "id": 5,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "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
-      }
-    },
-    {
-      "collapsed": false,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 5
-      },
-      "id": 7,
-      "panels": [],
-      "title": "Row",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 0,
-        "y": 6
-      },
-      "id": 2,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "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": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 12,
-        "y": 6
-      },
-      "id": 13,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "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
-      }
-    },
-    {
-      "collapsed": false,
-      "gridPos": {
-        "h": 1,
-        "w": 24,
-        "x": 0,
-        "y": 10
-      },
-      "id": 11,
-      "panels": [],
-      "title": "Row title",
-      "type": "row"
-    },
-    {
-      "aliasColors": {},
-      "bars": false,
-      "dashLength": 10,
-      "dashes": false,
-      "datasource": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 0,
-        "y": 11
-      },
-      "id": 4,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "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": "Prometheus",
-      "fill": 1,
-      "gridPos": {
-        "h": 4,
-        "w": 12,
-        "x": 12,
-        "y": 11
-      },
-      "id": 3,
-      "legend": {
-        "avg": false,
-        "current": false,
-        "max": false,
-        "min": false,
-        "show": true,
-        "total": false,
-        "values": false
-      },
-      "lines": true,
-      "linewidth": 1,
-      "nullPointMode": "null",
-      "percentage": false,
-      "pointradius": 5,
-      "points": false,
-      "renderer": "flot",
-      "seriesOverrides": [],
-      "spaceLength": 10,
-      "stack": false,
-      "steppedLine": false,
-      "targets": [
-        {
-          "expr": "go_goroutines",
-          "format": "time_series",
-          "intervalFactor": 1,
-          "refId": "A"
-        }
-      ],
-      "thresholds": [],
-      "timeFrom": null,
-      "timeShift": null,
-      "title": "Panel Title",
-      "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
-      }
-    }
-  ],
-  "schemaVersion": 16,
-  "style": "dark",
-  "tags": [],
-  "templating": {
-    "list": []
-  },
-  "time": {
-    "from": "now-30m",
-    "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": "Dashboard with rows",
-  "uid": "1DdOzBNmk",
-  "version": 5
-}

+ 1558 - 0
devenv/dev-dashboards/panel_tests_graph.json

@@ -0,0 +1,1558 @@
+{
+  "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",
+      "decimals": 3,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 24,
+        "x": 0,
+        "y": 44
+      },
+      "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": 51
+      },
+      "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": 51
+      },
+      "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": 58
+      },
+      "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": 58
+      },
+      "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",
+  "uid": "5SdHCadmz",
+  "version": 3
+}

+ 574 - 0
devenv/dev-dashboards/panel_tests_singlestat.json

@@ -0,0 +1,574 @@
+{
+  "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": [
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorValue": true,
+      "colors": [
+        "#299c46",
+        "rgba(237, 129, 40, 0.89)",
+        "#d44a3a"
+      ],
+      "datasource": "gdev-testdata",
+      "decimals": null,
+      "description": "",
+      "format": "ms",
+      "gauge": {
+        "maxValue": 100,
+        "minValue": 0,
+        "show": false,
+        "thresholdLabels": false,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 0,
+        "y": 0
+      },
+      "id": 2,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "postfix",
+      "postfixFontSize": "50%",
+      "prefix": "prefix",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": false,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": true
+      },
+      "tableColumn": "",
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,2,3,4,5"
+        }
+      ],
+      "thresholds": "5,10",
+      "title": "prefix 3 ms (green) postfixt + sparkline",
+      "type": "singlestat",
+      "valueFontSize": "80%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "avg"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorPrefix": false,
+      "colorValue": true,
+      "colors": [
+        "#d44a3a",
+        "rgba(237, 129, 40, 0.89)",
+        "#299c46"
+      ],
+      "datasource": "gdev-testdata",
+      "decimals": null,
+      "description": "",
+      "format": "ms",
+      "gauge": {
+        "maxValue": 100,
+        "minValue": 0,
+        "show": false,
+        "thresholdLabels": false,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 8,
+        "y": 0
+      },
+      "id": 3,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": true,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": true
+      },
+      "tableColumn": "",
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,2,3,4,5"
+        }
+      ],
+      "thresholds": "5,10",
+      "title": "3 ms (red)  + full height sparkline",
+      "type": "singlestat",
+      "valueFontSize": "200%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "avg"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": true,
+      "colorPrefix": false,
+      "colorValue": false,
+      "colors": [
+        "#d44a3a",
+        "rgba(237, 129, 40, 0.89)",
+        "#299c46"
+      ],
+      "datasource": "gdev-testdata",
+      "decimals": null,
+      "description": "",
+      "format": "ms",
+      "gauge": {
+        "maxValue": 100,
+        "minValue": 0,
+        "show": false,
+        "thresholdLabels": false,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 16,
+        "y": 0
+      },
+      "id": 4,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": true,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": false
+      },
+      "tableColumn": "",
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,2,3,4,5"
+        }
+      ],
+      "thresholds": "5,10",
+      "title": "3 ms + red background",
+      "type": "singlestat",
+      "valueFontSize": "200%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "avg"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorPrefix": false,
+      "colorValue": true,
+      "colors": [
+        "#299c46",
+        "rgba(237, 129, 40, 0.89)",
+        "#d44a3a"
+      ],
+      "datasource": "gdev-testdata",
+      "decimals": null,
+      "description": "",
+      "format": "ms",
+      "gauge": {
+        "maxValue": 150,
+        "minValue": 0,
+        "show": true,
+        "thresholdLabels": true,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 0,
+        "y": 7
+      },
+      "id": 5,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": true,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": false
+      },
+      "tableColumn": "",
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "10,20,80"
+        }
+      ],
+      "thresholds": "81,90",
+      "title": "80 ms green gauge, thresholds 81, 90",
+      "type": "singlestat",
+      "valueFontSize": "80%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "current"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorPrefix": false,
+      "colorValue": true,
+      "colors": [
+        "#299c46",
+        "rgba(237, 129, 40, 0.89)",
+        "#d44a3a"
+      ],
+      "datasource": "gdev-testdata",
+      "decimals": null,
+      "description": "",
+      "format": "ms",
+      "gauge": {
+        "maxValue": 150,
+        "minValue": 0,
+        "show": true,
+        "thresholdLabels": false,
+        "thresholdMarkers": true
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 8,
+        "y": 7
+      },
+      "id": 6,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": true,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": false
+      },
+      "tableColumn": "",
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "10,20,80"
+        }
+      ],
+      "thresholds": "81,90",
+      "title": "80 ms green gauge, thresholds 81, 90, no labels",
+      "type": "singlestat",
+      "valueFontSize": "80%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "current"
+    },
+    {
+      "cacheTimeout": null,
+      "colorBackground": false,
+      "colorPrefix": false,
+      "colorValue": true,
+      "colors": [
+        "#299c46",
+        "rgba(237, 129, 40, 0.89)",
+        "#d44a3a"
+      ],
+      "datasource": "gdev-testdata",
+      "decimals": null,
+      "description": "",
+      "format": "ms",
+      "gauge": {
+        "maxValue": 150,
+        "minValue": 0,
+        "show": true,
+        "thresholdLabels": false,
+        "thresholdMarkers": false
+      },
+      "gridPos": {
+        "h": 7,
+        "w": 8,
+        "x": 16,
+        "y": 7
+      },
+      "id": 7,
+      "interval": null,
+      "links": [],
+      "mappingType": 1,
+      "mappingTypes": [
+        {
+          "name": "value to text",
+          "value": 1
+        },
+        {
+          "name": "range to text",
+          "value": 2
+        }
+      ],
+      "maxDataPoints": 100,
+      "nullPointMode": "connected",
+      "nullText": null,
+      "postfix": "",
+      "postfixFontSize": "50%",
+      "prefix": "",
+      "prefixFontSize": "50%",
+      "rangeMaps": [
+        {
+          "from": "null",
+          "text": "N/A",
+          "to": "null"
+        }
+      ],
+      "sparkline": {
+        "fillColor": "rgba(31, 118, 189, 0.18)",
+        "full": true,
+        "lineColor": "rgb(31, 120, 193)",
+        "show": false
+      },
+      "tableColumn": "",
+      "targets": [
+        {
+          "expr": "",
+          "format": "time_series",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "10,20,80"
+        }
+      ],
+      "thresholds": "81,90",
+      "title": "80 ms green gauge, thresholds 81, 90, no markers or labels",
+      "type": "singlestat",
+      "valueFontSize": "80%",
+      "valueMaps": [
+        {
+          "op": "=",
+          "text": "N/A",
+          "value": "null"
+        }
+      ],
+      "valueName": "current"
+    }
+  ],
+  "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 - Singlestat",
+  "uid": "singlestat",
+  "version": 14
+}

+ 453 - 0
devenv/dev-dashboards/panel_tests_table.json

@@ -0,0 +1,453 @@
+{
+  "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": [
+    {
+      "columns": [],
+      "datasource": "gdev-testdata",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 11,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "id": 3,
+      "links": [],
+      "pageSize": 10,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "Time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": "cell",
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "mappingType": 1,
+          "pattern": "ColorCell",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "currencyUSD"
+        },
+        {
+          "alias": "",
+          "colorMode": "value",
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "mappingType": 1,
+          "pattern": "ColorValue",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "Bps"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "server1",
+          "expr": "",
+          "format": "table",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0,20,10"
+        },
+        {
+          "alias": "server2",
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0"
+        }
+      ],
+      "title": "Time series to rows (2 pages)",
+      "transform": "timeseries_to_rows",
+      "type": "table"
+    },
+    {
+      "columns": [
+        {
+          "text": "Avg",
+          "value": "avg"
+        },
+        {
+          "text": "Max",
+          "value": "max"
+        },
+        {
+          "text": "Current",
+          "value": "current"
+        }
+      ],
+      "datasource": "gdev-testdata",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 11,
+        "w": 12,
+        "x": 12,
+        "y": 0
+      },
+      "id": 4,
+      "links": [],
+      "pageSize": 10,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "Time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": "cell",
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "mappingType": 1,
+          "pattern": "ColorCell",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "currencyUSD"
+        },
+        {
+          "alias": "",
+          "colorMode": "value",
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "mappingType": 1,
+          "pattern": "ColorValue",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "Bps"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "server1",
+          "expr": "",
+          "format": "table",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0,20,10"
+        },
+        {
+          "alias": "server2",
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0"
+        }
+      ],
+      "title": "Time series aggregations",
+      "transform": "timeseries_aggregations",
+      "type": "table"
+    },
+    {
+      "columns": [],
+      "datasource": "gdev-testdata",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 7,
+        "w": 24,
+        "x": 0,
+        "y": 11
+      },
+      "id": 5,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "Time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": "row",
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "mappingType": 1,
+          "pattern": "/Color/",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "currencyUSD"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "ColorValue",
+          "expr": "",
+          "format": "table",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0,20,10"
+        }
+      ],
+      "title": "color row by threshold",
+      "transform": "timeseries_to_columns",
+      "type": "table"
+    },
+    {
+      "columns": [],
+      "datasource": "gdev-testdata",
+      "fontSize": "100%",
+      "gridPos": {
+        "h": 8,
+        "w": 24,
+        "x": 0,
+        "y": 18
+      },
+      "id": 2,
+      "links": [],
+      "pageSize": null,
+      "scroll": true,
+      "showHeader": true,
+      "sort": {
+        "col": 0,
+        "desc": true
+      },
+      "styles": [
+        {
+          "alias": "Time",
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "pattern": "Time",
+          "type": "date"
+        },
+        {
+          "alias": "",
+          "colorMode": "cell",
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "mappingType": 1,
+          "pattern": "ColorCell",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "currencyUSD"
+        },
+        {
+          "alias": "",
+          "colorMode": "value",
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "dateFormat": "YYYY-MM-DD HH:mm:ss",
+          "decimals": 2,
+          "mappingType": 1,
+          "pattern": "ColorValue",
+          "thresholds": [
+            "5",
+            "10"
+          ],
+          "type": "number",
+          "unit": "Bps"
+        },
+        {
+          "alias": "",
+          "colorMode": null,
+          "colors": [
+            "rgba(245, 54, 54, 0.9)",
+            "rgba(237, 129, 40, 0.89)",
+            "rgba(50, 172, 45, 0.97)"
+          ],
+          "decimals": 2,
+          "pattern": "/.*/",
+          "thresholds": [],
+          "type": "number",
+          "unit": "short"
+        }
+      ],
+      "targets": [
+        {
+          "alias": "ColorValue",
+          "expr": "",
+          "format": "table",
+          "intervalFactor": 1,
+          "refId": "A",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0,20,10"
+        },
+        {
+          "alias": "ColorCell",
+          "refId": "B",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "5,1,2,3,4,5,10,20"
+        }
+      ],
+      "title": "Column style thresholds & units",
+      "transform": "timeseries_to_columns",
+      "type": "table"
+    }
+  ],
+  "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 - Table",
+  "uid": "pttable",
+  "version": 1
+}

+ 3 - 3
public/app/plugins/app/testdata/dashboards/alerts.json → devenv/dev-dashboards/testdata_alerts.json

@@ -1,6 +1,6 @@
 {
 {
   "revision": 2,
   "revision": 2,
-  "title": "TestData - Alerts",
+  "title": "Alerting with TestData",
   "tags": [
   "tags": [
     "grafana-test"
     "grafana-test"
   ],
   ],
@@ -48,7 +48,7 @@
           },
           },
           "aliasColors": {},
           "aliasColors": {},
           "bars": false,
           "bars": false,
-          "datasource": "Grafana TestData",
+          "datasource": "gdev-testdata",
           "editable": true,
           "editable": true,
           "error": false,
           "error": false,
           "fill": 1,
           "fill": 1,
@@ -161,7 +161,7 @@
           },
           },
           "aliasColors": {},
           "aliasColors": {},
           "bars": false,
           "bars": false,
-          "datasource": "Grafana TestData",
+          "datasource": "gdev-testdata",
           "editable": true,
           "editable": true,
           "error": false,
           "error": false,
           "fill": 1,
           "fill": 1,

+ 13 - 7
devenv/setup.sh

@@ -1,4 +1,4 @@
-#/bin/bash
+#!/bin/bash
 
 
 bulkDashboard() {
 bulkDashboard() {
 
 
@@ -22,31 +22,37 @@ requiresJsonnet() {
 		fi
 		fi
 }
 }
 
 
-defaultDashboards() {
+devDashboards() {
+		echo -e "\xE2\x9C\x94 Setting up all dev dashboards using provisioning"
 		ln -s -f ../../../devenv/dashboards.yaml ../conf/provisioning/dashboards/dev.yaml
 		ln -s -f ../../../devenv/dashboards.yaml ../conf/provisioning/dashboards/dev.yaml
 }
 }
 
 
-defaultDatasources() {
-		echo "setting up all default datasources using provisioning"
+devDatasources() {
+		echo -e "\xE2\x9C\x94 Setting up all dev datasources using provisioning"
 
 
 		ln -s -f ../../../devenv/datasources.yaml ../conf/provisioning/datasources/dev.yaml
 		ln -s -f ../../../devenv/datasources.yaml ../conf/provisioning/datasources/dev.yaml
 }
 }
 
 
 usage() {
 usage() {
-	echo -e "install.sh\n\tThis script setups dev provision for datasources and dashboards"
+	echo -e "\n"
 	echo "Usage:"
 	echo "Usage:"
 	echo "  bulk-dashboards                     - create and provisioning 400 dashboards"
 	echo "  bulk-dashboards                     - create and provisioning 400 dashboards"
 	echo "  no args                             - provisiong core datasources and dev dashboards"
 	echo "  no args                             - provisiong core datasources and dev dashboards"
 }
 }
 
 
 main() {
 main() {
+	echo -e "------------------------------------------------------------------"
+	echo -e "This script setups provisioning for dev datasources and dashboards"
+	echo -e "------------------------------------------------------------------"
+	echo -e "\n"
+
 	local cmd=$1
 	local cmd=$1
 
 
 	if [[ $cmd == "bulk-dashboards" ]]; then
 	if [[ $cmd == "bulk-dashboards" ]]; then
 		bulkDashboard
 		bulkDashboard
 	else
 	else
-		defaultDashboards
-		defaultDatasources
+		devDashboards
+		devDatasources
 	fi
 	fi
 
 
   if [[ -z "$cmd" ]]; then
   if [[ -z "$cmd" ]]; then

+ 2 - 1
docker/blocks/nginx_proxy/Dockerfile

@@ -1,3 +1,4 @@
 FROM nginx:alpine
 FROM nginx:alpine
 
 
-COPY nginx.conf /etc/nginx/nginx.conf
+COPY nginx.conf /etc/nginx/nginx.conf
+COPY htpasswd /etc/nginx/htpasswd

+ 3 - 0
docker/blocks/nginx_proxy/htpasswd

@@ -0,0 +1,3 @@
+user1:$apr1$1odeeQb.$kwV8D/VAAGUDU7pnHuKoV0
+user2:$apr1$A2kf25r.$6S0kp3C7vIuixS5CL0XA9.
+admin:$apr1$IWn4DoRR$E2ol7fS/dkI18eU4bXnBO1

+ 20 - 1
docker/blocks/nginx_proxy/nginx.conf

@@ -13,7 +13,26 @@ http {
     listen 10080;
     listen 10080;
 
 
     location /grafana/ {
     location /grafana/ {
+      ################################################################
+      # Enable these settings to test with basic auth and an auth proxy header
+      # the htpasswd file contains an admin user with password admin and
+      # user1: grafana and user2: grafana
+      ################################################################
+
+      # auth_basic "Restricted Content";
+      # auth_basic_user_file /etc/nginx/htpasswd;
+
+      ################################################################
+      # To use the auth proxy header, set the following in custom.ini:
+      # [auth.proxy]
+      # enabled = true
+      # header_name = X-WEBAUTH-USER
+      # header_property = username
+      ################################################################
+
+      # proxy_set_header X-WEBAUTH-USER $remote_user;
+
       proxy_pass http://localhost:3000/;
       proxy_pass http://localhost:3000/;
     }
     }
   }
   }
-}
+}

+ 85 - 0
docker/blocks/openldap/ldap_dev.toml

@@ -0,0 +1,85 @@
+# To troubleshoot and get more log info enable ldap debug logging in grafana.ini
+# [log]
+# filters = ldap:debug
+
+[[servers]]
+# Ldap server host (specify multiple hosts space separated)
+host = "127.0.0.1"
+# Default port is 389 or 636 if use_ssl = true
+port = 389
+# Set to true if ldap server supports TLS
+use_ssl = false
+# Set to true if connect ldap server with STARTTLS pattern (create connection in insecure, then upgrade to secure connection with TLS)
+start_tls = false
+# set to true if you want to skip ssl cert validation
+ssl_skip_verify = false
+# set to the path to your root CA certificate or leave unset to use system defaults
+# root_ca_cert = "/path/to/certificate.crt"
+
+# Search user bind dn
+bind_dn = "cn=admin,dc=grafana,dc=org"
+# Search user bind password
+# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
+bind_password = 'grafana'
+
+# User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"
+search_filter = "(cn=%s)"
+
+# An array of base dns to search through
+search_base_dns = ["dc=grafana,dc=org"]
+
+# In POSIX LDAP schemas, without memberOf attribute a secondary query must be made for groups.
+# This is done by enabling group_search_filter below. You must also set member_of= "cn"
+# in [servers.attributes] below.
+
+# Users with nested/recursive group membership and an LDAP server that supports LDAP_MATCHING_RULE_IN_CHAIN
+# can set group_search_filter, group_search_filter_user_attribute, group_search_base_dns and member_of
+# below in such a way that the user's recursive group membership is considered.
+#
+# Nested Groups + Active Directory (AD) Example:
+#
+#   AD groups store the Distinguished Names (DNs) of members, so your filter must
+#   recursively search your groups for the authenticating user's DN. For example:
+#
+#     group_search_filter = "(member:1.2.840.113556.1.4.1941:=%s)"
+#     group_search_filter_user_attribute = "distinguishedName"
+#     group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
+#
+#     [servers.attributes]
+#     ...
+#     member_of = "distinguishedName"
+
+## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available)
+# group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))"
+## Group search filter user attribute defines what user attribute gets substituted for %s in group_search_filter.
+## Defaults to the value of username in [server.attributes]
+## Valid options are any of your values in [servers.attributes]
+## If you are using nested groups you probably want to set this and member_of in
+## [servers.attributes] to "distinguishedName"
+# group_search_filter_user_attribute = "distinguishedName"
+## An array of the base DNs to search through for groups. Typically uses ou=groups
+# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
+
+# Specify names of the ldap attributes your ldap uses
+[servers.attributes]
+name = "givenName"
+surname = "sn"
+username = "cn"
+member_of = "memberOf"
+email =  "email"
+
+# Map ldap groups to grafana org roles
+[[servers.group_mappings]]
+group_dn = "cn=admins,ou=groups,dc=grafana,dc=org"
+org_role = "Admin"
+# The Grafana organization database id, optional, if left out the default org (id 1) will be used
+# org_id = 1
+
+[[servers.group_mappings]]
+group_dn = "cn=editors,ou=groups,dc=grafana,dc=org"
+org_role = "Editor"
+
+[[servers.group_mappings]]
+# If you want to match all (or no ldap groups) then you can use wildcard
+group_dn = "*"
+org_role = "Viewer"

+ 2 - 5
docker/blocks/openldap/notes.md

@@ -14,12 +14,12 @@ After adding ldif files to `prepopulate`:
 
 
 ## Enabling LDAP in Grafana
 ## Enabling LDAP in Grafana
 
 
-The default `ldap.toml` file in `conf` has host set to `127.0.0.1` and port to set to 389 so all you need to do is enable it in the .ini file to get Grafana to use this block:
+Copy the ldap_dev.toml file in this folder into your `conf` folder (it is gitignored already). To enable it in the .ini file to get Grafana to use this block:
 
 
 ```ini
 ```ini
 [auth.ldap]
 [auth.ldap]
 enabled = true
 enabled = true
-config_file = conf/ldap.toml
+config_file = conf/ldap_dev.toml
 ; allow_sign_up = true
 ; allow_sign_up = true
 ```
 ```
 
 
@@ -43,6 +43,3 @@ editors
 
 
 no groups
 no groups
   ldap-viewer
   ldap-viewer
-
-
-

+ 286 - 0
docs/sources/http_api/playlist.md

@@ -0,0 +1,286 @@
++++
+title = "Playlist HTTP API "
+description = "Playlist Admin HTTP API"
+keywords = ["grafana", "http", "documentation", "api", "playlist"]
+aliases = ["/http_api/playlist/"]
+type = "docs"
+[menu.docs]
+name = "Playlist"
+parent = "http_api"
++++
+
+# Playlist API
+
+## Search Playlist
+
+`GET /api/playlists`
+
+Get all existing playlist for the current organization using pagination
+
+**Example Request**:
+
+```bash
+GET /api/playlists HTTP/1.1
+Accept: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+  Querystring Parameters:
+
+  These parameters are used as querystring parameters.
+  
+  - **query** - Limit response to playlist having a name like this value.
+  - **limit** - Limit response to *X* number of playlist.
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+[
+  {
+    "id": 1,
+    "name": "my playlist",
+    "interval": "5m"
+  }
+]
+```
+
+## Get one playlist
+
+`GET /api/playlists/:id`
+
+**Example Request**:
+
+```bash
+GET /api/playlists/1 HTTP/1.1
+Accept: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+{
+  "id" : 1,
+  "name": "my playlist",
+  "interval": "5m",
+  "orgId": "my org",
+  "items": [
+    {
+      "id": 1,
+      "playlistId": 1,
+      "type": "dashboard_by_id",
+      "value": "3",
+      "order": 1,
+      "title":"my third dasboard"
+    },
+    {
+      "id": 2,
+      "playlistId": 1,
+      "type": "dashboard_by_tag",
+      "value": "myTag",
+      "order": 2,
+      "title":"my other dasboard"
+    }
+  ]
+}
+```
+
+## Get Playlist items
+
+`GET /api/playlists/:id/items`
+
+**Example Request**:
+
+```bash
+GET /api/playlists/1/items HTTP/1.1
+Accept: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+[
+  {
+    "id": 1,
+    "playlistId": 1,
+    "type": "dashboard_by_id",
+    "value": "3",
+    "order": 1,
+    "title":"my third dasboard"
+  },
+  {
+    "id": 2,
+    "playlistId": 1,
+    "type": "dashboard_by_tag",
+    "value": "myTag",
+    "order": 2,
+    "title":"my other dasboard"
+  }
+]
+```
+
+## Get Playlist dashboards
+
+`GET /api/playlists/:id/dashboards`
+
+**Example Request**:
+
+```bash
+GET /api/playlists/1/dashboards HTTP/1.1
+Accept: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+[
+  {
+    "id": 3,
+    "title": "my third dasboard",
+    "order": 1,
+  },
+  {
+    "id": 5,
+    "title":"my other dasboard"
+    "order": 2,
+    
+  }
+]
+```
+
+## Create a playlist
+
+`POST /api/playlists/`
+
+**Example Request**:
+
+```bash
+PUT /api/playlists/1 HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+  {
+    "name": "my playlist",
+    "interval": "5m",
+    "items": [
+      {
+        "type": "dashboard_by_id",
+        "value": "3",
+        "order": 1,
+        "title":"my third dasboard"
+      },
+      {
+        "type": "dashboard_by_tag",
+        "value": "myTag",
+        "order": 2,
+        "title":"my other dasboard"
+      }
+    ]
+  }
+```
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+  {
+    "id": 1,
+    "name": "my playlist",
+    "interval": "5m"
+  }
+```
+
+## Update a playlist
+
+`PUT /api/playlists/:id`
+
+**Example Request**:
+
+```bash
+PUT /api/playlists/1 HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+  {
+    "name": "my playlist",
+    "interval": "5m",
+    "items": [
+      {
+        "playlistId": 1,
+        "type": "dashboard_by_id",
+        "value": "3",
+        "order": 1,
+        "title":"my third dasboard"
+      },
+      {
+        "playlistId": 1,
+        "type": "dashboard_by_tag",
+        "value": "myTag",
+        "order": 2,
+        "title":"my other dasboard"
+      }
+    ]
+  }
+```
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+{
+  "id" : 1,
+  "name": "my playlist",
+  "interval": "5m",
+  "orgId": "my org",
+  "items": [
+    {
+      "id": 1,
+      "playlistId": 1,
+      "type": "dashboard_by_id",
+      "value": "3",
+      "order": 1,
+      "title":"my third dasboard"
+    },
+    {
+      "id": 2,
+      "playlistId": 1,
+      "type": "dashboard_by_tag",
+      "value": "myTag",
+      "order": 2,
+      "title":"my other dasboard"
+    }
+  ]
+}
+```
+
+## Delete a playlist
+
+`DELETE /api/playlists/:id`
+
+**Example Request**:
+
+```bash
+DELETE /api/playlists/1 HTTP/1.1
+Accept: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+{}
+```

+ 7 - 1
docs/sources/installation/configuration.md

@@ -296,6 +296,12 @@ Set to `true` to automatically add new users to the main organization
 (id 1). When set to `false`, new users will automatically cause a new
 (id 1). When set to `false`, new users will automatically cause a new
 organization to be created for that new user.
 organization to be created for that new user.
 
 
+### auto_assign_org_id
+
+Set this value to automatically add new users to the provided org.
+This requires `auto_assign_org` to be set to `true`. Please make sure
+that this organization does already exists.
+
 ### auto_assign_org_role
 ### auto_assign_org_role
 
 
 The role new users will be assigned for the main organization (if the
 The role new users will be assigned for the main organization (if the
@@ -857,7 +863,7 @@ Secret key. e.g. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
 Url to where Grafana will send PUT request with images
 Url to where Grafana will send PUT request with images
 
 
 ### public_url
 ### public_url
-Optional parameter. Url to send to users in notifications, directly appended with the resulting uploaded file name.
+Optional parameter. Url to send to users in notifications. If the string contains the sequence ${file}, it will be replaced with the uploaded filename. Otherwise, the file name will be appended to the path part of the url, leaving any query string unchanged.
 
 
 ### username
 ### username
 basic auth username
 basic auth username

+ 9 - 2
docs/sources/installation/ldap.md

@@ -23,8 +23,9 @@ specific configuration file (default: `/etc/grafana/ldap.toml`).
 ### Example config
 ### Example config
 
 
 ```toml
 ```toml
-# Set to true to log user information returned from LDAP
-verbose_logging = false
+# To troubleshoot and get more log info enable ldap debug logging in grafana.ini
+# [log]
+# filters = ldap:debug
 
 
 [[servers]]
 [[servers]]
 # Ldap server host (specify multiple hosts space separated)
 # Ldap server host (specify multiple hosts space separated)
@@ -73,6 +74,8 @@ email =  "email"
 [[servers.group_mappings]]
 [[servers.group_mappings]]
 group_dn = "cn=admins,dc=grafana,dc=org"
 group_dn = "cn=admins,dc=grafana,dc=org"
 org_role = "Admin"
 org_role = "Admin"
+# To make user an instance admin  (Grafana Admin) uncomment line below
+# grafana_admin = true
 # The Grafana organization database id, optional, if left out the default org (id 1) will be used.  Setting this allows for multiple group_dn's to be assigned to the same org_role provided the org_id differs
 # The Grafana organization database id, optional, if left out the default org (id 1) will be used.  Setting this allows for multiple group_dn's to be assigned to the same org_role provided the org_id differs
 # org_id = 1
 # org_id = 1
 
 
@@ -132,6 +135,10 @@ Users page, this change will be reset the next time the user logs in. If you
 change the LDAP groups of a user, the change will take effect the next
 change the LDAP groups of a user, the change will take effect the next
 time the user logs in.
 time the user logs in.
 
 
+### Grafana Admin
+with a servers.group_mappings section you can set grafana_admin = true or false to sync Grafana Admin permission. A Grafana server admin has admin access over all orgs &
+users.
+
 ### Priority
 ### Priority
 The first group mapping that an LDAP user is matched to will be used for the sync. If you have LDAP users that fit multiple mappings, the topmost mapping in the TOML config will be used.
 The first group mapping that an LDAP user is matched to will be used for the sync. If you have LDAP users that fit multiple mappings, the topmost mapping in the TOML config will be used.
 
 

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

@@ -11,7 +11,7 @@ weight = 1
 # Variables
 # Variables
 
 
 Variables allows for more interactive and dynamic dashboards. Instead of hard-coding things like server, application
 Variables allows for more interactive and dynamic dashboards. Instead of hard-coding things like server, application
-and sensor name in you metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of
+and sensor name in your metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of
 the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard.
 the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard.
 
 
 {{< docs-imagebox img="/img/docs/v50/variables_dashboard.png" >}}
 {{< docs-imagebox img="/img/docs/v50/variables_dashboard.png" >}}

+ 1 - 2
pkg/api/api.go

@@ -73,8 +73,7 @@ func (hs *HTTPServer) registerRoutes() {
 	r.Get("/dashboards/", reqSignedIn, Index)
 	r.Get("/dashboards/", reqSignedIn, Index)
 	r.Get("/dashboards/*", reqSignedIn, Index)
 	r.Get("/dashboards/*", reqSignedIn, Index)
 
 
-	r.Get("/explore/", reqEditorRole, Index)
-	r.Get("/explore/*", reqEditorRole, Index)
+	r.Get("/explore", reqEditorRole, Index)
 
 
 	r.Get("/playlists/", reqSignedIn, Index)
 	r.Get("/playlists/", reqSignedIn, Index)
 	r.Get("/playlists/*", reqSignedIn, Index)
 	r.Get("/playlists/*", reqSignedIn, Index)

+ 1 - 0
pkg/api/playlist.go

@@ -160,6 +160,7 @@ func CreatePlaylist(c *m.ReqContext, cmd m.CreatePlaylistCommand) Response {
 
 
 func UpdatePlaylist(c *m.ReqContext, cmd m.UpdatePlaylistCommand) Response {
 func UpdatePlaylist(c *m.ReqContext, cmd m.UpdatePlaylistCommand) Response {
 	cmd.OrgId = c.OrgId
 	cmd.OrgId = c.OrgId
+	cmd.Id = c.ParamsInt64(":id")
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
 		return Error(500, "Failed to save playlist", err)
 		return Error(500, "Failed to save playlist", err)

+ 12 - 3
pkg/components/imguploader/webdavuploader.go

@@ -9,6 +9,7 @@ import (
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
 	"path"
 	"path"
+	"strings"
 	"time"
 	"time"
 
 
 	"github.com/grafana/grafana/pkg/util"
 	"github.com/grafana/grafana/pkg/util"
@@ -35,6 +36,16 @@ var netClient = &http.Client{
 	Transport: netTransport,
 	Transport: netTransport,
 }
 }
 
 
+func (u *WebdavUploader) PublicURL(filename string) string {
+	if strings.Contains(u.public_url, "${file}") {
+		return strings.Replace(u.public_url, "${file}", filename, -1)
+	} else {
+		publicURL, _ := url.Parse(u.public_url)
+		publicURL.Path = path.Join(publicURL.Path, filename)
+		return publicURL.String()
+	}
+}
+
 func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error) {
 func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error) {
 	url, _ := url.Parse(u.url)
 	url, _ := url.Parse(u.url)
 	filename := util.GetRandomString(20) + ".png"
 	filename := util.GetRandomString(20) + ".png"
@@ -65,9 +76,7 @@ func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error)
 	}
 	}
 
 
 	if u.public_url != "" {
 	if u.public_url != "" {
-		publicURL, _ := url.Parse(u.public_url)
-		publicURL.Path = path.Join(publicURL.Path, filename)
-		return publicURL.String(), nil
+		return u.PublicURL(filename), nil
 	}
 	}
 
 
 	return url.String(), nil
 	return url.String(), nil

+ 13 - 0
pkg/components/imguploader/webdavuploader_test.go

@@ -2,6 +2,7 @@ package imguploader
 
 
 import (
 import (
 	"context"
 	"context"
+	"net/url"
 	"testing"
 	"testing"
 
 
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
@@ -26,3 +27,15 @@ func TestUploadToWebdav(t *testing.T) {
 		So(path, ShouldStartWith, "http://publicurl:8888/webdav/")
 		So(path, ShouldStartWith, "http://publicurl:8888/webdav/")
 	})
 	})
 }
 }
+
+func TestPublicURL(t *testing.T) {
+	Convey("Given a public URL with parameters, and no template", t, func() {
+		webdavUploader, _ := NewWebdavImageUploader("http://localhost:8888/webdav/", "test", "test", "http://cloudycloud.me/s/DOIFDOMV/download?files=")
+		parsed, _ := url.Parse(webdavUploader.PublicURL("fileyfile.png"))
+		So(parsed.Path, ShouldEndWith, "fileyfile.png")
+	})
+	Convey("Given a public URL with parameters, and a template", t, func() {
+		webdavUploader, _ := NewWebdavImageUploader("http://localhost:8888/webdav/", "test", "test", "http://cloudycloud.me/s/DOIFDOMV/download?files=${file}")
+		So(webdavUploader.PublicURL("fileyfile.png"), ShouldEndWith, "fileyfile.png")
+	})
+}

+ 7 - 0
pkg/login/ext_user.go

@@ -72,6 +72,13 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
 		return err
 		return err
 	}
 	}
 
 
+	// Sync isGrafanaAdmin permission
+	if extUser.IsGrafanaAdmin != nil && *extUser.IsGrafanaAdmin != cmd.Result.IsAdmin {
+		if err := bus.Dispatch(&m.UpdateUserPermissionsCommand{UserId: cmd.Result.Id, IsGrafanaAdmin: *extUser.IsGrafanaAdmin}); err != nil {
+			return err
+		}
+	}
+
 	err = bus.Dispatch(&m.SyncTeamsCommand{
 	err = bus.Dispatch(&m.SyncTeamsCommand{
 		User:         cmd.Result,
 		User:         cmd.Result,
 		ExternalUser: extUser,
 		ExternalUser: extUser,

+ 4 - 3
pkg/login/ldap.go

@@ -175,6 +175,7 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
 
 
 		if ldapUser.isMemberOf(group.GroupDN) {
 		if ldapUser.isMemberOf(group.GroupDN) {
 			extUser.OrgRoles[group.OrgId] = group.OrgRole
 			extUser.OrgRoles[group.OrgId] = group.OrgRole
+			extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
 		}
 		}
 	}
 	}
 
 
@@ -190,18 +191,18 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
 	}
 	}
 
 
 	// add/update user in grafana
 	// add/update user in grafana
-	userQuery := &m.UpsertUserCommand{
+	upsertUserCmd := &m.UpsertUserCommand{
 		ReqContext:    ctx,
 		ReqContext:    ctx,
 		ExternalUser:  extUser,
 		ExternalUser:  extUser,
 		SignupAllowed: setting.LdapAllowSignup,
 		SignupAllowed: setting.LdapAllowSignup,
 	}
 	}
 
 
-	err := bus.Dispatch(userQuery)
+	err := bus.Dispatch(upsertUserCmd)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	return userQuery.Result, nil
+	return upsertUserCmd.Result, nil
 }
 }
 
 
 func (a *ldapAuther) serverBind() error {
 func (a *ldapAuther) serverBind() error {

+ 4 - 3
pkg/login/ldap_settings.go

@@ -44,9 +44,10 @@ type LdapAttributeMap struct {
 }
 }
 
 
 type LdapGroupToOrgRole struct {
 type LdapGroupToOrgRole struct {
-	GroupDN string     `toml:"group_dn"`
-	OrgId   int64      `toml:"org_id"`
-	OrgRole m.RoleType `toml:"org_role"`
+	GroupDN        string     `toml:"group_dn"`
+	OrgId          int64      `toml:"org_id"`
+	IsGrafanaAdmin *bool      `toml:"grafana_admin"` // This is a pointer to know if it was set or not (for backwards compatability)
+	OrgRole        m.RoleType `toml:"org_role"`
 }
 }
 
 
 var LdapCfg LdapConfig
 var LdapCfg LdapConfig

+ 42 - 8
pkg/login/ldap_test.go

@@ -98,6 +98,10 @@ func TestLdapAuther(t *testing.T) {
 				So(result.Login, ShouldEqual, "torkelo")
 				So(result.Login, ShouldEqual, "torkelo")
 			})
 			})
 
 
+			Convey("Should set isGrafanaAdmin to false by default", func() {
+				So(result.IsAdmin, ShouldBeFalse)
+			})
+
 		})
 		})
 
 
 	})
 	})
@@ -223,8 +227,32 @@ func TestLdapAuther(t *testing.T) {
 				So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
 				So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN)
 				So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
 				So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1)
 			})
 			})
+
+			Convey("Should not update permissions unless specified", func() {
+				So(err, ShouldBeNil)
+				So(sc.updateUserPermissionsCmd, ShouldBeNil)
+			})
 		})
 		})
 
 
+		ldapAutherScenario("given ldap groups with grafana_admin=true", func(sc *scenarioContext) {
+			trueVal := true
+
+			ldapAuther := NewLdapAuthenticator(&LdapServerConf{
+				LdapGroups: []*LdapGroupToOrgRole{
+					{GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin", IsGrafanaAdmin: &trueVal},
+				},
+			})
+
+			sc.userOrgsQueryReturns([]*m.UserOrgDTO{})
+			_, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{
+				MemberOf: []string{"cn=admins"},
+			})
+
+			Convey("Should create user with admin set to true", func() {
+				So(err, ShouldBeNil)
+				So(sc.updateUserPermissionsCmd.IsGrafanaAdmin, ShouldBeTrue)
+			})
+		})
 	})
 	})
 
 
 	Convey("When calling SyncUser", t, func() {
 	Convey("When calling SyncUser", t, func() {
@@ -332,6 +360,11 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
 			return nil
 			return nil
 		})
 		})
 
 
+		bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.UpdateUserPermissionsCommand) error {
+			sc.updateUserPermissionsCmd = cmd
+			return nil
+		})
+
 		bus.AddHandler("test", func(cmd *m.GetUserByAuthInfoQuery) error {
 		bus.AddHandler("test", func(cmd *m.GetUserByAuthInfoQuery) error {
 			sc.getUserByAuthInfoQuery = cmd
 			sc.getUserByAuthInfoQuery = cmd
 			sc.getUserByAuthInfoQuery.Result = &m.User{Login: cmd.Login}
 			sc.getUserByAuthInfoQuery.Result = &m.User{Login: cmd.Login}
@@ -379,14 +412,15 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
 }
 }
 
 
 type scenarioContext struct {
 type scenarioContext struct {
-	getUserByAuthInfoQuery *m.GetUserByAuthInfoQuery
-	getUserOrgListQuery    *m.GetUserOrgListQuery
-	createUserCmd          *m.CreateUserCommand
-	addOrgUserCmd          *m.AddOrgUserCommand
-	updateOrgUserCmd       *m.UpdateOrgUserCommand
-	removeOrgUserCmd       *m.RemoveOrgUserCommand
-	updateUserCmd          *m.UpdateUserCommand
-	setUsingOrgCmd         *m.SetUsingOrgCommand
+	getUserByAuthInfoQuery   *m.GetUserByAuthInfoQuery
+	getUserOrgListQuery      *m.GetUserOrgListQuery
+	createUserCmd            *m.CreateUserCommand
+	addOrgUserCmd            *m.AddOrgUserCommand
+	updateOrgUserCmd         *m.UpdateOrgUserCommand
+	removeOrgUserCmd         *m.RemoveOrgUserCommand
+	updateUserCmd            *m.UpdateUserCommand
+	setUsingOrgCmd           *m.SetUsingOrgCommand
+	updateUserPermissionsCmd *m.UpdateUserPermissionsCommand
 }
 }
 
 
 func (sc *scenarioContext) userQueryReturns(user *m.User) {
 func (sc *scenarioContext) userQueryReturns(user *m.User) {

+ 8 - 0
pkg/metrics/metrics.go

@@ -44,6 +44,7 @@ var (
 	M_Alerting_Notification_Sent         *prometheus.CounterVec
 	M_Alerting_Notification_Sent         *prometheus.CounterVec
 	M_Aws_CloudWatch_GetMetricStatistics prometheus.Counter
 	M_Aws_CloudWatch_GetMetricStatistics prometheus.Counter
 	M_Aws_CloudWatch_ListMetrics         prometheus.Counter
 	M_Aws_CloudWatch_ListMetrics         prometheus.Counter
+	M_Aws_CloudWatch_GetMetricData       prometheus.Counter
 	M_DB_DataSource_QueryById            prometheus.Counter
 	M_DB_DataSource_QueryById            prometheus.Counter
 
 
 	// Timers
 	// Timers
@@ -218,6 +219,12 @@ func init() {
 		Namespace: exporterName,
 		Namespace: exporterName,
 	})
 	})
 
 
+	M_Aws_CloudWatch_GetMetricData = prometheus.NewCounter(prometheus.CounterOpts{
+		Name:      "aws_cloudwatch_get_metric_data_total",
+		Help:      "counter for getting metric data time series from aws",
+		Namespace: exporterName,
+	})
+
 	M_DB_DataSource_QueryById = prometheus.NewCounter(prometheus.CounterOpts{
 	M_DB_DataSource_QueryById = prometheus.NewCounter(prometheus.CounterOpts{
 		Name:      "db_datasource_query_by_id_total",
 		Name:      "db_datasource_query_by_id_total",
 		Help:      "counter for getting datasource by id",
 		Help:      "counter for getting datasource by id",
@@ -307,6 +314,7 @@ func initMetricVars() {
 		M_Alerting_Notification_Sent,
 		M_Alerting_Notification_Sent,
 		M_Aws_CloudWatch_GetMetricStatistics,
 		M_Aws_CloudWatch_GetMetricStatistics,
 		M_Aws_CloudWatch_ListMetrics,
 		M_Aws_CloudWatch_ListMetrics,
+		M_Aws_CloudWatch_GetMetricData,
 		M_DB_DataSource_QueryById,
 		M_DB_DataSource_QueryById,
 		M_Alerting_Active_Alerts,
 		M_Alerting_Active_Alerts,
 		M_StatTotal_Dashboards,
 		M_StatTotal_Dashboards,

+ 1 - 1
pkg/models/playlist.go

@@ -63,7 +63,7 @@ type PlaylistDashboards []*PlaylistDashboard
 
 
 type UpdatePlaylistCommand struct {
 type UpdatePlaylistCommand struct {
 	OrgId    int64             `json:"-"`
 	OrgId    int64             `json:"-"`
-	Id       int64             `json:"id" binding:"Required"`
+	Id       int64             `json:"id"`
 	Name     string            `json:"name" binding:"Required"`
 	Name     string            `json:"name" binding:"Required"`
 	Interval string            `json:"interval"`
 	Interval string            `json:"interval"`
 	Items    []PlaylistItemDTO `json:"items"`
 	Items    []PlaylistItemDTO `json:"items"`

+ 9 - 8
pkg/models/user_auth.go

@@ -13,14 +13,15 @@ type UserAuth struct {
 }
 }
 
 
 type ExternalUserInfo struct {
 type ExternalUserInfo struct {
-	AuthModule string
-	AuthId     string
-	UserId     int64
-	Email      string
-	Login      string
-	Name       string
-	Groups     []string
-	OrgRoles   map[int64]RoleType
+	AuthModule     string
+	AuthId         string
+	UserId         int64
+	Email          string
+	Login          string
+	Name           string
+	Groups         []string
+	OrgRoles       map[int64]RoleType
+	IsGrafanaAdmin *bool // This is a pointer to know if we should sync this or not (nil = ignore sync)
 }
 }
 
 
 // ---------------------
 // ---------------------

+ 3 - 0
pkg/plugins/datasource_plugin.go

@@ -17,11 +17,14 @@ import (
 	plugin "github.com/hashicorp/go-plugin"
 	plugin "github.com/hashicorp/go-plugin"
 )
 )
 
 
+// DataSourcePlugin contains all metadata about a datasource plugin
 type DataSourcePlugin struct {
 type DataSourcePlugin struct {
 	FrontendPluginBase
 	FrontendPluginBase
 	Annotations  bool              `json:"annotations"`
 	Annotations  bool              `json:"annotations"`
 	Metrics      bool              `json:"metrics"`
 	Metrics      bool              `json:"metrics"`
 	Alerting     bool              `json:"alerting"`
 	Alerting     bool              `json:"alerting"`
+	Explore      bool              `json:"explore"`
+	Logs         bool              `json:"logs"`
 	QueryOptions map[string]bool   `json:"queryOptions,omitempty"`
 	QueryOptions map[string]bool   `json:"queryOptions,omitempty"`
 	BuiltIn      bool              `json:"builtIn,omitempty"`
 	BuiltIn      bool              `json:"builtIn,omitempty"`
 	Mixed        bool              `json:"mixed,omitempty"`
 	Mixed        bool              `json:"mixed,omitempty"`

+ 1 - 0
pkg/services/sqlstore/alert.go

@@ -73,6 +73,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
 		alert.name,
 		alert.name,
 		alert.state,
 		alert.state,
 		alert.new_state_date,
 		alert.new_state_date,
+		alert.eval_data,
 		alert.eval_date,
 		alert.eval_date,
 		alert.execution_error,
 		alert.execution_error,
 		dashboard.uid as dashboard_uid,
 		dashboard.uid as dashboard_uid,

+ 13 - 2
pkg/services/sqlstore/alert_test.go

@@ -13,7 +13,7 @@ func mockTimeNow() {
 	var timeSeed int64
 	var timeSeed int64
 	timeNow = func() time.Time {
 	timeNow = func() time.Time {
 		fakeNow := time.Unix(timeSeed, 0)
 		fakeNow := time.Unix(timeSeed, 0)
-		timeSeed += 1
+		timeSeed++
 		return fakeNow
 		return fakeNow
 	}
 	}
 }
 }
@@ -30,7 +30,7 @@ func TestAlertingDataAccess(t *testing.T) {
 		InitTestDB(t)
 		InitTestDB(t)
 
 
 		testDash := insertTestDashboard("dashboard with alerts", 1, 0, false, "alert")
 		testDash := insertTestDashboard("dashboard with alerts", 1, 0, false, "alert")
-
+		evalData, _ := simplejson.NewJson([]byte(`{"test": "test"}`))
 		items := []*m.Alert{
 		items := []*m.Alert{
 			{
 			{
 				PanelId:     1,
 				PanelId:     1,
@@ -40,6 +40,7 @@ func TestAlertingDataAccess(t *testing.T) {
 				Message:     "Alerting message",
 				Message:     "Alerting message",
 				Settings:    simplejson.New(),
 				Settings:    simplejson.New(),
 				Frequency:   1,
 				Frequency:   1,
+				EvalData:    evalData,
 			},
 			},
 		}
 		}
 
 
@@ -104,8 +105,18 @@ func TestAlertingDataAccess(t *testing.T) {
 
 
 			alert := alertQuery.Result[0]
 			alert := alertQuery.Result[0]
 			So(err2, ShouldBeNil)
 			So(err2, ShouldBeNil)
+			So(alert.Id, ShouldBeGreaterThan, 0)
+			So(alert.DashboardId, ShouldEqual, testDash.Id)
+			So(alert.PanelId, ShouldEqual, 1)
 			So(alert.Name, ShouldEqual, "Alerting title")
 			So(alert.Name, ShouldEqual, "Alerting title")
 			So(alert.State, ShouldEqual, "pending")
 			So(alert.State, ShouldEqual, "pending")
+			So(alert.NewStateDate, ShouldNotBeNil)
+			So(alert.EvalData, ShouldNotBeNil)
+			So(alert.EvalData.Get("test").MustString(), ShouldEqual, "test")
+			So(alert.EvalDate, ShouldNotBeNil)
+			So(alert.ExecutionError, ShouldEqual, "")
+			So(alert.DashboardUid, ShouldNotBeNil)
+			So(alert.DashboardSlug, ShouldEqual, "dashboard-with-alerts")
 		})
 		})
 
 
 		Convey("Viewer cannot read alerts", func() {
 		Convey("Viewer cannot read alerts", func() {

+ 1 - 0
pkg/services/sqlstore/dashboard_test.go

@@ -387,6 +387,7 @@ func insertTestDashboardForPlugin(title string, orgId int64, folderId int64, isF
 
 
 func createUser(name string, role string, isAdmin bool) m.User {
 func createUser(name string, role string, isAdmin bool) m.User {
 	setting.AutoAssignOrg = true
 	setting.AutoAssignOrg = true
+	setting.AutoAssignOrgId = 1
 	setting.AutoAssignOrgRole = role
 	setting.AutoAssignOrgRole = role
 
 
 	currentUserCmd := m.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: isAdmin}
 	currentUserCmd := m.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: isAdmin}

+ 1 - 0
pkg/services/sqlstore/org_test.go

@@ -17,6 +17,7 @@ func TestAccountDataAccess(t *testing.T) {
 
 
 		Convey("Given single org mode", func() {
 		Convey("Given single org mode", func() {
 			setting.AutoAssignOrg = true
 			setting.AutoAssignOrg = true
+			setting.AutoAssignOrgId = 1
 			setting.AutoAssignOrgRole = "Viewer"
 			setting.AutoAssignOrgRole = "Viewer"
 
 
 			Convey("Users should be added to default organization", func() {
 			Convey("Users should be added to default organization", func() {

+ 11 - 4
pkg/services/sqlstore/user.go

@@ -42,16 +42,23 @@ func getOrgIdForNewUser(cmd *m.CreateUserCommand, sess *DBSession) (int64, error
 	var org m.Org
 	var org m.Org
 
 
 	if setting.AutoAssignOrg {
 	if setting.AutoAssignOrg {
-		// right now auto assign to org with id 1
-		has, err := sess.Where("id=?", 1).Get(&org)
+		has, err := sess.Where("id=?", setting.AutoAssignOrgId).Get(&org)
 		if err != nil {
 		if err != nil {
 			return 0, err
 			return 0, err
 		}
 		}
 		if has {
 		if has {
 			return org.Id, nil
 			return org.Id, nil
+		} else {
+			if setting.AutoAssignOrgId == 1 {
+				org.Name = "Main Org."
+				org.Id = int64(setting.AutoAssignOrgId)
+			} else {
+				sqlog.Info("Could not create user: organization id %v does not exist",
+					setting.AutoAssignOrgId)
+				return 0, fmt.Errorf("Could not create user: organization id %v does not exist",
+					setting.AutoAssignOrgId)
+			}
 		}
 		}
-		org.Name = "Main Org."
-		org.Id = 1
 	} else {
 	} else {
 		org.Name = cmd.OrgName
 		org.Name = cmd.OrgName
 		if len(org.Name) == 0 {
 		if len(org.Name) == 0 {

+ 2 - 0
pkg/setting/setting.go

@@ -100,6 +100,7 @@ var (
 	AllowUserSignUp         bool
 	AllowUserSignUp         bool
 	AllowUserOrgCreate      bool
 	AllowUserOrgCreate      bool
 	AutoAssignOrg           bool
 	AutoAssignOrg           bool
+	AutoAssignOrgId         int
 	AutoAssignOrgRole       string
 	AutoAssignOrgRole       string
 	VerifyEmailEnabled      bool
 	VerifyEmailEnabled      bool
 	LoginHint               string
 	LoginHint               string
@@ -592,6 +593,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
 	AllowUserSignUp = users.Key("allow_sign_up").MustBool(true)
 	AllowUserSignUp = users.Key("allow_sign_up").MustBool(true)
 	AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true)
 	AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true)
 	AutoAssignOrg = users.Key("auto_assign_org").MustBool(true)
 	AutoAssignOrg = users.Key("auto_assign_org").MustBool(true)
+	AutoAssignOrgId = users.Key("auto_assign_org_id").MustInt(1)
 	AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Viewer"})
 	AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Viewer"})
 	VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
 	VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
 	LoginHint = users.Key("login_hint").String()
 	LoginHint = users.Key("login_hint").String()

+ 191 - 26
pkg/tsdb/cloudwatch/cloudwatch.go

@@ -14,6 +14,7 @@ import (
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/tsdb"
 	"github.com/grafana/grafana/pkg/tsdb"
+	"golang.org/x/sync/errgroup"
 
 
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/request"
 	"github.com/aws/aws-sdk-go/aws/request"
@@ -88,48 +89,67 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
 		Results: make(map[string]*tsdb.QueryResult),
 		Results: make(map[string]*tsdb.QueryResult),
 	}
 	}
 
 
-	errCh := make(chan error, 1)
-	resCh := make(chan *tsdb.QueryResult, 1)
+	eg, ectx := errgroup.WithContext(ctx)
 
 
-	currentlyExecuting := 0
+	getMetricDataQueries := make(map[string]map[string]*CloudWatchQuery)
 	for i, model := range queryContext.Queries {
 	for i, model := range queryContext.Queries {
 		queryType := model.Model.Get("type").MustString()
 		queryType := model.Model.Get("type").MustString()
 		if queryType != "timeSeriesQuery" && queryType != "" {
 		if queryType != "timeSeriesQuery" && queryType != "" {
 			continue
 			continue
 		}
 		}
-		currentlyExecuting++
-		go func(refId string, index int) {
-			queryRes, err := e.executeQuery(ctx, queryContext.Queries[index].Model, queryContext)
-			currentlyExecuting--
+
+		query, err := parseQuery(queryContext.Queries[i].Model)
+		if err != nil {
+			return nil, err
+		}
+		query.RefId = queryContext.Queries[i].RefId
+
+		if query.Id != "" {
+			if _, ok := getMetricDataQueries[query.Region]; !ok {
+				getMetricDataQueries[query.Region] = make(map[string]*CloudWatchQuery)
+			}
+			getMetricDataQueries[query.Region][query.Id] = query
+			continue
+		}
+
+		if query.Id == "" && query.Expression != "" {
+			return nil, fmt.Errorf("Invalid query: id should be set if using expression")
+		}
+
+		eg.Go(func() error {
+			queryRes, err := e.executeQuery(ectx, query, queryContext)
 			if err != nil {
 			if err != nil {
-				errCh <- err
-			} else {
-				queryRes.RefId = refId
-				resCh <- queryRes
+				return err
 			}
 			}
-		}(model.RefId, i)
+			result.Results[queryRes.RefId] = queryRes
+			return nil
+		})
 	}
 	}
 
 
-	for currentlyExecuting != 0 {
-		select {
-		case res := <-resCh:
-			result.Results[res.RefId] = res
-		case err := <-errCh:
-			return result, err
-		case <-ctx.Done():
-			return result, ctx.Err()
+	if len(getMetricDataQueries) > 0 {
+		for region, getMetricDataQuery := range getMetricDataQueries {
+			q := getMetricDataQuery
+			eg.Go(func() error {
+				queryResponses, err := e.executeGetMetricDataQuery(ectx, region, q, queryContext)
+				if err != nil {
+					return err
+				}
+				for _, queryRes := range queryResponses {
+					result.Results[queryRes.RefId] = queryRes
+				}
+				return nil
+			})
 		}
 		}
 	}
 	}
 
 
-	return result, nil
-}
-
-func (e *CloudWatchExecutor) executeQuery(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) (*tsdb.QueryResult, error) {
-	query, err := parseQuery(parameters)
-	if err != nil {
+	if err := eg.Wait(); err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
+	return result, nil
+}
+
+func (e *CloudWatchExecutor) executeQuery(ctx context.Context, query *CloudWatchQuery, queryContext *tsdb.TsdbQuery) (*tsdb.QueryResult, error) {
 	client, err := e.getClient(query.Region)
 	client, err := e.getClient(query.Region)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -201,6 +221,139 @@ func (e *CloudWatchExecutor) executeQuery(ctx context.Context, parameters *simpl
 	return queryRes, nil
 	return queryRes, nil
 }
 }
 
 
+func (e *CloudWatchExecutor) executeGetMetricDataQuery(ctx context.Context, region string, queries map[string]*CloudWatchQuery, queryContext *tsdb.TsdbQuery) ([]*tsdb.QueryResult, error) {
+	queryResponses := make([]*tsdb.QueryResult, 0)
+
+	// validate query
+	for _, query := range queries {
+		if !(len(query.Statistics) == 1 && len(query.ExtendedStatistics) == 0) &&
+			!(len(query.Statistics) == 0 && len(query.ExtendedStatistics) == 1) {
+			return queryResponses, errors.New("Statistics count should be 1")
+		}
+	}
+
+	client, err := e.getClient(region)
+	if err != nil {
+		return queryResponses, err
+	}
+
+	startTime, err := queryContext.TimeRange.ParseFrom()
+	if err != nil {
+		return queryResponses, err
+	}
+
+	endTime, err := queryContext.TimeRange.ParseTo()
+	if err != nil {
+		return queryResponses, err
+	}
+
+	params := &cloudwatch.GetMetricDataInput{
+		StartTime: aws.Time(startTime),
+		EndTime:   aws.Time(endTime),
+		ScanBy:    aws.String("TimestampAscending"),
+	}
+	for _, query := range queries {
+		// 1 minutes resolutin metrics is stored for 15 days, 15 * 24 * 60 = 21600
+		if query.HighResolution && (((endTime.Unix() - startTime.Unix()) / int64(query.Period)) > 21600) {
+			return nil, errors.New("too long query period")
+		}
+
+		mdq := &cloudwatch.MetricDataQuery{
+			Id:         aws.String(query.Id),
+			ReturnData: aws.Bool(query.ReturnData),
+		}
+		if query.Expression != "" {
+			mdq.Expression = aws.String(query.Expression)
+		} else {
+			mdq.MetricStat = &cloudwatch.MetricStat{
+				Metric: &cloudwatch.Metric{
+					Namespace:  aws.String(query.Namespace),
+					MetricName: aws.String(query.MetricName),
+				},
+				Period: aws.Int64(int64(query.Period)),
+			}
+			for _, d := range query.Dimensions {
+				mdq.MetricStat.Metric.Dimensions = append(mdq.MetricStat.Metric.Dimensions,
+					&cloudwatch.Dimension{
+						Name:  d.Name,
+						Value: d.Value,
+					})
+			}
+			if len(query.Statistics) == 1 {
+				mdq.MetricStat.Stat = query.Statistics[0]
+			} else {
+				mdq.MetricStat.Stat = query.ExtendedStatistics[0]
+			}
+		}
+		params.MetricDataQueries = append(params.MetricDataQueries, mdq)
+	}
+
+	nextToken := ""
+	mdr := make(map[string]*cloudwatch.MetricDataResult)
+	for {
+		if nextToken != "" {
+			params.NextToken = aws.String(nextToken)
+		}
+		resp, err := client.GetMetricDataWithContext(ctx, params)
+		if err != nil {
+			return queryResponses, err
+		}
+		metrics.M_Aws_CloudWatch_GetMetricData.Add(float64(len(params.MetricDataQueries)))
+
+		for _, r := range resp.MetricDataResults {
+			if _, ok := mdr[*r.Id]; !ok {
+				mdr[*r.Id] = r
+			} else {
+				mdr[*r.Id].Timestamps = append(mdr[*r.Id].Timestamps, r.Timestamps...)
+				mdr[*r.Id].Values = append(mdr[*r.Id].Values, r.Values...)
+			}
+		}
+
+		if resp.NextToken == nil || *resp.NextToken == "" {
+			break
+		}
+		nextToken = *resp.NextToken
+	}
+
+	for i, r := range mdr {
+		if *r.StatusCode != "Complete" {
+			return queryResponses, fmt.Errorf("Part of query is failed: %s", *r.StatusCode)
+		}
+
+		queryRes := tsdb.NewQueryResult()
+		queryRes.RefId = queries[i].RefId
+		query := queries[*r.Id]
+
+		series := tsdb.TimeSeries{
+			Tags:   map[string]string{},
+			Points: make([]tsdb.TimePoint, 0),
+		}
+		for _, d := range query.Dimensions {
+			series.Tags[*d.Name] = *d.Value
+		}
+		s := ""
+		if len(query.Statistics) == 1 {
+			s = *query.Statistics[0]
+		} else {
+			s = *query.ExtendedStatistics[0]
+		}
+		series.Name = formatAlias(query, s, series.Tags)
+
+		for j, t := range r.Timestamps {
+			expectedTimestamp := r.Timestamps[j].Add(time.Duration(query.Period) * time.Second)
+			if j > 0 && expectedTimestamp.Before(*t) {
+				series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), float64(expectedTimestamp.Unix()*1000)))
+			}
+			series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(*r.Values[j]), float64((*t).Unix())*1000))
+		}
+
+		queryRes.Series = append(queryRes.Series, &series)
+		queryResponses = append(queryResponses, queryRes)
+	}
+
+	return queryResponses, nil
+}
+
 func parseDimensions(model *simplejson.Json) ([]*cloudwatch.Dimension, error) {
 func parseDimensions(model *simplejson.Json) ([]*cloudwatch.Dimension, error) {
 	var result []*cloudwatch.Dimension
 	var result []*cloudwatch.Dimension
 
 
@@ -257,6 +410,9 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
+	id := model.Get("id").MustString("")
+	expression := model.Get("expression").MustString("")
+
 	dimensions, err := parseDimensions(model)
 	dimensions, err := parseDimensions(model)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -295,6 +451,7 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) {
 		alias = "{{metric}}_{{stat}}"
 		alias = "{{metric}}_{{stat}}"
 	}
 	}
 
 
+	returnData := model.Get("returnData").MustBool(false)
 	highResolution := model.Get("highResolution").MustBool(false)
 	highResolution := model.Get("highResolution").MustBool(false)
 
 
 	return &CloudWatchQuery{
 	return &CloudWatchQuery{
@@ -306,11 +463,18 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) {
 		ExtendedStatistics: aws.StringSlice(extendedStatistics),
 		ExtendedStatistics: aws.StringSlice(extendedStatistics),
 		Period:             period,
 		Period:             period,
 		Alias:              alias,
 		Alias:              alias,
+		Id:                 id,
+		Expression:         expression,
+		ReturnData:         returnData,
 		HighResolution:     highResolution,
 		HighResolution:     highResolution,
 	}, nil
 	}, nil
 }
 }
 
 
 func formatAlias(query *CloudWatchQuery, stat string, dimensions map[string]string) string {
 func formatAlias(query *CloudWatchQuery, stat string, dimensions map[string]string) string {
+	if len(query.Id) > 0 && len(query.Expression) > 0 {
+		return query.Id
+	}
+
 	data := map[string]string{}
 	data := map[string]string{}
 	data["region"] = query.Region
 	data["region"] = query.Region
 	data["namespace"] = query.Namespace
 	data["namespace"] = query.Namespace
@@ -338,6 +502,7 @@ func formatAlias(query *CloudWatchQuery, stat string, dimensions map[string]stri
 func parseResponse(resp *cloudwatch.GetMetricStatisticsOutput, query *CloudWatchQuery) (*tsdb.QueryResult, error) {
 func parseResponse(resp *cloudwatch.GetMetricStatisticsOutput, query *CloudWatchQuery) (*tsdb.QueryResult, error) {
 	queryRes := tsdb.NewQueryResult()
 	queryRes := tsdb.NewQueryResult()
 
 
+	queryRes.RefId = query.RefId
 	var value float64
 	var value float64
 	for _, s := range append(query.Statistics, query.ExtendedStatistics...) {
 	for _, s := range append(query.Statistics, query.ExtendedStatistics...) {
 		series := tsdb.TimeSeries{
 		series := tsdb.TimeSeries{

+ 4 - 0
pkg/tsdb/cloudwatch/types.go

@@ -5,6 +5,7 @@ import (
 )
 )
 
 
 type CloudWatchQuery struct {
 type CloudWatchQuery struct {
+	RefId              string
 	Region             string
 	Region             string
 	Namespace          string
 	Namespace          string
 	MetricName         string
 	MetricName         string
@@ -13,5 +14,8 @@ type CloudWatchQuery struct {
 	ExtendedStatistics []*string
 	ExtendedStatistics []*string
 	Period             int
 	Period             int
 	Alias              string
 	Alias              string
+	Id                 string
+	Expression         string
+	ReturnData         bool
 	HighResolution     bool
 	HighResolution     bool
 }
 }

+ 1 - 0
pkg/tsdb/sql_engine.go

@@ -68,6 +68,7 @@ func (e *DefaultSqlEngine) InitEngine(driverName string, dsInfo *models.DataSour
 	engine.SetMaxOpenConns(10)
 	engine.SetMaxOpenConns(10)
 	engine.SetMaxIdleConns(10)
 	engine.SetMaxIdleConns(10)
 
 
+	engineCache.versions[dsInfo.Id] = dsInfo.Version
 	engineCache.cache[dsInfo.Id] = engine
 	engineCache.cache[dsInfo.Id] = engine
 	e.XormEngine = engine
 	e.XormEngine = engine
 
 

+ 1 - 1
pkg/tsdb/testdata/testdata.go

@@ -21,7 +21,7 @@ func NewTestDataExecutor(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, err
 }
 }
 
 
 func init() {
 func init() {
-	tsdb.RegisterTsdbQueryEndpoint("grafana-testdata-datasource", NewTestDataExecutor)
+	tsdb.RegisterTsdbQueryEndpoint("testdata", NewTestDataExecutor)
 }
 }
 
 
 func (e *TestDataExecutor) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) {
 func (e *TestDataExecutor) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) {

+ 209 - 53
public/app/containers/Explore/Explore.tsx

@@ -1,16 +1,20 @@
 import React from 'react';
 import React from 'react';
 import { hot } from 'react-hot-loader';
 import { hot } from 'react-hot-loader';
+import Select from 'react-select';
+
+import kbn from 'app/core/utils/kbn';
 import colors from 'app/core/utils/colors';
 import colors from 'app/core/utils/colors';
 import TimeSeries from 'app/core/time_series2';
 import TimeSeries from 'app/core/time_series2';
+import { decodePathComponent } from 'app/core/utils/location_util';
+import { parse as parseDate } from 'app/core/utils/datemath';
 
 
 import ElapsedTime from './ElapsedTime';
 import ElapsedTime from './ElapsedTime';
 import QueryRows from './QueryRows';
 import QueryRows from './QueryRows';
 import Graph from './Graph';
 import Graph from './Graph';
+import Logs from './Logs';
 import Table from './Table';
 import Table from './Table';
 import TimePicker, { DEFAULT_RANGE } from './TimePicker';
 import TimePicker, { DEFAULT_RANGE } from './TimePicker';
-import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
-import { buildQueryOptions, ensureQueries, generateQueryKey, hasQuery } from './utils/query';
-import { decodePathComponent } from 'app/core/utils/location_util';
+import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
 
 
 function makeTimeSeriesList(dataList, options) {
 function makeTimeSeriesList(dataList, options) {
   return dataList.map((seriesData, index) => {
   return dataList.map((seriesData, index) => {
@@ -30,74 +34,136 @@ function makeTimeSeriesList(dataList, options) {
   });
   });
 }
 }
 
 
-function parseInitialState(initial) {
-  try {
-    const parsed = JSON.parse(decodePathComponent(initial));
-    return {
-      queries: parsed.queries.map(q => q.query),
-      range: parsed.range,
-    };
-  } catch (e) {
-    console.error(e);
-    return { queries: [], range: DEFAULT_RANGE };
+function parseInitialState(initial: string | undefined) {
+  if (initial) {
+    try {
+      const parsed = JSON.parse(decodePathComponent(initial));
+      return {
+        datasource: parsed.datasource,
+        queries: parsed.queries.map(q => q.query),
+        range: parsed.range,
+      };
+    } catch (e) {
+      console.error(e);
+    }
   }
   }
+  return { datasource: null, queries: [], range: DEFAULT_RANGE };
 }
 }
 
 
 interface IExploreState {
 interface IExploreState {
   datasource: any;
   datasource: any;
   datasourceError: any;
   datasourceError: any;
-  datasourceLoading: any;
+  datasourceLoading: boolean | null;
+  datasourceMissing: boolean;
   graphResult: any;
   graphResult: any;
+  initialDatasource?: string;
   latency: number;
   latency: number;
   loading: any;
   loading: any;
+  logsResult: any;
   queries: any;
   queries: any;
   queryError: any;
   queryError: any;
   range: any;
   range: any;
   requestOptions: any;
   requestOptions: any;
   showingGraph: boolean;
   showingGraph: boolean;
+  showingLogs: boolean;
   showingTable: boolean;
   showingTable: boolean;
+  supportsGraph: boolean | null;
+  supportsLogs: boolean | null;
+  supportsTable: boolean | null;
   tableResult: any;
   tableResult: any;
 }
 }
 
 
-// @observer
 export class Explore extends React.Component<any, IExploreState> {
 export class Explore extends React.Component<any, IExploreState> {
-  datasourceSrv: DatasourceSrv;
+  el: any;
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
-    const { range, queries } = parseInitialState(props.routeParams.initial);
+    const { datasource, queries, range } = parseInitialState(props.routeParams.state);
     this.state = {
     this.state = {
       datasource: null,
       datasource: null,
       datasourceError: null,
       datasourceError: null,
-      datasourceLoading: true,
+      datasourceLoading: null,
+      datasourceMissing: false,
       graphResult: null,
       graphResult: null,
+      initialDatasource: datasource,
       latency: 0,
       latency: 0,
       loading: false,
       loading: false,
+      logsResult: null,
       queries: ensureQueries(queries),
       queries: ensureQueries(queries),
       queryError: null,
       queryError: null,
       range: range || { ...DEFAULT_RANGE },
       range: range || { ...DEFAULT_RANGE },
       requestOptions: null,
       requestOptions: null,
       showingGraph: true,
       showingGraph: true,
+      showingLogs: true,
       showingTable: true,
       showingTable: true,
+      supportsGraph: null,
+      supportsLogs: null,
+      supportsTable: null,
       tableResult: null,
       tableResult: null,
       ...props.initialState,
       ...props.initialState,
     };
     };
   }
   }
 
 
   async componentDidMount() {
   async componentDidMount() {
-    const datasource = await this.props.datasourceSrv.get();
-    const testResult = await datasource.testDatasource();
-    if (testResult.status === 'success') {
-      this.setState({ datasource, datasourceError: null, datasourceLoading: false }, () => this.handleSubmit());
+    const { datasourceSrv } = this.props;
+    const { initialDatasource } = this.state;
+    if (!datasourceSrv) {
+      throw new Error('No datasource service passed as props.');
+    }
+    const datasources = datasourceSrv.getExploreSources();
+    if (datasources.length > 0) {
+      this.setState({ datasourceLoading: true });
+      // Priority: datasource in url, default datasource, first explore datasource
+      let datasource;
+      if (initialDatasource) {
+        datasource = await datasourceSrv.get(initialDatasource);
+      } else {
+        datasource = await datasourceSrv.get();
+      }
+      if (!datasource.meta.explore) {
+        datasource = await datasourceSrv.get(datasources[0].name);
+      }
+      this.setDatasource(datasource);
     } else {
     } else {
-      this.setState({ datasource: null, datasourceError: testResult.message, datasourceLoading: false });
+      this.setState({ datasourceMissing: true });
     }
     }
   }
   }
 
 
   componentDidCatch(error) {
   componentDidCatch(error) {
+    this.setState({ datasourceError: error });
     console.error(error);
     console.error(error);
   }
   }
 
 
+  async setDatasource(datasource) {
+    const supportsGraph = datasource.meta.metrics;
+    const supportsLogs = datasource.meta.logs;
+    const supportsTable = datasource.meta.metrics;
+    let datasourceError = null;
+
+    try {
+      const testResult = await datasource.testDatasource();
+      datasourceError = testResult.status === 'success' ? null : testResult.message;
+    } catch (error) {
+      datasourceError = (error && error.statusText) || error;
+    }
+
+    this.setState(
+      {
+        datasource,
+        datasourceError,
+        supportsGraph,
+        supportsLogs,
+        supportsTable,
+        datasourceLoading: false,
+      },
+      () => datasourceError === null && this.handleSubmit()
+    );
+  }
+
+  getRef = el => {
+    this.el = el;
+  };
+
   handleAddQueryRow = index => {
   handleAddQueryRow = index => {
     const { queries } = this.state;
     const { queries } = this.state;
     const nextQueries = [
     const nextQueries = [
@@ -108,6 +174,19 @@ export class Explore extends React.Component<any, IExploreState> {
     this.setState({ queries: nextQueries });
     this.setState({ queries: nextQueries });
   };
   };
 
 
+  handleChangeDatasource = async option => {
+    this.setState({
+      datasource: null,
+      datasourceError: null,
+      datasourceLoading: true,
+      graphResult: null,
+      logsResult: null,
+      tableResult: null,
+    });
+    const datasource = await this.props.datasourceSrv.get(option.value);
+    this.setDatasource(datasource);
+  };
+
   handleChangeQuery = (query, index) => {
   handleChangeQuery = (query, index) => {
     const { queries } = this.state;
     const { queries } = this.state;
     const nextQuery = {
     const nextQuery = {
@@ -138,6 +217,10 @@ export class Explore extends React.Component<any, IExploreState> {
     this.setState(state => ({ showingGraph: !state.showingGraph }));
     this.setState(state => ({ showingGraph: !state.showingGraph }));
   };
   };
 
 
+  handleClickLogsButton = () => {
+    this.setState(state => ({ showingLogs: !state.showingLogs }));
+  };
+
   handleClickSplit = () => {
   handleClickSplit = () => {
     const { onChangeSplit } = this.props;
     const { onChangeSplit } = this.props;
     if (onChangeSplit) {
     if (onChangeSplit) {
@@ -159,29 +242,45 @@ export class Explore extends React.Component<any, IExploreState> {
   };
   };
 
 
   handleSubmit = () => {
   handleSubmit = () => {
-    const { showingGraph, showingTable } = this.state;
-    if (showingTable) {
+    const { showingLogs, showingGraph, showingTable, supportsGraph, supportsLogs, supportsTable } = this.state;
+    if (showingTable && supportsTable) {
       this.runTableQuery();
       this.runTableQuery();
     }
     }
-    if (showingGraph) {
+    if (showingGraph && supportsGraph) {
       this.runGraphQuery();
       this.runGraphQuery();
     }
     }
+    if (showingLogs && supportsLogs) {
+      this.runLogsQuery();
+    }
   };
   };
 
 
-  async runGraphQuery() {
+  buildQueryOptions(targetOptions: { format: string; instant?: boolean }) {
     const { datasource, queries, range } = this.state;
     const { datasource, queries, range } = this.state;
+    const resolution = this.el.offsetWidth;
+    const absoluteRange = {
+      from: parseDate(range.from, false),
+      to: parseDate(range.to, true),
+    };
+    const { interval } = kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
+    const targets = queries.map(q => ({
+      ...targetOptions,
+      expr: q.query,
+    }));
+    return {
+      interval,
+      range,
+      targets,
+    };
+  }
+
+  async runGraphQuery() {
+    const { datasource, queries } = this.state;
     if (!hasQuery(queries)) {
     if (!hasQuery(queries)) {
       return;
       return;
     }
     }
     this.setState({ latency: 0, loading: true, graphResult: null, queryError: null });
     this.setState({ latency: 0, loading: true, graphResult: null, queryError: null });
     const now = Date.now();
     const now = Date.now();
-    const options = buildQueryOptions({
-      format: 'time_series',
-      interval: datasource.interval,
-      instant: false,
-      range,
-      queries: queries.map(q => q.query),
-    });
+    const options = this.buildQueryOptions({ format: 'time_series', instant: false });
     try {
     try {
       const res = await datasource.query(options);
       const res = await datasource.query(options);
       const result = makeTimeSeriesList(res.data, options);
       const result = makeTimeSeriesList(res.data, options);
@@ -195,18 +294,15 @@ export class Explore extends React.Component<any, IExploreState> {
   }
   }
 
 
   async runTableQuery() {
   async runTableQuery() {
-    const { datasource, queries, range } = this.state;
+    const { datasource, queries } = this.state;
     if (!hasQuery(queries)) {
     if (!hasQuery(queries)) {
       return;
       return;
     }
     }
     this.setState({ latency: 0, loading: true, queryError: null, tableResult: null });
     this.setState({ latency: 0, loading: true, queryError: null, tableResult: null });
     const now = Date.now();
     const now = Date.now();
-    const options = buildQueryOptions({
+    const options = this.buildQueryOptions({
       format: 'table',
       format: 'table',
-      interval: datasource.interval,
       instant: true,
       instant: true,
-      range,
-      queries: queries.map(q => q.query),
     });
     });
     try {
     try {
       const res = await datasource.query(options);
       const res = await datasource.query(options);
@@ -220,35 +316,71 @@ export class Explore extends React.Component<any, IExploreState> {
     }
     }
   }
   }
 
 
+  async runLogsQuery() {
+    const { datasource, queries } = this.state;
+    if (!hasQuery(queries)) {
+      return;
+    }
+    this.setState({ latency: 0, loading: true, queryError: null, logsResult: null });
+    const now = Date.now();
+    const options = this.buildQueryOptions({
+      format: 'logs',
+    });
+
+    try {
+      const res = await datasource.query(options);
+      const logsData = res.data;
+      const latency = Date.now() - now;
+      this.setState({ latency, loading: false, logsResult: logsData, requestOptions: options });
+    } catch (response) {
+      console.error(response);
+      const queryError = response.data ? response.data.error : response;
+      this.setState({ loading: false, queryError });
+    }
+  }
+
   request = url => {
   request = url => {
     const { datasource } = this.state;
     const { datasource } = this.state;
     return datasource.metadataRequest(url);
     return datasource.metadataRequest(url);
   };
   };
 
 
   render() {
   render() {
-    const { position, split } = this.props;
+    const { datasourceSrv, position, split } = this.props;
     const {
     const {
       datasource,
       datasource,
       datasourceError,
       datasourceError,
       datasourceLoading,
       datasourceLoading,
+      datasourceMissing,
       graphResult,
       graphResult,
       latency,
       latency,
       loading,
       loading,
+      logsResult,
       queries,
       queries,
       queryError,
       queryError,
       range,
       range,
       requestOptions,
       requestOptions,
       showingGraph,
       showingGraph,
+      showingLogs,
       showingTable,
       showingTable,
+      supportsGraph,
+      supportsLogs,
+      supportsTable,
       tableResult,
       tableResult,
     } = this.state;
     } = this.state;
     const showingBoth = showingGraph && showingTable;
     const showingBoth = showingGraph && showingTable;
     const graphHeight = showingBoth ? '200px' : '400px';
     const graphHeight = showingBoth ? '200px' : '400px';
     const graphButtonActive = showingBoth || showingGraph ? 'active' : '';
     const graphButtonActive = showingBoth || showingGraph ? 'active' : '';
+    const logsButtonActive = showingLogs ? 'active' : '';
     const tableButtonActive = showingBoth || showingTable ? 'active' : '';
     const tableButtonActive = showingBoth || showingTable ? 'active' : '';
     const exploreClass = split ? 'explore explore-split' : 'explore';
     const exploreClass = split ? 'explore explore-split' : 'explore';
+    const datasources = datasourceSrv.getExploreSources().map(ds => ({
+      value: ds.name,
+      label: ds.name,
+    }));
+    const selectedDatasource = datasource ? datasource.name : undefined;
+
     return (
     return (
-      <div className={exploreClass}>
+      <div className={exploreClass} ref={this.getRef}>
         <div className="navbar">
         <div className="navbar">
           {position === 'left' ? (
           {position === 'left' ? (
             <div>
             <div>
@@ -264,6 +396,18 @@ export class Explore extends React.Component<any, IExploreState> {
               </button>
               </button>
             </div>
             </div>
           )}
           )}
+          {!datasourceMissing ? (
+            <div className="navbar-buttons">
+              <Select
+                className="datasource-picker"
+                clearable={false}
+                onChange={this.handleChangeDatasource}
+                options={datasources}
+                placeholder="Loading datasources..."
+                value={selectedDatasource}
+              />
+            </div>
+          ) : null}
           <div className="navbar__spacer" />
           <div className="navbar__spacer" />
           {position === 'left' && !split ? (
           {position === 'left' && !split ? (
             <div className="navbar-buttons">
             <div className="navbar-buttons">
@@ -273,12 +417,21 @@ export class Explore extends React.Component<any, IExploreState> {
             </div>
             </div>
           ) : null}
           ) : null}
           <div className="navbar-buttons">
           <div className="navbar-buttons">
-            <button className={`btn navbar-button ${graphButtonActive}`} onClick={this.handleClickGraphButton}>
-              Graph
-            </button>
-            <button className={`btn navbar-button ${tableButtonActive}`} onClick={this.handleClickTableButton}>
-              Table
-            </button>
+            {supportsGraph ? (
+              <button className={`btn navbar-button ${graphButtonActive}`} onClick={this.handleClickGraphButton}>
+                Graph
+              </button>
+            ) : null}
+            {supportsTable ? (
+              <button className={`btn navbar-button ${tableButtonActive}`} onClick={this.handleClickTableButton}>
+                Table
+              </button>
+            ) : null}
+            {supportsLogs ? (
+              <button className={`btn navbar-button ${logsButtonActive}`} onClick={this.handleClickLogsButton}>
+                Logs
+              </button>
+            ) : null}
           </div>
           </div>
           <TimePicker range={range} onChangeTime={this.handleChangeTime} />
           <TimePicker range={range} onChangeTime={this.handleChangeTime} />
           <div className="navbar-buttons relative">
           <div className="navbar-buttons relative">
@@ -291,13 +444,15 @@ export class Explore extends React.Component<any, IExploreState> {
 
 
         {datasourceLoading ? <div className="explore-container">Loading datasource...</div> : null}
         {datasourceLoading ? <div className="explore-container">Loading datasource...</div> : null}
 
 
+        {datasourceMissing ? (
+          <div className="explore-container">Please add a datasource that supports Explore (e.g., Prometheus).</div>
+        ) : null}
+
         {datasourceError ? (
         {datasourceError ? (
-          <div className="explore-container" title={datasourceError}>
-            Error connecting to datasource.
-          </div>
+          <div className="explore-container">Error connecting to datasource. [{datasourceError}]</div>
         ) : null}
         ) : null}
 
 
-        {datasource ? (
+        {datasource && !datasourceError ? (
           <div className="explore-container">
           <div className="explore-container">
             <QueryRows
             <QueryRows
               queries={queries}
               queries={queries}
@@ -309,7 +464,7 @@ export class Explore extends React.Component<any, IExploreState> {
             />
             />
             {queryError ? <div className="text-warning m-a-2">{queryError}</div> : null}
             {queryError ? <div className="text-warning m-a-2">{queryError}</div> : null}
             <main className="m-t-2">
             <main className="m-t-2">
-              {showingGraph ? (
+              {supportsGraph && showingGraph ? (
                 <Graph
                 <Graph
                   data={graphResult}
                   data={graphResult}
                   id={`explore-graph-${position}`}
                   id={`explore-graph-${position}`}
@@ -318,7 +473,8 @@ export class Explore extends React.Component<any, IExploreState> {
                   split={split}
                   split={split}
                 />
                 />
               ) : null}
               ) : null}
-              {showingTable ? <Table data={tableResult} className="m-t-3" /> : null}
+              {supportsTable && showingTable ? <Table data={tableResult} className="m-t-3" /> : null}
+              {supportsLogs && showingLogs ? <Logs data={logsResult} /> : null}
             </main>
             </main>
           </div>
           </div>
         ) : null}
         ) : null}

+ 9 - 0
public/app/containers/Explore/JSONViewer.tsx

@@ -0,0 +1,9 @@
+import React from 'react';
+
+export default function({ value }) {
+  return (
+    <div>
+      <pre>{JSON.stringify(value, undefined, 2)}</pre>
+    </div>
+  );
+}

+ 66 - 0
public/app/containers/Explore/Logs.tsx

@@ -0,0 +1,66 @@
+import React, { Fragment, PureComponent } from 'react';
+
+import { LogsModel, LogRow } from 'app/core/logs_model';
+
+interface LogsProps {
+  className?: string;
+  data: LogsModel;
+}
+
+const EXAMPLE_QUERY = '{job="default/prometheus"}';
+
+const Entry: React.SFC<LogRow> = props => {
+  const { entry, searchMatches } = props;
+  if (searchMatches && searchMatches.length > 0) {
+    let lastMatchEnd = 0;
+    const spans = searchMatches.reduce((acc, match, i) => {
+      // Insert non-match
+      if (match.start !== lastMatchEnd) {
+        acc.push(<>{entry.slice(lastMatchEnd, match.start)}</>);
+      }
+      // Match
+      acc.push(
+        <span className="logs-row-match-highlight" title={`Matching expression: ${match.text}`}>
+          {entry.substr(match.start, match.length)}
+        </span>
+      );
+      lastMatchEnd = match.start + match.length;
+      // Non-matching end
+      if (i === searchMatches.length - 1) {
+        acc.push(<>{entry.slice(lastMatchEnd)}</>);
+      }
+      return acc;
+    }, []);
+    return <>{spans}</>;
+  }
+  return <>{props.entry}</>;
+};
+
+export default class Logs extends PureComponent<LogsProps, any> {
+  render() {
+    const { className = '', data } = this.props;
+    const hasData = data && data.rows && data.rows.length > 0;
+    return (
+      <div className={`${className} logs`}>
+        {hasData ? (
+          <div className="logs-entries panel-container">
+            {data.rows.map(row => (
+              <Fragment key={row.key}>
+                <div className={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''} />
+                <div title={`${row.timestamp} (${row.timeFromNow})`}>{row.timeLocal}</div>
+                <div>
+                  <Entry {...row} />
+                </div>
+              </Fragment>
+            ))}
+          </div>
+        ) : null}
+        {!hasData ? (
+          <div className="panel-container">
+            Enter a query like <code>{EXAMPLE_QUERY}</code>
+          </div>
+        ) : null}
+      </div>
+    );
+  }
+}

+ 4 - 2
public/app/containers/Explore/QueryField.tsx

@@ -17,6 +17,7 @@ import { processLabels, RATE_RANGES, cleanText } from './utils/prometheus';
 import Typeahead from './Typeahead';
 import Typeahead from './Typeahead';
 
 
 const EMPTY_METRIC = '';
 const EMPTY_METRIC = '';
+const METRIC_MARK = 'metric';
 export const TYPEAHEAD_DEBOUNCE = 300;
 export const TYPEAHEAD_DEBOUNCE = 300;
 
 
 function flattenSuggestions(s) {
 function flattenSuggestions(s) {
@@ -135,7 +136,7 @@ class QueryField extends React.Component<any, any> {
     if (!this.state.metrics) {
     if (!this.state.metrics) {
       return;
       return;
     }
     }
-    setPrismTokens(this.props.prismLanguage, 'metrics', this.state.metrics);
+    setPrismTokens(this.props.prismLanguage, METRIC_MARK, this.state.metrics);
 
 
     // Trigger re-render
     // Trigger re-render
     window.requestAnimationFrame(() => {
     window.requestAnimationFrame(() => {
@@ -184,7 +185,7 @@ class QueryField extends React.Component<any, any> {
       let typeaheadContext = null;
       let typeaheadContext = null;
 
 
       // Take first metric as lucky guess
       // Take first metric as lucky guess
-      const metricNode = editorNode.querySelector('.metric');
+      const metricNode = editorNode.querySelector(`.${METRIC_MARK}`);
 
 
       if (wrapperClasses.contains('context-range')) {
       if (wrapperClasses.contains('context-range')) {
         // Rate ranges
         // Rate ranges
@@ -416,6 +417,7 @@ class QueryField extends React.Component<any, any> {
     const url = `/api/v1/label/${key}/values`;
     const url = `/api/v1/label/${key}/values`;
     try {
     try {
       const res = await this.request(url);
       const res = await this.request(url);
+      console.log(res);
       const body = await (res.data || res.json());
       const body = await (res.data || res.json());
       const pairs = this.state.labelValues[EMPTY_METRIC];
       const pairs = this.state.labelValues[EMPTY_METRIC];
       const values = {
       const values = {

+ 0 - 12
public/app/containers/Explore/utils/query.ts

@@ -1,15 +1,3 @@
-export function buildQueryOptions({ format, interval, instant, range, queries }) {
-  return {
-    interval,
-    range,
-    targets: queries.map(expr => ({
-      expr,
-      format,
-      instant,
-    })),
-  };
-}
-
 export function generateQueryKey(index = 0) {
 export function generateQueryKey(index = 0) {
   return `Q-${Date.now()}-${Math.random()}-${index}`;
   return `Q-${Date.now()}-${Math.random()}-${index}`;
 }
 }

+ 4 - 2
public/app/core/components/scroll/page_scroll.ts

@@ -29,11 +29,13 @@ export function pageScrollbar() {
       scope.$on('$routeChangeSuccess', () => {
       scope.$on('$routeChangeSuccess', () => {
         lastPos = 0;
         lastPos = 0;
         elem[0].scrollTop = 0;
         elem[0].scrollTop = 0;
-        elem[0].focus();
+        // Focus page to enable scrolling by keyboard
+        elem[0].focus({ preventScroll: true });
       });
       });
 
 
       elem[0].tabIndex = -1;
       elem[0].tabIndex = -1;
-      elem[0].focus();
+      // Focus page to enable scrolling by keyboard
+      elem[0].focus({ preventScroll: true });
     },
     },
   };
   };
 }
 }

+ 29 - 0
public/app/core/logs_model.ts

@@ -0,0 +1,29 @@
+export enum LogLevel {
+  crit = 'crit',
+  warn = 'warn',
+  err = 'error',
+  error = 'error',
+  info = 'info',
+  debug = 'debug',
+  trace = 'trace',
+}
+
+export interface LogSearchMatch {
+  start: number;
+  length: number;
+  text?: string;
+}
+
+export interface LogRow {
+  key: string;
+  entry: string;
+  logLevel: LogLevel;
+  timestamp: string;
+  timeFromNow: string;
+  timeLocal: string;
+  searchMatches?: LogSearchMatch[];
+}
+
+export interface LogsModel {
+  rows: LogRow[];
+}

+ 1 - 1
public/app/core/services/keybindingSrv.ts

@@ -191,7 +191,7 @@ export class KeybindingSrv {
               range,
               range,
             };
             };
             const exploreState = encodePathComponent(JSON.stringify(state));
             const exploreState = encodePathComponent(JSON.stringify(state));
-            this.$location.url(`/explore/${exploreState}`);
+            this.$location.url(`/explore?state=${exploreState}`);
           }
           }
         }
         }
       });
       });

+ 2 - 0
public/app/core/utils/kbn.ts

@@ -449,6 +449,7 @@ kbn.valueFormats.currencyNOK = kbn.formatBuilders.currency('kr');
 kbn.valueFormats.currencySEK = kbn.formatBuilders.currency('kr');
 kbn.valueFormats.currencySEK = kbn.formatBuilders.currency('kr');
 kbn.valueFormats.currencyCZK = kbn.formatBuilders.currency('czk');
 kbn.valueFormats.currencyCZK = kbn.formatBuilders.currency('czk');
 kbn.valueFormats.currencyCHF = kbn.formatBuilders.currency('CHF');
 kbn.valueFormats.currencyCHF = kbn.formatBuilders.currency('CHF');
+kbn.valueFormats.currencyPLN = kbn.formatBuilders.currency('zł');
 
 
 // Data (Binary)
 // Data (Binary)
 kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
 kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
@@ -880,6 +881,7 @@ kbn.getUnitFormats = function() {
         { text: 'Swedish Krona (kr)', value: 'currencySEK' },
         { text: 'Swedish Krona (kr)', value: 'currencySEK' },
         { text: 'Czech koruna (czk)', value: 'currencyCZK' },
         { text: 'Czech koruna (czk)', value: 'currencyCZK' },
         { text: 'Swiss franc (CHF)', value: 'currencyCHF' },
         { text: 'Swiss franc (CHF)', value: 'currencyCHF' },
+        { text: 'Polish Złoty (PLN)', value: 'currencyPLN' },
       ],
       ],
     },
     },
     {
     {

+ 2 - 2
public/app/features/panel/metrics_panel_ctrl.ts

@@ -222,7 +222,7 @@ class MetricsPanelCtrl extends PanelCtrl {
     // and add built in variables interval and interval_ms
     // and add built in variables interval and interval_ms
     var scopedVars = Object.assign({}, this.panel.scopedVars, {
     var scopedVars = Object.assign({}, this.panel.scopedVars, {
       __interval: { text: this.interval, value: this.interval },
       __interval: { text: this.interval, value: this.interval },
-      __interval_ms: { text: String(this.intervalMs), value: String(this.intervalMs) },
+      __interval_ms: { text: this.intervalMs, value: this.intervalMs },
     });
     });
 
 
     var metricsQuery = {
     var metricsQuery = {
@@ -332,7 +332,7 @@ class MetricsPanelCtrl extends PanelCtrl {
       range,
       range,
     };
     };
     const exploreState = encodePathComponent(JSON.stringify(state));
     const exploreState = encodePathComponent(JSON.stringify(state));
-    this.$location.url(`/explore/${exploreState}`);
+    this.$location.url(`/explore?state=${exploreState}`);
   }
   }
 
 
   addQuery(target) {
   addQuery(target) {

+ 4 - 5
public/app/features/plugins/built_in_plugins.ts

@@ -4,11 +4,13 @@ import * as elasticsearchPlugin from 'app/plugins/datasource/elasticsearch/modul
 import * as opentsdbPlugin from 'app/plugins/datasource/opentsdb/module';
 import * as opentsdbPlugin from 'app/plugins/datasource/opentsdb/module';
 import * as grafanaPlugin from 'app/plugins/datasource/grafana/module';
 import * as grafanaPlugin from 'app/plugins/datasource/grafana/module';
 import * as influxdbPlugin from 'app/plugins/datasource/influxdb/module';
 import * as influxdbPlugin from 'app/plugins/datasource/influxdb/module';
+import * as loggingPlugin from 'app/plugins/datasource/logging/module';
 import * as mixedPlugin from 'app/plugins/datasource/mixed/module';
 import * as mixedPlugin from 'app/plugins/datasource/mixed/module';
 import * as mysqlPlugin from 'app/plugins/datasource/mysql/module';
 import * as mysqlPlugin from 'app/plugins/datasource/mysql/module';
 import * as postgresPlugin from 'app/plugins/datasource/postgres/module';
 import * as postgresPlugin from 'app/plugins/datasource/postgres/module';
 import * as prometheusPlugin from 'app/plugins/datasource/prometheus/module';
 import * as prometheusPlugin from 'app/plugins/datasource/prometheus/module';
 import * as mssqlPlugin from 'app/plugins/datasource/mssql/module';
 import * as mssqlPlugin from 'app/plugins/datasource/mssql/module';
+import * as testDataDSPlugin from 'app/plugins/datasource/testdata/module';
 
 
 import * as textPanel from 'app/plugins/panel/text/module';
 import * as textPanel from 'app/plugins/panel/text/module';
 import * as graphPanel from 'app/plugins/panel/graph/module';
 import * as graphPanel from 'app/plugins/panel/graph/module';
@@ -20,9 +22,6 @@ import * as tablePanel from 'app/plugins/panel/table/module';
 import * as singlestatPanel from 'app/plugins/panel/singlestat/module';
 import * as singlestatPanel from 'app/plugins/panel/singlestat/module';
 import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
 import * as gettingStartedPanel from 'app/plugins/panel/gettingstarted/module';
 
 
-import * as testDataAppPlugin from 'app/plugins/app/testdata/module';
-import * as testDataDSPlugin from 'app/plugins/app/testdata/datasource/module';
-
 const builtInPlugins = {
 const builtInPlugins = {
   'app/plugins/datasource/graphite/module': graphitePlugin,
   'app/plugins/datasource/graphite/module': graphitePlugin,
   'app/plugins/datasource/cloudwatch/module': cloudwatchPlugin,
   'app/plugins/datasource/cloudwatch/module': cloudwatchPlugin,
@@ -30,13 +29,13 @@ const builtInPlugins = {
   'app/plugins/datasource/opentsdb/module': opentsdbPlugin,
   'app/plugins/datasource/opentsdb/module': opentsdbPlugin,
   'app/plugins/datasource/grafana/module': grafanaPlugin,
   'app/plugins/datasource/grafana/module': grafanaPlugin,
   'app/plugins/datasource/influxdb/module': influxdbPlugin,
   'app/plugins/datasource/influxdb/module': influxdbPlugin,
+  'app/plugins/datasource/logging/module': loggingPlugin,
   'app/plugins/datasource/mixed/module': mixedPlugin,
   'app/plugins/datasource/mixed/module': mixedPlugin,
   'app/plugins/datasource/mysql/module': mysqlPlugin,
   'app/plugins/datasource/mysql/module': mysqlPlugin,
   'app/plugins/datasource/postgres/module': postgresPlugin,
   'app/plugins/datasource/postgres/module': postgresPlugin,
   'app/plugins/datasource/mssql/module': mssqlPlugin,
   'app/plugins/datasource/mssql/module': mssqlPlugin,
   'app/plugins/datasource/prometheus/module': prometheusPlugin,
   'app/plugins/datasource/prometheus/module': prometheusPlugin,
-  'app/plugins/app/testdata/module': testDataAppPlugin,
-  'app/plugins/app/testdata/datasource/module': testDataDSPlugin,
+  'app/plugins/datasource/testdata/module': testDataDSPlugin,
 
 
   'app/plugins/panel/text/module': textPanel,
   'app/plugins/panel/text/module': textPanel,
   'app/plugins/panel/graph/module': graphPanel,
   'app/plugins/panel/graph/module': graphPanel,

+ 14 - 5
public/app/features/plugins/datasource_srv.ts

@@ -34,13 +34,13 @@ export class DatasourceSrv {
   }
   }
 
 
   loadDatasource(name) {
   loadDatasource(name) {
-    var dsConfig = config.datasources[name];
+    const dsConfig = config.datasources[name];
     if (!dsConfig) {
     if (!dsConfig) {
       return this.$q.reject({ message: 'Datasource named ' + name + ' was not found' });
       return this.$q.reject({ message: 'Datasource named ' + name + ' was not found' });
     }
     }
 
 
-    var deferred = this.$q.defer();
-    var pluginDef = dsConfig.meta;
+    const deferred = this.$q.defer();
+    const pluginDef = dsConfig.meta;
 
 
     importPluginModule(pluginDef.module)
     importPluginModule(pluginDef.module)
       .then(plugin => {
       .then(plugin => {
@@ -55,7 +55,7 @@ export class DatasourceSrv {
           throw new Error('Plugin module is missing Datasource constructor');
           throw new Error('Plugin module is missing Datasource constructor');
         }
         }
 
 
-        var instance = this.$injector.instantiate(plugin.Datasource, { instanceSettings: dsConfig });
+        const instance = this.$injector.instantiate(plugin.Datasource, { instanceSettings: dsConfig });
         instance.meta = pluginDef;
         instance.meta = pluginDef;
         instance.name = name;
         instance.name = name;
         this.datasources[name] = instance;
         this.datasources[name] = instance;
@@ -73,7 +73,7 @@ export class DatasourceSrv {
   }
   }
 
 
   getAnnotationSources() {
   getAnnotationSources() {
-    var sources = [];
+    const sources = [];
 
 
     this.addDataSourceVariables(sources);
     this.addDataSourceVariables(sources);
 
 
@@ -86,6 +86,14 @@ export class DatasourceSrv {
     return sources;
     return sources;
   }
   }
 
 
+  getExploreSources() {
+    const { datasources } = config;
+    const es = Object.keys(datasources)
+      .map(name => datasources[name])
+      .filter(ds => ds.meta && ds.meta.explore);
+    return _.sortBy(es, ['name']);
+  }
+
   getMetricSources(options) {
   getMetricSources(options) {
     var metricSources = [];
     var metricSources = [];
 
 
@@ -155,3 +163,4 @@ export class DatasourceSrv {
 }
 }
 
 
 coreModule.service('datasourceSrv', DatasourceSrv);
 coreModule.service('datasourceSrv', DatasourceSrv);
+export default DatasourceSrv;

+ 1 - 0
public/app/features/plugins/ds_list_ctrl.ts

@@ -19,6 +19,7 @@ export class DataSourcesCtrl {
   onQueryUpdated() {
   onQueryUpdated() {
     let regex = new RegExp(this.searchQuery, 'ig');
     let regex = new RegExp(this.searchQuery, 'ig');
     this.datasources = _.filter(this.unfiltered, item => {
     this.datasources = _.filter(this.unfiltered, item => {
+      regex.lastIndex = 0;
       return regex.test(item.name) || regex.test(item.type);
       return regex.test(item.name) || regex.test(item.type);
     });
     });
   }
   }

+ 3 - 1
public/app/features/plugins/plugin_loader.ts

@@ -56,7 +56,7 @@ System.config({
     css: 'vendor/plugin-css/css.js',
     css: 'vendor/plugin-css/css.js',
   },
   },
   meta: {
   meta: {
-    '*': {
+    '/*': {
       esModule: true,
       esModule: true,
       authorization: true,
       authorization: true,
       loader: 'plugin-loader',
       loader: 'plugin-loader',
@@ -126,6 +126,7 @@ import 'vendor/flot/jquery.flot.stackpercent';
 import 'vendor/flot/jquery.flot.fillbelow';
 import 'vendor/flot/jquery.flot.fillbelow';
 import 'vendor/flot/jquery.flot.crosshair';
 import 'vendor/flot/jquery.flot.crosshair';
 import 'vendor/flot/jquery.flot.dashes';
 import 'vendor/flot/jquery.flot.dashes';
+import 'vendor/flot/jquery.flot.gauge';
 
 
 const flotDeps = [
 const flotDeps = [
   'jquery.flot',
   'jquery.flot',
@@ -137,6 +138,7 @@ const flotDeps = [
   'jquery.flot.selection',
   'jquery.flot.selection',
   'jquery.flot.stackpercent',
   'jquery.flot.stackpercent',
   'jquery.flot.events',
   'jquery.flot.events',
+  'jquery.flot.gauge',
 ];
 ];
 for (let flotDep of flotDeps) {
 for (let flotDep of flotDeps) {
   exposeToPlugin(flotDep, { fakeDep: 1 });
   exposeToPlugin(flotDep, { fakeDep: 1 });

+ 27 - 1
public/app/features/plugins/specs/datasource_srv.jest.ts

@@ -17,9 +17,35 @@ const templateSrv = {
 
 
 describe('datasource_srv', function() {
 describe('datasource_srv', function() {
   let _datasourceSrv = new DatasourceSrv({}, {}, {}, templateSrv);
   let _datasourceSrv = new DatasourceSrv({}, {}, {}, templateSrv);
-  let metricSources;
+
+  describe('when loading explore sources', () => {
+    beforeEach(() => {
+      config.datasources = {
+        explore1: {
+          name: 'explore1',
+          meta: { explore: true, metrics: true },
+        },
+        explore2: {
+          name: 'explore2',
+          meta: { explore: true, metrics: false },
+        },
+        nonExplore: {
+          name: 'nonExplore',
+          meta: { explore: false, metrics: true },
+        },
+      };
+    });
+
+    it('should return list of explore sources', () => {
+      const exploreSources = _datasourceSrv.getExploreSources();
+      expect(exploreSources.length).toBe(2);
+      expect(exploreSources[0].name).toBe('explore1');
+      expect(exploreSources[1].name).toBe('explore2');
+    });
+  });
 
 
   describe('when loading metric sources', () => {
   describe('when loading metric sources', () => {
+    let metricSources;
     let unsortedDatasources = {
     let unsortedDatasources = {
       mmm: {
       mmm: {
         type: 'test-db',
         type: 'test-db',

+ 0 - 1448
public/app/plugins/app/testdata/dashboards/graph_last_1h.json

@@ -1,1448 +0,0 @@
-{
-  "annotations": {
-    "list": []
-  },
-  "editable": true,
-  "gnetId": null,
-  "graphTooltip": 0,
-  "hideControls": false,
-  "links": [],
-  "refresh": false,
-  "revision": 8,
-  "rows": [
-    {
-      "collapse": false,
-      "height": "250px",
-      "panels": [
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "editable": true,
-          "error": false,
-          "fill": 1,
-          "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": [],
-          "span": 4,
-          "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": {
-            "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
-            }
-          ]
-        },
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "editable": true,
-          "error": false,
-          "fill": 1,
-          "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": [],
-          "span": 4,
-          "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": {
-            "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
-            }
-          ]
-        },
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "editable": true,
-          "error": false,
-          "fill": 1,
-          "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": [],
-          "span": 4,
-          "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": {
-            "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
-            }
-          ]
-        }
-      ],
-      "repeat": null,
-      "repeatIteration": null,
-      "repeatRowId": null,
-      "showTitle": false,
-      "title": "New row",
-      "titleSize": "h6"
-    },
-    {
-      "collapse": false,
-      "height": "250px",
-      "panels": [
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "editable": true,
-          "error": false,
-          "fill": 1,
-          "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": [],
-          "span": 8,
-          "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": {
-            "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
-            }
-          ]
-        },
-        {
-          "content": "Just verify that the tooltip time has millisecond resolution ",
-          "editable": true,
-          "error": false,
-          "id": 6,
-          "links": [],
-          "mode": "markdown",
-          "span": 4,
-          "title": "",
-          "type": "text"
-        }
-      ],
-      "repeat": null,
-      "repeatIteration": null,
-      "repeatRowId": null,
-      "showTitle": false,
-      "title": "New row",
-      "titleSize": "h6"
-    },
-    {
-      "collapse": false,
-      "height": 336,
-      "panels": [
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "editable": true,
-          "error": false,
-          "fill": 1,
-          "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
-            }
-          ],
-          "span": 8,
-          "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": {
-            "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
-            }
-          ]
-        },
-        {
-          "content": "Verify that axis labels look ok",
-          "editable": true,
-          "error": false,
-          "id": 7,
-          "links": [],
-          "mode": "markdown",
-          "span": 4,
-          "title": "",
-          "type": "text"
-        }
-      ],
-      "repeat": null,
-      "repeatIteration": null,
-      "repeatRowId": null,
-      "showTitle": false,
-      "title": "New row",
-      "titleSize": "h6"
-    },
-    {
-      "collapse": false,
-      "height": "250px",
-      "panels": [
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "editable": true,
-          "error": false,
-          "fill": 1,
-          "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": [],
-          "span": 4,
-          "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": {
-            "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
-            }
-          ]
-        },
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "editable": true,
-          "error": false,
-          "fill": 1,
-          "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": [],
-          "span": 4,
-          "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": {
-            "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
-            }
-          ]
-        },
-        {
-          "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,
-          "id": 13,
-          "links": [],
-          "mode": "markdown",
-          "span": 4,
-          "title": "",
-          "type": "text"
-        }
-      ],
-      "repeat": null,
-      "repeatIteration": null,
-      "repeatRowId": null,
-      "showTitle": false,
-      "title": "New row",
-      "titleSize": "h6"
-    },
-    {
-      "collapse": false,
-      "height": 250,
-      "panels": [
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "editable": true,
-          "error": false,
-          "fill": 1,
-          "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
-            }
-          ],
-          "span": 8,
-          "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": {
-            "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
-            }
-          ]
-        },
-        {
-          "content": "Stacking values on top of nulls, should treat the null values as zero. ",
-          "editable": true,
-          "error": false,
-          "id": 14,
-          "links": [],
-          "mode": "markdown",
-          "span": 4,
-          "title": "",
-          "type": "text"
-        }
-      ],
-      "repeat": null,
-      "repeatIteration": null,
-      "repeatRowId": null,
-      "showTitle": false,
-      "title": "Dashboard Row",
-      "titleSize": "h6"
-    },
-    {
-      "collapse": false,
-      "height": 250,
-      "panels": [
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "editable": true,
-          "error": false,
-          "fill": 1,
-          "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
-            }
-          ],
-          "span": 8,
-          "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": {
-            "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
-            }
-          ]
-        },
-        {
-          "content": "Stacking when all values are null should leave a gap in the graph",
-          "editable": true,
-          "error": false,
-          "id": 15,
-          "links": [],
-          "mode": "markdown",
-          "span": 4,
-          "title": "",
-          "type": "text"
-        }
-      ],
-      "repeat": null,
-      "repeatIteration": null,
-      "repeatRowId": null,
-      "showTitle": false,
-      "title": "Dashboard Row",
-      "titleSize": "h6"
-    },
-    {
-      "collapse": false,
-      "height": 250,
-      "panels": [
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "decimals": 3,
-          "fill": 1,
-          "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": [],
-          "span": 12,
-          "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": {
-            "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
-            }
-          ]
-        }
-      ],
-      "repeat": null,
-      "repeatIteration": null,
-      "repeatRowId": null,
-      "showTitle": false,
-      "title": "Dashboard Row",
-      "titleSize": "h6"
-    },
-    {
-      "collapse": false,
-      "height": 250,
-      "panels": [
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "decimals": 3,
-          "fill": 1,
-          "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": [],
-          "span": 6,
-          "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": {
-            "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
-            }
-          ]
-        },
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "decimals": 3,
-          "fill": 1,
-          "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": [],
-          "span": 6,
-          "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": {
-            "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
-            }
-          ]
-        }
-      ],
-      "repeat": null,
-      "repeatIteration": null,
-      "repeatRowId": null,
-      "showTitle": false,
-      "title": "Dashboard Row",
-      "titleSize": "h6"
-    },
-    {
-      "collapse": false,
-      "height": 250,
-      "panels": [
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "decimals": 3,
-          "fill": 1,
-          "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": [],
-          "span": 6,
-          "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": {
-            "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
-            }
-          ]
-        },
-        {
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "Grafana TestData",
-          "decimals": 3,
-          "fill": 1,
-          "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": [],
-          "span": 6,
-          "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": {
-            "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
-            }
-          ]
-        }
-      ],
-      "repeat": null,
-      "repeatIteration": null,
-      "repeatRowId": null,
-      "showTitle": false,
-      "title": "Dashboard Row",
-      "titleSize": "h6"
-    }
-  ],
-  "schemaVersion": 14,
-  "style": "dark",
-  "tags": [
-    "grafana-test"
-  ],
-  "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": "TestData - Graph Panel Last 1h",
-  "version": 2
-}

+ 0 - 34
public/app/plugins/app/testdata/module.ts

@@ -1,34 +0,0 @@
-export class ConfigCtrl {
-  static template = '';
-
-  appEditCtrl: any;
-
-  /** @ngInject **/
-  constructor(private backendSrv) {
-    this.appEditCtrl.setPreUpdateHook(this.initDatasource.bind(this));
-  }
-
-  initDatasource() {
-    return this.backendSrv.get('/api/datasources').then(res => {
-      var found = false;
-      for (let ds of res) {
-        if (ds.type === 'grafana-testdata-datasource') {
-          found = true;
-        }
-      }
-
-      if (!found) {
-        var dsInstance = {
-          name: 'Grafana TestData',
-          type: 'grafana-testdata-datasource',
-          access: 'direct',
-          jsonData: {},
-        };
-
-        return this.backendSrv.post('/api/datasources', dsInstance);
-      }
-
-      return Promise.resolve();
-    });
-  }
-}

+ 0 - 32
public/app/plugins/app/testdata/plugin.json

@@ -1,32 +0,0 @@
-{
-  "type": "app",
-  "name": "Grafana TestData",
-  "id": "testdata",
-
-  "info": {
-    "description": "Grafana test data app",
-    "author": {
-      "name": "Grafana Project",
-      "url": "https://grafana.com"
-    },
-    "version": "1.0.17",
-    "updated": "2016-09-26"
-  },
-
-  "includes": [
-    {
-      "type": "dashboard",
-      "name": "TestData - Graph Last 1h",
-      "path": "dashboards/graph_last_1h.json"
-    },
-    {
-      "type": "dashboard",
-      "name": "TestData - Alerts",
-      "path": "dashboards/alerts.json"
-    }
-  ],
-
-  "dependencies": {
-    "grafanaVersion": "4.x.x"
-  }
-}

+ 11 - 1
public/app/plugins/datasource/cloudwatch/datasource.ts

@@ -30,7 +30,9 @@ export default class CloudWatchDatasource {
 
 
     var queries = _.filter(options.targets, item => {
     var queries = _.filter(options.targets, item => {
       return (
       return (
-        item.hide !== true && !!item.region && !!item.namespace && !!item.metricName && !_.isEmpty(item.statistics)
+        (item.id !== '' || item.hide !== true) &&
+        ((!!item.region && !!item.namespace && !!item.metricName && !_.isEmpty(item.statistics)) ||
+          item.expression.length > 0)
       );
       );
     }).map(item => {
     }).map(item => {
       item.region = this.templateSrv.replace(this.getActualRegion(item.region), options.scopedVars);
       item.region = this.templateSrv.replace(this.getActualRegion(item.region), options.scopedVars);
@@ -38,6 +40,9 @@ export default class CloudWatchDatasource {
       item.metricName = this.templateSrv.replace(item.metricName, options.scopedVars);
       item.metricName = this.templateSrv.replace(item.metricName, options.scopedVars);
       item.dimensions = this.convertDimensionFormat(item.dimensions, options.scopedVars);
       item.dimensions = this.convertDimensionFormat(item.dimensions, options.scopedVars);
       item.period = String(this.getPeriod(item, options)); // use string format for period in graph query, and alerting
       item.period = String(this.getPeriod(item, options)); // use string format for period in graph query, and alerting
+      item.id = this.templateSrv.replace(item.id, options.scopedVars);
+      item.expression = this.templateSrv.replace(item.expression, options.scopedVars);
+      item.returnData = typeof item.hide === 'undefined' ? true : !item.hide;
 
 
       // valid ExtendedStatistics is like p90.00, check the pattern
       // valid ExtendedStatistics is like p90.00, check the pattern
       let hasInvalidStatistics = item.statistics.some(s => {
       let hasInvalidStatistics = item.statistics.some(s => {
@@ -407,6 +412,11 @@ export default class CloudWatchDatasource {
       scopedVar[variable.name] = v;
       scopedVar[variable.name] = v;
       t.refId = target.refId + '_' + v.value;
       t.refId = target.refId + '_' + v.value;
       t.dimensions[dimensionKey] = templateSrv.replace(t.dimensions[dimensionKey], scopedVar);
       t.dimensions[dimensionKey] = templateSrv.replace(t.dimensions[dimensionKey], scopedVar);
+      if (variable.multi && target.id) {
+        t.id = target.id + window.btoa(v.value).replace(/=/g, '0'); // generate unique id
+      } else {
+        t.id = target.id;
+      }
       return t;
       return t;
     });
     });
   }
   }

+ 31 - 14
public/app/plugins/datasource/cloudwatch/partials/query.parameter.html

@@ -1,4 +1,4 @@
-<div class="gf-form-inline">
+<div class="gf-form-inline" ng-if="target.expression.length === 0">
 	<div class="gf-form">
 	<div class="gf-form">
 		<label class="gf-form-label query-keyword width-8">Metric</label>
 		<label class="gf-form-label query-keyword width-8">Metric</label>
 
 
@@ -20,7 +20,7 @@
 	</div>
 	</div>
 </div>
 </div>
 
 
-<div class="gf-form-inline">
+<div class="gf-form-inline" ng-if="target.expression.length === 0">
 	<div class="gf-form">
 	<div class="gf-form">
 		<label class="gf-form-label query-keyword width-8">Dimensions</label>
 		<label class="gf-form-label query-keyword width-8">Dimensions</label>
 		<metric-segment ng-repeat="segment in dimSegments" segment="segment" get-options="getDimSegments(segment, $index)" on-change="dimSegmentChanged(segment, $index)"></metric-segment>
 		<metric-segment ng-repeat="segment in dimSegments" segment="segment" get-options="getDimSegments(segment, $index)" on-change="dimSegmentChanged(segment, $index)"></metric-segment>
@@ -31,18 +31,35 @@
 	</div>
 	</div>
 </div>
 </div>
 
 
-<div class="gf-form-inline">
+<div class="gf-form-inline" ng-if="target.statistics.length === 1">
 	<div class="gf-form">
 	<div class="gf-form">
-		<label class="gf-form-label query-keyword width-8">
+		<label class=" gf-form-label query-keyword width-8 ">
+			Id
+			<info-popover mode="right-normal ">Id can include numbers, letters, and underscore, and must start with a lowercase letter.</info-popover>
+		</label>
+		<input type="text " class="gf-form-input " ng-model="target.id " spellcheck='false' ng-pattern='/^[a-z][A-Z0-9_]*/' ng-model-onblur
+		 ng-change="onChange() ">
+	</div>
+	<div class="gf-form max-width-30 ">
+		<label class="gf-form-label query-keyword width-7 ">Expression</label>
+		<input type="text " class="gf-form-input " ng-model="target.expression
+	 " spellcheck='false' ng-model-onblur ng-change="onChange() ">
+	</div>
+</div>
+
+<div class="gf-form-inline ">
+	<div class="gf-form ">
+		<label class="gf-form-label query-keyword width-8 ">
 			Min period
 			Min period
-			<info-popover mode="right-normal">Minimum interval between points in seconds</info-popover>
+			<info-popover mode="right-normal ">Minimum interval between points in seconds</info-popover>
 		</label>
 		</label>
-		<input type="text" class="gf-form-input" ng-model="target.period" spellcheck='false' placeholder="auto" ng-model-onblur ng-change="onChange()" />
+		<input type="text " class="gf-form-input " ng-model="target.period " spellcheck='false' placeholder="auto
+	 " ng-model-onblur ng-change="onChange() " />
 	</div>
 	</div>
-	<div class="gf-form max-width-30">
-		<label class="gf-form-label query-keyword width-7">Alias</label>
-		<input type="text" class="gf-form-input"  ng-model="target.alias" spellcheck='false' ng-model-onblur ng-change="onChange()">
-		<info-popover mode="right-absolute">
+	<div class="gf-form max-width-30 ">
+		<label class="gf-form-label query-keyword width-7 ">Alias</label>
+		<input type="text " class="gf-form-input " ng-model="target.alias " spellcheck='false' ng-model-onblur ng-change="onChange() ">
+		<info-popover mode="right-absolute ">
 			Alias replacement variables:
 			Alias replacement variables:
 			<ul ng-non-bindable>
 			<ul ng-non-bindable>
 				<li>{{metric}}</li>
 				<li>{{metric}}</li>
@@ -54,12 +71,12 @@
 			</ul>
 			</ul>
 		</info-popover>
 		</info-popover>
 	</div>
 	</div>
-	<div class="gf-form">
-		<gf-form-switch class="gf-form" label="HighRes" label-class="width-5" checked="target.highResolution" on-change="onChange()">
+	<div class="gf-form ">
+		<gf-form-switch class="gf-form " label="HighRes " label-class="width-5 " checked="target.highResolution " on-change="onChange() ">
 		</gf-form-switch>
 		</gf-form-switch>
 	</div>
 	</div>
 
 
-	<div class="gf-form gf-form--grow">
-		<div class="gf-form-label gf-form-label--grow"></div>
+	<div class="gf-form gf-form--grow ">
+		<div class="gf-form-label gf-form-label--grow "></div>
 	</div>
 	</div>
 </div>
 </div>

+ 3 - 0
public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts

@@ -27,6 +27,9 @@ export class CloudWatchQueryParameterCtrl {
       target.dimensions = target.dimensions || {};
       target.dimensions = target.dimensions || {};
       target.period = target.period || '';
       target.period = target.period || '';
       target.region = target.region || 'default';
       target.region = target.region || 'default';
+      target.id = target.id || '';
+      target.expression = target.expression || '';
+      target.returnData = target.returnData || false;
       target.highResolution = target.highResolution || false;
       target.highResolution = target.highResolution || false;
 
 
       $scope.regionSegment = uiSegmentSrv.getSegmentForValue($scope.target.region, 'select region');
       $scope.regionSegment = uiSegmentSrv.getSegmentForValue($scope.target.region, 'select region');

+ 3 - 0
public/app/plugins/datasource/logging/README.md

@@ -0,0 +1,3 @@
+# Grafana Logging Datasource -  Native Plugin
+
+This is a **built in** datasource that allows you to connect to Grafana's logging service.

+ 38 - 0
public/app/plugins/datasource/logging/datasource.jest.ts

@@ -0,0 +1,38 @@
+import { parseQuery } from './datasource';
+
+describe('parseQuery', () => {
+  it('returns empty for empty string', () => {
+    expect(parseQuery('')).toEqual({
+      query: '',
+      regexp: '',
+    });
+  });
+
+  it('returns regexp for strings without query', () => {
+    expect(parseQuery('test')).toEqual({
+      query: '',
+      regexp: 'test',
+    });
+  });
+
+  it('returns query for strings without regexp', () => {
+    expect(parseQuery('{foo="bar"}')).toEqual({
+      query: '{foo="bar"}',
+      regexp: '',
+    });
+  });
+
+  it('returns query for strings with query and search string', () => {
+    expect(parseQuery('x {foo="bar"}')).toEqual({
+      query: '{foo="bar"}',
+      regexp: 'x',
+    });
+  });
+
+  it('returns query for strings with query and regexp', () => {
+    expect(parseQuery('{foo="bar"} x|y')).toEqual({
+      query: '{foo="bar"}',
+      regexp: 'x|y',
+    });
+  });
+});

+ 134 - 0
public/app/plugins/datasource/logging/datasource.ts

@@ -0,0 +1,134 @@
+import _ from 'lodash';
+
+import * as dateMath from 'app/core/utils/datemath';
+
+import { processStreams } from './result_transformer';
+
+const DEFAULT_LIMIT = 100;
+
+const DEFAULT_QUERY_PARAMS = {
+  direction: 'BACKWARD',
+  limit: DEFAULT_LIMIT,
+  regexp: '',
+  query: '',
+};
+
+const QUERY_REGEXP = /({\w+="[^"]+"})?\s*(\w[^{]+)?\s*({\w+="[^"]+"})?/;
+export function parseQuery(input: string) {
+  const match = input.match(QUERY_REGEXP);
+  let query = '';
+  let regexp = '';
+
+  if (match) {
+    if (match[1]) {
+      query = match[1];
+    }
+    if (match[2]) {
+      regexp = match[2].trim();
+    }
+    if (match[3]) {
+      if (match[1]) {
+        query = `${match[1].slice(0, -1)},${match[3].slice(1)}`;
+      } else {
+        query = match[3];
+      }
+    }
+  }
+
+  return { query, regexp };
+}
+
+function serializeParams(data: any) {
+  return Object.keys(data)
+    .map(k => {
+      const v = data[k];
+      return encodeURIComponent(k) + '=' + encodeURIComponent(v);
+    })
+    .join('&');
+}
+
+export default class LoggingDatasource {
+  /** @ngInject */
+  constructor(private instanceSettings, private backendSrv, private templateSrv) {}
+
+  _request(apiUrl: string, data?, options?: any) {
+    const baseUrl = this.instanceSettings.url;
+    const params = data ? serializeParams(data) : '';
+    const url = `${baseUrl}${apiUrl}?${params}`;
+    const req = {
+      ...options,
+      url,
+    };
+    return this.backendSrv.datasourceRequest(req);
+  }
+
+  prepareQueryTarget(target, options) {
+    const interpolated = this.templateSrv.replace(target.expr);
+    const start = this.getTime(options.range.from, false);
+    const end = this.getTime(options.range.to, true);
+    return {
+      ...DEFAULT_QUERY_PARAMS,
+      ...parseQuery(interpolated),
+      start,
+      end,
+    };
+  }
+
+  query(options) {
+    const queryTargets = options.targets
+      .filter(target => target.expr)
+      .map(target => this.prepareQueryTarget(target, options));
+    if (queryTargets.length === 0) {
+      return Promise.resolve({ data: [] });
+    }
+
+    const queries = queryTargets.map(target => this._request('/api/prom/query', target));
+
+    return Promise.all(queries).then((results: any[]) => {
+      // Flatten streams from multiple queries
+      const allStreams = results.reduce((acc, response, i) => {
+        const streams = response.data.streams || [];
+        // Inject search for match highlighting
+        const search = queryTargets[i].regexp;
+        streams.forEach(s => {
+          s.search = search;
+        });
+        return [...acc, ...streams];
+      }, []);
+      const model = processStreams(allStreams, DEFAULT_LIMIT);
+      return { data: model };
+    });
+  }
+
+  metadataRequest(url) {
+    // HACK to get label values for {job=|}, will be replaced when implementing LoggingQueryField
+    const apiUrl = url.replace('v1', 'prom');
+    return this._request(apiUrl, { silent: true }).then(res => {
+      const data = { data: { data: res.data.values || [] } };
+      return data;
+    });
+  }
+
+  getTime(date, roundUp) {
+    if (_.isString(date)) {
+      date = dateMath.parse(date, roundUp);
+    }
+    return Math.ceil(date.valueOf() * 1e6);
+  }
+
+  testDatasource() {
+    return this._request('/api/prom/label')
+      .then(res => {
+        if (res && res.data && res.data.values && res.data.values.length > 0) {
+          return { status: 'success', message: 'Data source connected and labels found.' };
+        }
+        return {
+          status: 'error',
+          message: 'Data source connected, but no labels received. Verify that logging is configured properly.',
+        };
+      })
+      .catch(err => {
+        return { status: 'error', message: err.message };
+      });
+  }
+}

+ 57 - 0
public/app/plugins/datasource/logging/img/grafana_icon.svg

@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="351px" height="365px" viewBox="0 0 351 365" style="enable-background:new 0 0 351 365;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:url(#SVGID_1_);}
+</style>
+<g id="Layer_1_1_">
+</g>
+<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="175.5" y1="445.4948" x2="175.5" y2="114.0346">
+	<stop  offset="0" style="stop-color:#FFF100"/>
+	<stop  offset="1" style="stop-color:#F05A28"/>
+</linearGradient>
+<path class="st0" d="M342,161.2c-0.6-6.1-1.6-13.1-3.6-20.9c-2-7.7-5-16.2-9.4-25c-4.4-8.8-10.1-17.9-17.5-26.8
+	c-2.9-3.5-6.1-6.9-9.5-10.2c5.1-20.3-6.2-37.9-6.2-37.9c-19.5-1.2-31.9,6.1-36.5,9.4c-0.8-0.3-1.5-0.7-2.3-1
+	c-3.3-1.3-6.7-2.6-10.3-3.7c-3.5-1.1-7.1-2.1-10.8-3c-3.7-0.9-7.4-1.6-11.2-2.2c-0.7-0.1-1.3-0.2-2-0.3
+	c-8.5-27.2-32.9-38.6-32.9-38.6c-27.3,17.3-32.4,41.5-32.4,41.5s-0.1,0.5-0.3,1.4c-1.5,0.4-3,0.9-4.5,1.3c-2.1,0.6-4.2,1.4-6.2,2.2
+	c-2.1,0.8-4.1,1.6-6.2,2.5c-4.1,1.8-8.2,3.8-12.2,6c-3.9,2.2-7.7,4.6-11.4,7.1c-0.5-0.2-1-0.4-1-0.4c-37.8-14.4-71.3,2.9-71.3,2.9
+	c-3.1,40.2,15.1,65.5,18.7,70.1c-0.9,2.5-1.7,5-2.5,7.5c-2.8,9.1-4.9,18.4-6.2,28.1c-0.2,1.4-0.4,2.8-0.5,4.2
+	C18.8,192.7,8.5,228,8.5,228c29.1,33.5,63.1,35.6,63.1,35.6c0,0,0.1-0.1,0.1-0.1c4.3,7.7,9.3,15,14.9,21.9c2.4,2.9,4.8,5.6,7.4,8.3
+	c-10.6,30.4,1.5,55.6,1.5,55.6c32.4,1.2,53.7-14.2,58.2-17.7c3.2,1.1,6.5,2.1,9.8,2.9c10,2.6,20.2,4.1,30.4,4.5
+	c2.5,0.1,5.1,0.2,7.6,0.1l1.2,0l0.8,0l1.6,0l1.6-0.1l0,0.1c15.3,21.8,42.1,24.9,42.1,24.9c19.1-20.1,20.2-40.1,20.2-44.4l0,0
+	c0,0,0-0.1,0-0.3c0-0.4,0-0.6,0-0.6l0,0c0-0.3,0-0.6,0-0.9c4-2.8,7.8-5.8,11.4-9.1c7.6-6.9,14.3-14.8,19.9-23.3
+	c0.5-0.8,1-1.6,1.5-2.4c21.6,1.2,36.9-13.4,36.9-13.4c-3.6-22.5-16.4-33.5-19.1-35.6l0,0c0,0-0.1-0.1-0.3-0.2
+	c-0.2-0.1-0.2-0.2-0.2-0.2c0,0,0,0,0,0c-0.1-0.1-0.3-0.2-0.5-0.3c0.1-1.4,0.2-2.7,0.3-4.1c0.2-2.4,0.2-4.9,0.2-7.3l0-1.8l0-0.9
+	l0-0.5c0-0.6,0-0.4,0-0.6l-0.1-1.5l-0.1-2c0-0.7-0.1-1.3-0.2-1.9c-0.1-0.6-0.1-1.3-0.2-1.9l-0.2-1.9l-0.3-1.9
+	c-0.4-2.5-0.8-4.9-1.4-7.4c-2.3-9.7-6.1-18.9-11-27.2c-5-8.3-11.2-15.6-18.3-21.8c-7-6.2-14.9-11.2-23.1-14.9
+	c-8.3-3.7-16.9-6.1-25.5-7.2c-4.3-0.6-8.6-0.8-12.9-0.7l-1.6,0l-0.4,0c-0.1,0-0.6,0-0.5,0l-0.7,0l-1.6,0.1c-0.6,0-1.2,0.1-1.7,0.1
+	c-2.2,0.2-4.4,0.5-6.5,0.9c-8.6,1.6-16.7,4.7-23.8,9c-7.1,4.3-13.3,9.6-18.3,15.6c-5,6-8.9,12.7-11.6,19.6c-2.7,6.9-4.2,14.1-4.6,21
+	c-0.1,1.7-0.1,3.5-0.1,5.2c0,0.4,0,0.9,0,1.3l0.1,1.4c0.1,0.8,0.1,1.7,0.2,2.5c0.3,3.5,1,6.9,1.9,10.1c1.9,6.5,4.9,12.4,8.6,17.4
+	c3.7,5,8.2,9.1,12.9,12.4c4.7,3.2,9.8,5.5,14.8,7c5,1.5,10,2.1,14.7,2.1c0.6,0,1.2,0,1.7,0c0.3,0,0.6,0,0.9,0c0.3,0,0.6,0,0.9-0.1
+	c0.5,0,1-0.1,1.5-0.1c0.1,0,0.3,0,0.4-0.1l0.5-0.1c0.3,0,0.6-0.1,0.9-0.1c0.6-0.1,1.1-0.2,1.7-0.3c0.6-0.1,1.1-0.2,1.6-0.4
+	c1.1-0.2,2.1-0.6,3.1-0.9c2-0.7,4-1.5,5.7-2.4c1.8-0.9,3.4-2,5-3c0.4-0.3,0.9-0.6,1.3-1c1.6-1.3,1.9-3.7,0.6-5.3
+	c-1.1-1.4-3.1-1.8-4.7-0.9c-0.4,0.2-0.8,0.4-1.2,0.6c-1.4,0.7-2.8,1.3-4.3,1.8c-1.5,0.5-3.1,0.9-4.7,1.2c-0.8,0.1-1.6,0.2-2.5,0.3
+	c-0.4,0-0.8,0.1-1.3,0.1c-0.4,0-0.9,0-1.2,0c-0.4,0-0.8,0-1.2,0c-0.5,0-1,0-1.5-0.1c0,0-0.3,0-0.1,0l-0.2,0l-0.3,0
+	c-0.2,0-0.5,0-0.7-0.1c-0.5-0.1-0.9-0.1-1.4-0.2c-3.7-0.5-7.4-1.6-10.9-3.2c-3.6-1.6-7-3.8-10.1-6.6c-3.1-2.8-5.8-6.1-7.9-9.9
+	c-2.1-3.8-3.6-8-4.3-12.4c-0.3-2.2-0.5-4.5-0.4-6.7c0-0.6,0.1-1.2,0.1-1.8c0,0.2,0-0.1,0-0.1l0-0.2l0-0.5c0-0.3,0.1-0.6,0.1-0.9
+	c0.1-1.2,0.3-2.4,0.5-3.6c1.7-9.6,6.5-19,13.9-26.1c1.9-1.8,3.9-3.4,6-4.9c2.1-1.5,4.4-2.8,6.8-3.9c2.4-1.1,4.8-2,7.4-2.7
+	c2.5-0.7,5.1-1.1,7.8-1.4c1.3-0.1,2.6-0.2,4-0.2c0.4,0,0.6,0,0.9,0l1.1,0l0.7,0c0.3,0,0,0,0.1,0l0.3,0l1.1,0.1
+	c2.9,0.2,5.7,0.6,8.5,1.3c5.6,1.2,11.1,3.3,16.2,6.1c10.2,5.7,18.9,14.5,24.2,25.1c2.7,5.3,4.6,11,5.5,16.9c0.2,1.5,0.4,3,0.5,4.5
+	l0.1,1.1l0.1,1.1c0,0.4,0,0.8,0,1.1c0,0.4,0,0.8,0,1.1l0,1l0,1.1c0,0.7-0.1,1.9-0.1,2.6c-0.1,1.6-0.3,3.3-0.5,4.9
+	c-0.2,1.6-0.5,3.2-0.8,4.8c-0.3,1.6-0.7,3.2-1.1,4.7c-0.8,3.1-1.8,6.2-3,9.3c-2.4,6-5.6,11.8-9.4,17.1
+	c-7.7,10.6-18.2,19.2-30.2,24.7c-6,2.7-12.3,4.7-18.8,5.7c-3.2,0.6-6.5,0.9-9.8,1l-0.6,0l-0.5,0l-1.1,0l-1.6,0l-0.8,0
+	c0.4,0-0.1,0-0.1,0l-0.3,0c-1.8,0-3.5-0.1-5.3-0.3c-7-0.5-13.9-1.8-20.7-3.7c-6.7-1.9-13.2-4.6-19.4-7.8
+	c-12.3-6.6-23.4-15.6-32-26.5c-4.3-5.4-8.1-11.3-11.2-17.4c-3.1-6.1-5.6-12.6-7.4-19.1c-1.8-6.6-2.9-13.3-3.4-20.1l-0.1-1.3l0-0.3
+	l0-0.3l0-0.6l0-1.1l0-0.3l0-0.4l0-0.8l0-1.6l0-0.3c0,0,0,0.1,0-0.1l0-0.6c0-0.8,0-1.7,0-2.5c0.1-3.3,0.4-6.8,0.8-10.2
+	c0.4-3.4,1-6.9,1.7-10.3c0.7-3.4,1.5-6.8,2.5-10.2c1.9-6.7,4.3-13.2,7.1-19.3c5.7-12.2,13.1-23.1,22-31.8c2.2-2.2,4.5-4.2,6.9-6.2
+	c2.4-1.9,4.9-3.7,7.5-5.4c2.5-1.7,5.2-3.2,7.9-4.6c1.3-0.7,2.7-1.4,4.1-2c0.7-0.3,1.4-0.6,2.1-0.9c0.7-0.3,1.4-0.6,2.1-0.9
+	c2.8-1.2,5.7-2.2,8.7-3.1c0.7-0.2,1.5-0.4,2.2-0.7c0.7-0.2,1.5-0.4,2.2-0.6c1.5-0.4,3-0.8,4.5-1.1c0.7-0.2,1.5-0.3,2.3-0.5
+	c0.8-0.2,1.5-0.3,2.3-0.5c0.8-0.1,1.5-0.3,2.3-0.4l1.1-0.2l1.2-0.2c0.8-0.1,1.5-0.2,2.3-0.3c0.9-0.1,1.7-0.2,2.6-0.3
+	c0.7-0.1,1.9-0.2,2.6-0.3c0.5-0.1,1.1-0.1,1.6-0.2l1.1-0.1l0.5-0.1l0.6,0c0.9-0.1,1.7-0.1,2.6-0.2l1.3-0.1c0,0,0.5,0,0.1,0l0.3,0
+	l0.6,0c0.7,0,1.5-0.1,2.2-0.1c2.9-0.1,5.9-0.1,8.8,0c5.8,0.2,11.5,0.9,17,1.9c11.1,2.1,21.5,5.6,31,10.3
+	c9.5,4.6,17.9,10.3,25.3,16.5c0.5,0.4,0.9,0.8,1.4,1.2c0.4,0.4,0.9,0.8,1.3,1.2c0.9,0.8,1.7,1.6,2.6,2.4c0.9,0.8,1.7,1.6,2.5,2.4
+	c0.8,0.8,1.6,1.6,2.4,2.5c3.1,3.3,6,6.6,8.6,10c5.2,6.7,9.4,13.5,12.7,19.9c0.2,0.4,0.4,0.8,0.6,1.2c0.2,0.4,0.4,0.8,0.6,1.2
+	c0.4,0.8,0.8,1.6,1.1,2.4c0.4,0.8,0.7,1.5,1.1,2.3c0.3,0.8,0.7,1.5,1,2.3c1.2,3,2.4,5.9,3.3,8.6c1.5,4.4,2.6,8.3,3.5,11.7
+	c0.3,1.4,1.6,2.3,3,2.1c1.5-0.1,2.6-1.3,2.6-2.8C342.6,170.4,342.5,166.1,342,161.2z"/>
+</svg>

+ 7 - 0
public/app/plugins/datasource/logging/module.ts

@@ -0,0 +1,7 @@
+import Datasource from './datasource';
+
+export class LoggingConfigCtrl {
+  static templateUrl = 'partials/config.html';
+}
+
+export { Datasource, LoggingConfigCtrl as ConfigCtrl };

+ 2 - 0
public/app/plugins/datasource/logging/partials/config.html

@@ -0,0 +1,2 @@
+<datasource-http-settings current="ctrl.current" no-direct-access="true">
+</datasource-http-settings>

+ 28 - 0
public/app/plugins/datasource/logging/plugin.json

@@ -0,0 +1,28 @@
+{
+  "type": "datasource",
+  "name": "Grafana Logging",
+  "id": "logging",
+  "metrics": false,
+  "alerting": false,
+  "annotations": false,
+  "logs": true,
+  "explore": true,
+  "info": {
+    "description": "Grafana Logging Data Source for Grafana",
+    "author": {
+      "name": "Grafana Project",
+      "url": "https://grafana.com"
+    },
+    "logos": {
+      "small": "img/grafana_icon.svg",
+      "large": "img/grafana_icon.svg"
+    },
+    "links": [
+      {
+        "name": "Grafana Logging",
+        "url": "https://grafana.com/"
+      }
+    ],
+    "version": "5.3.0"
+  }
+}

+ 45 - 0
public/app/plugins/datasource/logging/result_transformer.jest.ts

@@ -0,0 +1,45 @@
+import { LogLevel } from 'app/core/logs_model';
+
+import { getLogLevel, getSearchMatches } from './result_transformer';
+
+describe('getSearchMatches()', () => {
+  it('gets no matches for when search and or line are empty', () => {
+    expect(getSearchMatches('', '')).toEqual([]);
+    expect(getSearchMatches('foo', '')).toEqual([]);
+    expect(getSearchMatches('', 'foo')).toEqual([]);
+  });
+
+  it('gets no matches for unmatched search string', () => {
+    expect(getSearchMatches('foo', 'bar')).toEqual([]);
+  });
+
+  it('gets matches for matched search string', () => {
+    expect(getSearchMatches('foo', 'foo')).toEqual([{ length: 3, start: 0, text: 'foo' }]);
+    expect(getSearchMatches(' foo ', 'foo')).toEqual([{ length: 3, start: 1, text: 'foo' }]);
+  });
+
+  expect(getSearchMatches(' foo foo bar ', 'foo|bar')).toEqual([
+    { length: 3, start: 1, text: 'foo' },
+    { length: 3, start: 5, text: 'foo' },
+    { length: 3, start: 9, text: 'bar' },
+  ]);
+});
+
+describe('getLoglevel()', () => {
+  it('returns no log level on empty line', () => {
+    expect(getLogLevel('')).toBe(undefined);
+  });
+
+  it('returns no log level on when level is part of a word', () => {
+    expect(getLogLevel('this is a warning')).toBe(undefined);
+  });
+
+  it('returns log level on line contains a log level', () => {
+    expect(getLogLevel('warn: it is looking bad')).toBe(LogLevel.warn);
+    expect(getLogLevel('2007-12-12 12:12:12 [WARN]: it is looking bad')).toBe(LogLevel.warn);
+  });
+
+  it('returns first log level found', () => {
+    expect(getLogLevel('WARN this could be a debug message')).toBe(LogLevel.warn);
+  });
+});

+ 71 - 0
public/app/plugins/datasource/logging/result_transformer.ts

@@ -0,0 +1,71 @@
+import _ from 'lodash';
+import moment from 'moment';
+
+import { LogLevel, LogsModel, LogRow } from 'app/core/logs_model';
+
+export function getLogLevel(line: string): LogLevel {
+  if (!line) {
+    return undefined;
+  }
+  let level: LogLevel;
+  Object.keys(LogLevel).forEach(key => {
+    if (!level) {
+      const regexp = new RegExp(`\\b${key}\\b`, 'i');
+      if (regexp.test(line)) {
+        level = LogLevel[key];
+      }
+    }
+  });
+  return level;
+}
+
+export function getSearchMatches(line: string, search: string) {
+  // Empty search can send re.exec() into infinite loop, exit early
+  if (!line || !search) {
+    return [];
+  }
+  const regexp = new RegExp(`(?:${search})`, 'g');
+  const matches = [];
+  let match;
+  while ((match = regexp.exec(line))) {
+    matches.push({
+      text: match[0],
+      start: match.index,
+      length: match[0].length,
+    });
+  }
+  return matches;
+}
+
+export function processEntry(entry: { line: string; timestamp: string }, stream): LogRow {
+  const { line, timestamp } = entry;
+  const { labels } = stream;
+  const key = `EK${timestamp}${labels}`;
+  const time = moment(timestamp);
+  const timeFromNow = time.fromNow();
+  const timeLocal = time.format('YYYY-MM-DD HH:mm:ss');
+  const searchMatches = getSearchMatches(line, stream.search);
+  const logLevel = getLogLevel(line);
+
+  return {
+    key,
+    logLevel,
+    searchMatches,
+    timeFromNow,
+    timeLocal,
+    entry: line,
+    timestamp: timestamp,
+  };
+}
+
+export function processStreams(streams, limit?: number): LogsModel {
+  const combinedEntries = streams.reduce((acc, stream) => {
+    return [...acc, ...stream.entries.map(entry => processEntry(entry, stream))];
+  }, []);
+  const sortedEntries = _.chain(combinedEntries)
+    .sortBy('timestamp')
+    .reverse()
+    .slice(0, limit || combinedEntries.length)
+    .value();
+  return { rows: sortedEntries };
+}

+ 3 - 3
public/app/plugins/datasource/opentsdb/datasource.ts

@@ -480,17 +480,17 @@ export default class OpenTsDatasource {
 
 
   mapMetricsToTargets(metrics, options, tsdbVersion) {
   mapMetricsToTargets(metrics, options, tsdbVersion) {
     var interpolatedTagValue, arrTagV;
     var interpolatedTagValue, arrTagV;
-    return _.map(metrics, function(metricData) {
+    return _.map(metrics, metricData => {
       if (tsdbVersion === 3) {
       if (tsdbVersion === 3) {
         return metricData.query.index;
         return metricData.query.index;
       } else {
       } else {
-        return _.findIndex(options.targets, function(target) {
+        return _.findIndex(options.targets, target => {
           if (target.filters && target.filters.length > 0) {
           if (target.filters && target.filters.length > 0) {
             return target.metric === metricData.metric;
             return target.metric === metricData.metric;
           } else {
           } else {
             return (
             return (
               target.metric === metricData.metric &&
               target.metric === metricData.metric &&
-              _.every(target.tags, function(tagV, tagK) {
+              _.every(target.tags, (tagV, tagK) => {
                 interpolatedTagValue = this.templateSrv.replace(tagV, options.scopedVars, 'pipe');
                 interpolatedTagValue = this.templateSrv.replace(tagV, options.scopedVars, 'pipe');
                 arrTagV = interpolatedTagValue.split('|');
                 arrTagV = interpolatedTagValue.split('|');
                 return _.includes(arrTagV, metricData.tags[tagK]) || interpolatedTagValue === '*';
                 return _.includes(arrTagV, metricData.tags[tagK]) || interpolatedTagValue === '*';

+ 28 - 5
public/app/plugins/datasource/prometheus/datasource.ts

@@ -17,11 +17,17 @@ export function alignRange(start, end, step) {
 }
 }
 
 
 export function prometheusRegularEscape(value) {
 export function prometheusRegularEscape(value) {
-  return value.replace(/'/g, "\\\\'");
+  if (typeof value === 'string') {
+    return value.replace(/'/g, "\\\\'");
+  }
+  return value;
 }
 }
 
 
 export function prometheusSpecialRegexEscape(value) {
 export function prometheusSpecialRegexEscape(value) {
-  return prometheusRegularEscape(value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]+?.()]/g, '\\\\$&'));
+  if (typeof value === 'string') {
+    return prometheusRegularEscape(value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]+?.()]/g, '\\\\$&'));
+  }
+  return value;
 }
 }
 
 
 export class PrometheusDatasource {
 export class PrometheusDatasource {
@@ -190,13 +196,14 @@ export class PrometheusDatasource {
     var intervalFactor = target.intervalFactor || 1;
     var intervalFactor = target.intervalFactor || 1;
     // Adjust the interval to take into account any specified minimum and interval factor plus Prometheus limits
     // Adjust the interval to take into account any specified minimum and interval factor plus Prometheus limits
     var adjustedInterval = this.adjustInterval(interval, minInterval, range, intervalFactor);
     var adjustedInterval = this.adjustInterval(interval, minInterval, range, intervalFactor);
-    var scopedVars = options.scopedVars;
+    var scopedVars = { ...options.scopedVars, ...this.getRangeScopedVars() };
     // If the interval was adjusted, make a shallow copy of scopedVars with updated interval vars
     // If the interval was adjusted, make a shallow copy of scopedVars with updated interval vars
     if (interval !== adjustedInterval) {
     if (interval !== adjustedInterval) {
       interval = adjustedInterval;
       interval = adjustedInterval;
       scopedVars = Object.assign({}, options.scopedVars, {
       scopedVars = Object.assign({}, options.scopedVars, {
         __interval: { text: interval + 's', value: interval + 's' },
         __interval: { text: interval + 's', value: interval + 's' },
-        __interval_ms: { text: String(interval * 1000), value: String(interval * 1000) },
+        __interval_ms: { text: interval * 1000, value: interval * 1000 },
+        ...this.getRangeScopedVars(),
       });
       });
     }
     }
     query.step = interval;
     query.step = interval;
@@ -279,11 +286,26 @@ export class PrometheusDatasource {
       return this.$q.when([]);
       return this.$q.when([]);
     }
     }
 
 
-    let interpolated = this.templateSrv.replace(query, {}, this.interpolateQueryExpr);
+    let scopedVars = {
+      __interval: { text: this.interval, value: this.interval },
+      __interval_ms: { text: kbn.interval_to_ms(this.interval), value: kbn.interval_to_ms(this.interval) },
+      ...this.getRangeScopedVars(),
+    };
+    let interpolated = this.templateSrv.replace(query, scopedVars, this.interpolateQueryExpr);
     var metricFindQuery = new PrometheusMetricFindQuery(this, interpolated, this.timeSrv);
     var metricFindQuery = new PrometheusMetricFindQuery(this, interpolated, this.timeSrv);
     return metricFindQuery.process();
     return metricFindQuery.process();
   }
   }
 
 
+  getRangeScopedVars() {
+    let range = this.timeSrv.timeRange();
+    let msRange = range.to.diff(range.from);
+    let regularRange = kbn.secondsToHms(msRange / 1000);
+    return {
+      __range_ms: { text: msRange, value: msRange },
+      __range: { text: regularRange, value: regularRange },
+    };
+  }
+
   annotationQuery(options) {
   annotationQuery(options) {
     var annotation = options.annotation;
     var annotation = options.annotation;
     var expr = annotation.expr || '';
     var expr = annotation.expr || '';
@@ -357,6 +379,7 @@ export class PrometheusDatasource {
       state = {
       state = {
         ...state,
         ...state,
         queries,
         queries,
+        datasource: this.name,
       };
       };
     }
     }
     return state;
     return state;

+ 21 - 9
public/app/plugins/datasource/prometheus/plugin.json

@@ -2,21 +2,30 @@
   "type": "datasource",
   "type": "datasource",
   "name": "Prometheus",
   "name": "Prometheus",
   "id": "prometheus",
   "id": "prometheus",
-
   "includes": [
   "includes": [
-    {"type": "dashboard", "name": "Prometheus Stats", "path": "dashboards/prometheus_stats.json"},
-    {"type": "dashboard", "name": "Prometheus 2.0 Stats", "path": "dashboards/prometheus_2_stats.json"},
-    {"type": "dashboard", "name": "Grafana Stats", "path": "dashboards/grafana_stats.json"}
+    {
+      "type": "dashboard",
+      "name": "Prometheus Stats",
+      "path": "dashboards/prometheus_stats.json"
+    },
+    {
+      "type": "dashboard",
+      "name": "Prometheus 2.0 Stats",
+      "path": "dashboards/prometheus_2_stats.json"
+    },
+    {
+      "type": "dashboard",
+      "name": "Grafana Stats",
+      "path": "dashboards/grafana_stats.json"
+    }
   ],
   ],
-
   "metrics": true,
   "metrics": true,
   "alerting": true,
   "alerting": true,
   "annotations": true,
   "annotations": true,
-
+  "explore": true,
   "queryOptions": {
   "queryOptions": {
     "minInterval": true
     "minInterval": true
   },
   },
-
   "info": {
   "info": {
     "description": "Prometheus Data Source for Grafana",
     "description": "Prometheus Data Source for Grafana",
     "author": {
     "author": {
@@ -28,8 +37,11 @@
       "large": "img/prometheus_logo.svg"
       "large": "img/prometheus_logo.svg"
     },
     },
     "links": [
     "links": [
-      {"name": "Prometheus", "url": "https://prometheus.io/"}
+      {
+        "name": "Prometheus",
+        "url": "https://prometheus.io/"
+      }
     ],
     ],
     "version": "5.0.0"
     "version": "5.0.0"
   }
   }
-}
+}

+ 45 - 1
public/app/plugins/datasource/prometheus/specs/datasource.jest.ts

@@ -2,6 +2,7 @@ import _ from 'lodash';
 import moment from 'moment';
 import moment from 'moment';
 import q from 'q';
 import q from 'q';
 import { alignRange, PrometheusDatasource, prometheusSpecialRegexEscape, prometheusRegularEscape } from '../datasource';
 import { alignRange, PrometheusDatasource, prometheusSpecialRegexEscape, prometheusRegularEscape } from '../datasource';
+jest.mock('../metric_find_query');
 
 
 describe('PrometheusDatasource', () => {
 describe('PrometheusDatasource', () => {
   let ctx: any = {};
   let ctx: any = {};
@@ -18,7 +19,14 @@ describe('PrometheusDatasource', () => {
   ctx.templateSrvMock = {
   ctx.templateSrvMock = {
     replace: a => a,
     replace: a => a,
   };
   };
-  ctx.timeSrvMock = {};
+  ctx.timeSrvMock = {
+    timeRange: () => {
+      return {
+        from: moment(1531468681),
+        to: moment(1531489712),
+      };
+    },
+  };
 
 
   beforeEach(() => {
   beforeEach(() => {
     ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock);
     ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock);
@@ -166,6 +174,9 @@ describe('PrometheusDatasource', () => {
   });
   });
 
 
   describe('Prometheus regular escaping', function() {
   describe('Prometheus regular escaping', function() {
+    it('should not escape non-string', function() {
+      expect(prometheusRegularEscape(12)).toEqual(12);
+    });
     it('should not escape simple string', function() {
     it('should not escape simple string', function() {
       expect(prometheusRegularEscape('cryptodepression')).toEqual('cryptodepression');
       expect(prometheusRegularEscape('cryptodepression')).toEqual('cryptodepression');
     });
     });
@@ -201,4 +212,37 @@ describe('PrometheusDatasource', () => {
       expect(prometheusSpecialRegexEscape('+looking$glass?')).toEqual('\\\\+looking\\\\$glass\\\\?');
       expect(prometheusSpecialRegexEscape('+looking$glass?')).toEqual('\\\\+looking\\\\$glass\\\\?');
     });
     });
   });
   });
+
+  describe('metricFindQuery', () => {
+    beforeEach(() => {
+      let query = 'query_result(topk(5,rate(http_request_duration_microseconds_count[$__interval])))';
+      ctx.templateSrvMock.replace = jest.fn();
+      ctx.timeSrvMock.timeRange = () => {
+        return {
+          from: moment(1531468681),
+          to: moment(1531489712),
+        };
+      };
+      ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock);
+      ctx.ds.metricFindQuery(query);
+    });
+
+    it('should call templateSrv.replace with scopedVars', () => {
+      expect(ctx.templateSrvMock.replace.mock.calls[0][1]).toBeDefined();
+    });
+
+    it('should have the correct range and range_ms', () => {
+      let range = ctx.templateSrvMock.replace.mock.calls[0][1].__range;
+      let rangeMs = ctx.templateSrvMock.replace.mock.calls[0][1].__range_ms;
+      expect(range).toEqual({ text: '21s', value: '21s' });
+      expect(rangeMs).toEqual({ text: 21031, value: 21031 });
+    });
+
+    it('should pass the default interval value', () => {
+      let interval = ctx.templateSrvMock.replace.mock.calls[0][1].__interval;
+      let intervalMs = ctx.templateSrvMock.replace.mock.calls[0][1].__interval_ms;
+      expect(interval).toEqual({ text: '15s', value: '15s' });
+      expect(intervalMs).toEqual({ text: 15000, value: 15000 });
+    });
+  });
 });
 });

+ 18 - 18
public/app/plugins/datasource/prometheus/specs/datasource_specs.ts

@@ -452,7 +452,7 @@ describe('PrometheusDatasource', function() {
         interval: '10s',
         interval: '10s',
         scopedVars: {
         scopedVars: {
           __interval: { text: '10s', value: '10s' },
           __interval: { text: '10s', value: '10s' },
-          __interval_ms: { text: String(10 * 1000), value: String(10 * 1000) },
+          __interval_ms: { text: 10 * 1000, value: 10 * 1000 },
         },
         },
       };
       };
       var urlExpected =
       var urlExpected =
@@ -463,8 +463,8 @@ describe('PrometheusDatasource', function() {
 
 
       expect(query.scopedVars.__interval.text).to.be('10s');
       expect(query.scopedVars.__interval.text).to.be('10s');
       expect(query.scopedVars.__interval.value).to.be('10s');
       expect(query.scopedVars.__interval.value).to.be('10s');
-      expect(query.scopedVars.__interval_ms.text).to.be(String(10 * 1000));
-      expect(query.scopedVars.__interval_ms.value).to.be(String(10 * 1000));
+      expect(query.scopedVars.__interval_ms.text).to.be(10 * 1000);
+      expect(query.scopedVars.__interval_ms.value).to.be(10 * 1000);
     });
     });
     it('should be min interval when it is greater than auto interval', function() {
     it('should be min interval when it is greater than auto interval', function() {
       var query = {
       var query = {
@@ -479,7 +479,7 @@ describe('PrometheusDatasource', function() {
         interval: '5s',
         interval: '5s',
         scopedVars: {
         scopedVars: {
           __interval: { text: '5s', value: '5s' },
           __interval: { text: '5s', value: '5s' },
-          __interval_ms: { text: String(5 * 1000), value: String(5 * 1000) },
+          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
         },
         },
       };
       };
       var urlExpected =
       var urlExpected =
@@ -490,8 +490,8 @@ describe('PrometheusDatasource', function() {
 
 
       expect(query.scopedVars.__interval.text).to.be('5s');
       expect(query.scopedVars.__interval.text).to.be('5s');
       expect(query.scopedVars.__interval.value).to.be('5s');
       expect(query.scopedVars.__interval.value).to.be('5s');
-      expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000));
-      expect(query.scopedVars.__interval_ms.value).to.be(String(5 * 1000));
+      expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
+      expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
     });
     });
     it('should account for intervalFactor', function() {
     it('should account for intervalFactor', function() {
       var query = {
       var query = {
@@ -507,7 +507,7 @@ describe('PrometheusDatasource', function() {
         interval: '10s',
         interval: '10s',
         scopedVars: {
         scopedVars: {
           __interval: { text: '10s', value: '10s' },
           __interval: { text: '10s', value: '10s' },
-          __interval_ms: { text: String(10 * 1000), value: String(10 * 1000) },
+          __interval_ms: { text: 10 * 1000, value: 10 * 1000 },
         },
         },
       };
       };
       var urlExpected =
       var urlExpected =
@@ -518,8 +518,8 @@ describe('PrometheusDatasource', function() {
 
 
       expect(query.scopedVars.__interval.text).to.be('10s');
       expect(query.scopedVars.__interval.text).to.be('10s');
       expect(query.scopedVars.__interval.value).to.be('10s');
       expect(query.scopedVars.__interval.value).to.be('10s');
-      expect(query.scopedVars.__interval_ms.text).to.be(String(10 * 1000));
-      expect(query.scopedVars.__interval_ms.value).to.be(String(10 * 1000));
+      expect(query.scopedVars.__interval_ms.text).to.be(10 * 1000);
+      expect(query.scopedVars.__interval_ms.value).to.be(10 * 1000);
     });
     });
     it('should be interval * intervalFactor when greater than min interval', function() {
     it('should be interval * intervalFactor when greater than min interval', function() {
       var query = {
       var query = {
@@ -535,7 +535,7 @@ describe('PrometheusDatasource', function() {
         interval: '5s',
         interval: '5s',
         scopedVars: {
         scopedVars: {
           __interval: { text: '5s', value: '5s' },
           __interval: { text: '5s', value: '5s' },
-          __interval_ms: { text: String(5 * 1000), value: String(5 * 1000) },
+          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
         },
         },
       };
       };
       var urlExpected =
       var urlExpected =
@@ -546,8 +546,8 @@ describe('PrometheusDatasource', function() {
 
 
       expect(query.scopedVars.__interval.text).to.be('5s');
       expect(query.scopedVars.__interval.text).to.be('5s');
       expect(query.scopedVars.__interval.value).to.be('5s');
       expect(query.scopedVars.__interval.value).to.be('5s');
-      expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000));
-      expect(query.scopedVars.__interval_ms.value).to.be(String(5 * 1000));
+      expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
+      expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
     });
     });
     it('should be min interval when greater than interval * intervalFactor', function() {
     it('should be min interval when greater than interval * intervalFactor', function() {
       var query = {
       var query = {
@@ -563,7 +563,7 @@ describe('PrometheusDatasource', function() {
         interval: '5s',
         interval: '5s',
         scopedVars: {
         scopedVars: {
           __interval: { text: '5s', value: '5s' },
           __interval: { text: '5s', value: '5s' },
-          __interval_ms: { text: String(5 * 1000), value: String(5 * 1000) },
+          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
         },
         },
       };
       };
       var urlExpected =
       var urlExpected =
@@ -574,8 +574,8 @@ describe('PrometheusDatasource', function() {
 
 
       expect(query.scopedVars.__interval.text).to.be('5s');
       expect(query.scopedVars.__interval.text).to.be('5s');
       expect(query.scopedVars.__interval.value).to.be('5s');
       expect(query.scopedVars.__interval.value).to.be('5s');
-      expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000));
-      expect(query.scopedVars.__interval_ms.value).to.be(String(5 * 1000));
+      expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
+      expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
     });
     });
     it('should be determined by the 11000 data points limit, accounting for intervalFactor', function() {
     it('should be determined by the 11000 data points limit, accounting for intervalFactor', function() {
       var query = {
       var query = {
@@ -590,7 +590,7 @@ describe('PrometheusDatasource', function() {
         interval: '5s',
         interval: '5s',
         scopedVars: {
         scopedVars: {
           __interval: { text: '5s', value: '5s' },
           __interval: { text: '5s', value: '5s' },
-          __interval_ms: { text: String(5 * 1000), value: String(5 * 1000) },
+          __interval_ms: { text: 5 * 1000, value: 5 * 1000 },
         },
         },
       };
       };
       var end = 7 * 24 * 60 * 60;
       var end = 7 * 24 * 60 * 60;
@@ -609,8 +609,8 @@ describe('PrometheusDatasource', function() {
 
 
       expect(query.scopedVars.__interval.text).to.be('5s');
       expect(query.scopedVars.__interval.text).to.be('5s');
       expect(query.scopedVars.__interval.value).to.be('5s');
       expect(query.scopedVars.__interval.value).to.be('5s');
-      expect(query.scopedVars.__interval_ms.text).to.be(String(5 * 1000));
-      expect(query.scopedVars.__interval_ms.value).to.be(String(5 * 1000));
+      expect(query.scopedVars.__interval_ms.text).to.be(5 * 1000);
+      expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000);
     });
     });
   });
   });
 });
 });

+ 0 - 0
public/app/plugins/app/testdata/datasource/datasource.ts → public/app/plugins/datasource/testdata/datasource.ts


+ 0 - 0
public/app/plugins/app/testdata/datasource/module.ts → public/app/plugins/datasource/testdata/module.ts


+ 0 - 1
public/app/plugins/app/testdata/partials/query.editor.html → public/app/plugins/datasource/testdata/partials/query.editor.html

@@ -37,4 +37,3 @@
 		</div>
 		</div>
 	</div>
 	</div>
 </query-editor-row>
 </query-editor-row>
-

+ 4 - 4
public/app/plugins/app/testdata/datasource/plugin.json → public/app/plugins/datasource/testdata/plugin.json

@@ -1,7 +1,7 @@
 {
 {
   "type": "datasource",
   "type": "datasource",
-  "name": "Grafana TestDataDB",
-  "id": "grafana-testdata-datasource",
+  "name": "TestData DB",
+  "id": "testdata",
 
 
   "metrics": true,
   "metrics": true,
   "alerting": true,
   "alerting": true,
@@ -13,8 +13,8 @@
       "url": "https://grafana.com"
       "url": "https://grafana.com"
     },
     },
     "logos": {
     "logos": {
-      "small": "",
-      "large": ""
+      "small": "../../../../img/grafana_icon.svg",
+      "large": "../../../../img/grafana_icon.svg"
     }
     }
   }
   }
 }
 }

+ 0 - 0
public/app/plugins/app/testdata/datasource/query_ctrl.ts → public/app/plugins/datasource/testdata/query_ctrl.ts


+ 0 - 604
public/app/plugins/panel/graph/jquery.flot.events.js

@@ -1,604 +0,0 @@
-define([
-  'jquery',
-  'lodash',
-  'angular',
-  'tether-drop',
-],
-function ($, _, angular, Drop) {
-  'use strict';
-
-  function createAnnotationToolip(element, event, plot) {
-    var injector = angular.element(document).injector();
-    var content = document.createElement('div');
-    content.innerHTML = '<annotation-tooltip event="event" on-edit="onEdit()"></annotation-tooltip>';
-
-    injector.invoke(["$compile", "$rootScope", function($compile, $rootScope) {
-      var eventManager = plot.getOptions().events.manager;
-      var tmpScope = $rootScope.$new(true);
-      tmpScope.event = event;
-      tmpScope.onEdit = function() {
-        eventManager.editEvent(event);
-      };
-
-      $compile(content)(tmpScope);
-      tmpScope.$digest();
-      tmpScope.$destroy();
-
-      var drop = new Drop({
-        target: element[0],
-        content: content,
-        position: "bottom center",
-        classes: 'drop-popover drop-popover--annotation',
-        openOn: 'hover',
-        hoverCloseDelay: 200,
-        tetherOptions: {
-          constraints: [{to: 'window', pin: true, attachment: "both"}]
-        }
-      });
-
-      drop.open();
-
-      drop.on('close', function() {
-        setTimeout(function() {
-          drop.destroy();
-        });
-      });
-    }]);
-  }
-
-  var markerElementToAttachTo = null;
-
-  function createEditPopover(element, event, plot) {
-    var eventManager = plot.getOptions().events.manager;
-    if (eventManager.editorOpen) {
-      // update marker element to attach to (needed in case of legend on the right
-      // when there is a double render pass and the initial marker element is removed)
-      markerElementToAttachTo = element;
-      return;
-    }
-
-    // mark as openend
-    eventManager.editorOpened();
-    // set marker element to attache to
-    markerElementToAttachTo = element;
-
-    // wait for element to be attached and positioned
-    setTimeout(function() {
-
-      var injector = angular.element(document).injector();
-      var content = document.createElement('div');
-      content.innerHTML = '<event-editor panel-ctrl="panelCtrl" event="event" close="close()"></event-editor>';
-
-      injector.invoke(["$compile", "$rootScope", function($compile, $rootScope) {
-        var scope = $rootScope.$new(true);
-        var drop;
-
-        scope.event = event;
-        scope.panelCtrl = eventManager.panelCtrl;
-        scope.close = function() {
-          drop.close();
-        };
-
-        $compile(content)(scope);
-        scope.$digest();
-
-        drop = new Drop({
-          target: markerElementToAttachTo[0],
-          content: content,
-          position: "bottom center",
-          classes: 'drop-popover drop-popover--form',
-          openOn: 'click',
-          tetherOptions: {
-            constraints: [{to: 'window', pin: true, attachment: "both"}]
-          }
-        });
-
-        drop.open();
-        eventManager.editorOpened();
-
-        drop.on('close', function() {
-          // need timeout here in order call drop.destroy
-          setTimeout(function() {
-            eventManager.editorClosed();
-            scope.$destroy();
-            drop.destroy();
-          });
-        });
-      }]);
-
-    }, 100);
-  }
-
-  /*
-   * jquery.flot.events
-   *
-   * description: Flot plugin for adding events/markers to the plot
-   * version: 0.2.5
-   * authors:
-   *    Alexander Wunschik <alex@wunschik.net>
-   *    Joel Oughton <joeloughton@gmail.com>
-   *    Nicolas Joseph <www.nicolasjoseph.com>
-   *
-   * website: https://github.com/mojoaxel/flot-events
-   *
-   * released under MIT License and GPLv2+
-   */
-
-  /**
-   * A class that allows for the drawing an remove of some object
-   */
-  var DrawableEvent = function(object, drawFunc, clearFunc, moveFunc, left, top, width, height) {
-    var _object = object;
-    var	_drawFunc = drawFunc;
-    var	_clearFunc = clearFunc;
-    var	_moveFunc = moveFunc;
-    var	_position = { left: left, top: top };
-    var	_width = width;
-    var	_height = height;
-
-    this.width = function() { return _width; };
-    this.height = function() { return _height; };
-    this.position = function() { return _position; };
-    this.draw = function() { _drawFunc(_object); };
-    this.clear = function() { _clearFunc(_object); };
-    this.getObject = function() { return _object; };
-    this.moveTo = function(position) {
-      _position = position;
-      _moveFunc(_object, _position);
-    };
-  };
-
-  /**
-   * Event class that stores options (eventType, min, max, title, description) and the object to draw.
-   */
-  var VisualEvent = function(options, drawableEvent) {
-    var _parent;
-    var _options = options;
-    var _drawableEvent = drawableEvent;
-    var _hidden = false;
-
-    this.visual = function() { return _drawableEvent; };
-    this.getOptions = function() { return _options; };
-    this.getParent = function() { return _parent; };
-    this.isHidden = function() { return _hidden; };
-    this.hide = function() { _hidden = true; };
-    this.unhide = function() { _hidden = false; };
-  };
-
-  /**
-   * A Class that handles the event-markers inside the given plot
-   */
-  var EventMarkers = function(plot) {
-    var _events = [];
-
-    this._types = [];
-    this._plot = plot;
-    this.eventsEnabled = false;
-
-    this.getEvents = function() {
-      return _events;
-    };
-
-    this.setTypes = function(types) {
-      return this._types = types;
-    };
-
-    /**
-     * create internal objects for the given events
-     */
-    this.setupEvents = function(events) {
-      var that = this;
-      var parts = _.partition(events, 'isRegion');
-      var regions = parts[0];
-      events = parts[1];
-
-      $.each(events, function(index, event) {
-        var ve = new VisualEvent(event, that._buildDiv(event));
-        _events.push(ve);
-      });
-
-      $.each(regions, function (index, event) {
-        var vre = new VisualEvent(event, that._buildRegDiv(event));
-        _events.push(vre);
-      });
-
-      _events.sort(function(a, b) {
-        var ao = a.getOptions(), bo = b.getOptions();
-        if (ao.min > bo.min) { return 1; }
-        if (ao.min < bo.min) { return -1; }
-        return 0;
-      });
-    };
-
-    /**
-     * draw the events to the plot
-     */
-    this.drawEvents = function() {
-      var that = this;
-      // var o = this._plot.getPlotOffset();
-
-      $.each(_events, function(index, event) {
-        // check event is inside the graph range
-        if (that._insidePlot(event.getOptions().min) && !event.isHidden()) {
-          event.visual().draw();
-        }  else {
-          event.visual().getObject().hide();
-        }
-      });
-    };
-
-    /**
-     * update the position of the event-markers (e.g. after scrolling or zooming)
-     */
-    this.updateEvents = function() {
-      var that = this;
-      var o = this._plot.getPlotOffset(), left, top;
-      var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
-
-      $.each(_events, function(index, event) {
-        top = o.top + that._plot.height() - event.visual().height();
-        left = xaxis.p2c(event.getOptions().min) + o.left - event.visual().width() / 2;
-        event.visual().moveTo({ top: top, left: left });
-      });
-    };
-
-    /**
-     * remove all events from the plot
-     */
-    this._clearEvents = function() {
-      $.each(_events, function(index, val) {
-        val.visual().clear();
-      });
-      _events = [];
-    };
-
-    /**
-     * create a DOM element for the given event
-     */
-    this._buildDiv = function(event) {
-      var that = this;
-
-      var container = this._plot.getPlaceholder();
-      var o = this._plot.getPlotOffset();
-      var axes = this._plot.getAxes();
-      var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
-      var yaxis, top, left, color, markerSize, markerShow, lineStyle, lineWidth;
-      var markerTooltip;
-
-      // determine the y axis used
-      if (axes.yaxis && axes.yaxis.used) { yaxis = axes.yaxis; }
-      if (axes.yaxis2 && axes.yaxis2.used) { yaxis = axes.yaxis2; }
-
-      // map the eventType to a types object
-      var eventTypeId = event.eventType;
-
-      if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) {
-        color = '#666';
-      } else {
-        color = this._types[eventTypeId].color;
-      }
-
-      if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].markerSize) {
-        markerSize = 8; //default marker size
-      } else {
-        markerSize = this._types[eventTypeId].markerSize;
-      }
-
-      if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerShow === undefined) {
-        markerShow = true;
-      } else {
-        markerShow = this._types[eventTypeId].markerShow;
-      }
-
-      if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) {
-        markerTooltip = true;
-      } else {
-        markerTooltip = this._types[eventTypeId].markerTooltip;
-      }
-
-      if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) {
-        lineStyle = 'dashed'; //default line style
-      } else {
-        lineStyle = this._types[eventTypeId].lineStyle.toLowerCase();
-      }
-
-      if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) {
-        lineWidth = 1; //default line width
-      } else {
-        lineWidth = this._types[eventTypeId].lineWidth;
-      }
-
-      var topOffset = xaxis.options.eventSectionHeight || 0;
-      topOffset = topOffset / 3;
-
-      top = o.top + this._plot.height() + topOffset;
-      left = xaxis.p2c(event.min) + o.left;
-
-      var line = $('<div class="events_line flot-temp-elem"></div>').css({
-        "position": "absolute",
-        "opacity": 0.8,
-        "left": left + 'px',
-        "top": 8,
-        "width": lineWidth + "px",
-        "height": this._plot.height() + topOffset * 0.8,
-        "border-left-width": lineWidth + "px",
-        "border-left-style": lineStyle,
-        "border-left-color": color,
-        "color": color
-      })
-      .appendTo(container);
-
-      if (markerShow) {
-        var marker = $('<div class="events_marker"></div>').css({
-          "position": "absolute",
-          "left": (-markerSize - Math.round(lineWidth / 2)) + "px",
-          "font-size": 0,
-          "line-height": 0,
-          "width": 0,
-          "height": 0,
-          "border-left": markerSize+"px solid transparent",
-          "border-right": markerSize+"px solid transparent"
-        });
-
-        marker.appendTo(line);
-
-        if (this._types[eventTypeId] && this._types[eventTypeId].position && this._types[eventTypeId].position.toUpperCase() === 'BOTTOM') {
-          marker.css({
-            "top": top-markerSize-8 +"px",
-            "border-top": "none",
-            "border-bottom": markerSize+"px solid " + color
-          });
-        } else {
-          marker.css({
-            "top": "0px",
-            "border-top": markerSize+"px solid " + color,
-            "border-bottom": "none"
-          });
-        }
-
-        marker.data({
-          "event": event
-        });
-
-        var mouseenter = function() {
-          createAnnotationToolip(marker, $(this).data("event"), that._plot);
-        };
-
-        if (event.editModel) {
-          createEditPopover(marker, event.editModel, that._plot);
-        }
-
-        var mouseleave = function() {
-          that._plot.clearSelection();
-        };
-
-        if (markerTooltip) {
-          marker.css({ "cursor": "help" });
-          marker.hover(mouseenter, mouseleave);
-        }
-      }
-
-      var drawableEvent = new DrawableEvent(
-        line,
-        function drawFunc(obj) { obj.show(); },
-        function(obj) { obj.remove(); },
-        function(obj, position) {
-          obj.css({
-            top: position.top,
-            left: position.left
-          });
-        },
-        left,
-        top,
-        line.width(),
-        line.height()
-      );
-
-      return drawableEvent;
-    };
-
-    /**
-     * create a DOM element for the given region
-     */
-    this._buildRegDiv = function (event) {
-      var that = this;
-
-      var container = this._plot.getPlaceholder();
-      var o = this._plot.getPlotOffset();
-      var axes = this._plot.getAxes();
-      var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
-      var yaxis, top, left, lineWidth, regionWidth, lineStyle, color, markerTooltip;
-
-      // determine the y axis used
-      if (axes.yaxis && axes.yaxis.used) { yaxis = axes.yaxis; }
-      if (axes.yaxis2 && axes.yaxis2.used) { yaxis = axes.yaxis2; }
-
-      // map the eventType to a types object
-      var eventTypeId = event.eventType;
-
-      if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) {
-        color = '#666';
-      } else {
-        color = this._types[eventTypeId].color;
-      }
-
-      if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) {
-        markerTooltip = true;
-      } else {
-        markerTooltip = this._types[eventTypeId].markerTooltip;
-      }
-
-      if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) {
-        lineWidth = 1; //default line width
-      } else {
-        lineWidth = this._types[eventTypeId].lineWidth;
-      }
-
-      if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) {
-        lineStyle = 'dashed'; //default line style
-      } else {
-        lineStyle = this._types[eventTypeId].lineStyle.toLowerCase();
-      }
-
-      var topOffset = 2;
-      top = o.top + this._plot.height() + topOffset;
-
-      var timeFrom = Math.min(event.min, event.timeEnd);
-      var timeTo = Math.max(event.min, event.timeEnd);
-      left = xaxis.p2c(timeFrom) + o.left;
-      var right = xaxis.p2c(timeTo) + o.left;
-      regionWidth = right - left;
-
-      _.each([left, right], function(position) {
-        var line = $('<div class="events_line flot-temp-elem"></div>').css({
-          "position": "absolute",
-          "opacity": 0.8,
-          "left": position + 'px',
-          "top": 8,
-          "width": lineWidth + "px",
-          "height": that._plot.height() + topOffset,
-          "border-left-width": lineWidth + "px",
-          "border-left-style": lineStyle,
-          "border-left-color": color,
-          "color": color
-        });
-        line.appendTo(container);
-      });
-
-      var region = $('<div class="events_marker region_marker flot-temp-elem"></div>').css({
-        "position": "absolute",
-        "opacity": 0.5,
-        "left": left + 'px',
-        "top": top,
-        "width": Math.round(regionWidth + lineWidth) + "px",
-        "height": "0.5rem",
-        "border-left-color": color,
-        "color": color,
-        "background-color": color
-      });
-      region.appendTo(container);
-
-      region.data({
-        "event": event
-      });
-
-      var mouseenter = function () {
-        createAnnotationToolip(region, $(this).data("event"), that._plot);
-      };
-
-      if (event.editModel) {
-        createEditPopover(region, event.editModel, that._plot);
-      }
-
-      var mouseleave = function () {
-        that._plot.clearSelection();
-      };
-
-      if (markerTooltip) {
-        region.css({ "cursor": "help" });
-        region.hover(mouseenter, mouseleave);
-      }
-
-      var drawableEvent = new DrawableEvent(
-        region,
-        function drawFunc(obj) { obj.show(); },
-        function (obj) { obj.remove(); },
-        function (obj, position) {
-          obj.css({
-            top: position.top,
-            left: position.left
-          });
-        },
-        left,
-        top,
-        region.width(),
-        region.height()
-      );
-
-      return drawableEvent;
-    };
-
-    /**
-     * check if the event is inside visible range
-     */
-    this._insidePlot = function(x) {
-      var xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
-      var xc = xaxis.p2c(x);
-      return xc > 0 && xc < xaxis.p2c(xaxis.max);
-    };
-  };
-
-  /**
-   * initialize the plugin for the given plot
-   */
-  function init(plot) {
-    /*jshint validthis:true */
-    var that = this;
-    var eventMarkers = new EventMarkers(plot);
-
-    plot.getEvents = function() {
-      return eventMarkers._events;
-    };
-
-    plot.hideEvents = function() {
-      $.each(eventMarkers._events, function(index, event) {
-        event.visual().getObject().hide();
-      });
-    };
-
-    plot.showEvents = function() {
-      plot.hideEvents();
-      $.each(eventMarkers._events, function(index, event) {
-        event.hide();
-      });
-
-      that.eventMarkers.drawEvents();
-    };
-
-    // change events on an existing plot
-    plot.setEvents = function(events) {
-      if (eventMarkers.eventsEnabled) {
-        eventMarkers.setupEvents(events);
-      }
-    };
-
-    plot.hooks.processOptions.push(function(plot, options) {
-      // enable the plugin
-      if (options.events.data != null) {
-        eventMarkers.eventsEnabled = true;
-      }
-    });
-
-    plot.hooks.draw.push(function(plot) {
-      var options = plot.getOptions();
-
-      if (eventMarkers.eventsEnabled) {
-        // check for first run
-        if (eventMarkers.getEvents().length < 1) {
-          eventMarkers.setTypes(options.events.types);
-          eventMarkers.setupEvents(options.events.data);
-        } else {
-          eventMarkers.updateEvents();
-        }
-      }
-
-      eventMarkers.drawEvents();
-    });
-  }
-
-  var defaultOptions = {
-    events: {
-      data: null,
-      types: null,
-      xaxis: 1,
-      position: 'BOTTOM'
-    }
-  };
-
-  $.plot.plugins.push({
-    init: init,
-    options: defaultOptions,
-    name: "events",
-    version: "0.2.5"
-  });
-});

+ 671 - 0
public/app/plugins/panel/graph/jquery.flot.events.ts

@@ -0,0 +1,671 @@
+import angular from 'angular';
+import $ from 'jquery';
+import _ from 'lodash';
+import Drop from 'tether-drop';
+
+/** @ngInject */
+export function createAnnotationToolip(element, event, plot) {
+  let injector = angular.element(document).injector();
+  let content = document.createElement('div');
+  content.innerHTML = '<annotation-tooltip event="event" on-edit="onEdit()"></annotation-tooltip>';
+
+  injector.invoke([
+    '$compile',
+    '$rootScope',
+    function($compile, $rootScope) {
+      let eventManager = plot.getOptions().events.manager;
+      let tmpScope = $rootScope.$new(true);
+      tmpScope.event = event;
+      tmpScope.onEdit = function() {
+        eventManager.editEvent(event);
+      };
+
+      $compile(content)(tmpScope);
+      tmpScope.$digest();
+      tmpScope.$destroy();
+
+      let drop = new Drop({
+        target: element[0],
+        content: content,
+        position: 'bottom center',
+        classes: 'drop-popover drop-popover--annotation',
+        openOn: 'hover',
+        hoverCloseDelay: 200,
+        tetherOptions: {
+          constraints: [{ to: 'window', pin: true, attachment: 'both' }],
+        },
+      });
+
+      drop.open();
+
+      drop.on('close', function() {
+        setTimeout(function() {
+          drop.destroy();
+        });
+      });
+    },
+  ]);
+}
+
+let markerElementToAttachTo = null;
+
+/** @ngInject */
+export function createEditPopover(element, event, plot) {
+  let eventManager = plot.getOptions().events.manager;
+  if (eventManager.editorOpen) {
+    // update marker element to attach to (needed in case of legend on the right
+    // when there is a double render pass and the inital marker element is removed)
+    markerElementToAttachTo = element;
+    return;
+  }
+
+  // mark as openend
+  eventManager.editorOpened();
+  // set marker elment to attache to
+  markerElementToAttachTo = element;
+
+  // wait for element to be attached and positioned
+  setTimeout(function() {
+    let injector = angular.element(document).injector();
+    let content = document.createElement('div');
+    content.innerHTML = '<event-editor panel-ctrl="panelCtrl" event="event" close="close()"></event-editor>';
+
+    injector.invoke([
+      '$compile',
+      '$rootScope',
+      function($compile, $rootScope) {
+        let scope = $rootScope.$new(true);
+        let drop;
+
+        scope.event = event;
+        scope.panelCtrl = eventManager.panelCtrl;
+        scope.close = function() {
+          drop.close();
+        };
+
+        $compile(content)(scope);
+        scope.$digest();
+
+        drop = new Drop({
+          target: markerElementToAttachTo[0],
+          content: content,
+          position: 'bottom center',
+          classes: 'drop-popover drop-popover--form',
+          openOn: 'click',
+          tetherOptions: {
+            constraints: [{ to: 'window', pin: true, attachment: 'both' }],
+          },
+        });
+
+        drop.open();
+        eventManager.editorOpened();
+
+        drop.on('close', function() {
+          // need timeout here in order call drop.destroy
+          setTimeout(function() {
+            eventManager.editorClosed();
+            scope.$destroy();
+            drop.destroy();
+          });
+        });
+      },
+    ]);
+  }, 100);
+}
+
+/*
+ * jquery.flot.events
+ *
+ * description: Flot plugin for adding events/markers to the plot
+ * version: 0.2.5
+ * authors:
+ *    Alexander Wunschik <alex@wunschik.net>
+ *    Joel Oughton <joeloughton@gmail.com>
+ *    Nicolas Joseph <www.nicolasjoseph.com>
+ *
+ * website: https://github.com/mojoaxel/flot-events
+ *
+ * released under MIT License and GPLv2+
+ */
+
+/**
+ * A class that allows for the drawing an remove of some object
+ */
+export class DrawableEvent {
+  _object: any;
+  _drawFunc: any;
+  _clearFunc: any;
+  _moveFunc: any;
+  _position: any;
+  _width: any;
+  _height: any;
+
+  /** @ngInject */
+  constructor(object, drawFunc, clearFunc, moveFunc, left, top, width, height) {
+    this._object = object;
+    this._drawFunc = drawFunc;
+    this._clearFunc = clearFunc;
+    this._moveFunc = moveFunc;
+    this._position = { left: left, top: top };
+    this._width = width;
+    this._height = height;
+  }
+
+  width() {
+    return this._width;
+  }
+  height() {
+    return this._height;
+  }
+  position() {
+    return this._position;
+  }
+  draw() {
+    this._drawFunc(this._object);
+  }
+  clear() {
+    this._clearFunc(this._object);
+  }
+  getObject() {
+    return this._object;
+  }
+  moveTo(position) {
+    this._position = position;
+    this._moveFunc(this._object, this._position);
+  }
+}
+
+/**
+ * Event class that stores options (eventType, min, max, title, description) and the object to draw.
+ */
+export class VisualEvent {
+  _parent: any;
+  _options: any;
+  _drawableEvent: any;
+  _hidden: any;
+
+  /** @ngInject */
+  constructor(options, drawableEvent) {
+    this._options = options;
+    this._drawableEvent = drawableEvent;
+    this._hidden = false;
+  }
+
+  visual() {
+    return this._drawableEvent;
+  }
+  getOptions() {
+    return this._options;
+  }
+  getParent() {
+    return this._parent;
+  }
+  isHidden() {
+    return this._hidden;
+  }
+  hide() {
+    this._hidden = true;
+  }
+  unhide() {
+    this._hidden = false;
+  }
+}
+
+/**
+ * A Class that handles the event-markers inside the given plot
+ */
+export class EventMarkers {
+  _events: any;
+  _types: any;
+  _plot: any;
+  eventsEnabled: any;
+
+  /** @ngInject */
+  constructor(plot) {
+    this._events = [];
+    this._types = [];
+    this._plot = plot;
+    this.eventsEnabled = false;
+  }
+
+  getEvents() {
+    return this._events;
+  }
+
+  setTypes(types) {
+    return (this._types = types);
+  }
+
+  /**
+   * create internal objects for the given events
+   */
+  setupEvents(events) {
+    let parts = _.partition(events, 'isRegion');
+    let regions = parts[0];
+    events = parts[1];
+
+    $.each(events, (index, event) => {
+      let ve = new VisualEvent(event, this._buildDiv(event));
+      this._events.push(ve);
+    });
+
+    $.each(regions, (index, event) => {
+      let vre = new VisualEvent(event, this._buildRegDiv(event));
+      this._events.push(vre);
+    });
+
+    this._events.sort((a, b) => {
+      let ao = a.getOptions(),
+        bo = b.getOptions();
+      if (ao.min > bo.min) {
+        return 1;
+      }
+      if (ao.min < bo.min) {
+        return -1;
+      }
+      return 0;
+    });
+  }
+
+  /**
+   * draw the events to the plot
+   */
+  drawEvents() {
+    // var o = this._plot.getPlotOffset();
+
+    $.each(this._events, (index, event) => {
+      // check event is inside the graph range
+      if (this._insidePlot(event.getOptions().min) && !event.isHidden()) {
+        event.visual().draw();
+      } else {
+        event
+          .visual()
+          .getObject()
+          .hide();
+      }
+    });
+  }
+
+  /**
+   * update the position of the event-markers (e.g. after scrolling or zooming)
+   */
+  updateEvents() {
+    let o = this._plot.getPlotOffset(),
+      left,
+      top;
+    let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
+
+    $.each(this._events, (index, event) => {
+      top = o.top + this._plot.height() - event.visual().height();
+      left = xaxis.p2c(event.getOptions().min) + o.left - event.visual().width() / 2;
+      event.visual().moveTo({ top: top, left: left });
+    });
+  }
+
+  /**
+   * remove all events from the plot
+   */
+  _clearEvents() {
+    $.each(this._events, (index, val) => {
+      val.visual().clear();
+    });
+    this._events = [];
+  }
+
+  /**
+   * create a DOM element for the given event
+   */
+  _buildDiv(event) {
+    let that = this;
+
+    let container = this._plot.getPlaceholder();
+    let o = this._plot.getPlotOffset();
+    let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
+    let top, left, color, markerSize, markerShow, lineStyle, lineWidth;
+    let markerTooltip;
+
+    // map the eventType to a types object
+    let eventTypeId = event.eventType;
+
+    if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) {
+      color = '#666';
+    } else {
+      color = this._types[eventTypeId].color;
+    }
+
+    if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].markerSize) {
+      markerSize = 8; //default marker size
+    } else {
+      markerSize = this._types[eventTypeId].markerSize;
+    }
+
+    if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerShow === undefined) {
+      markerShow = true;
+    } else {
+      markerShow = this._types[eventTypeId].markerShow;
+    }
+
+    if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) {
+      markerTooltip = true;
+    } else {
+      markerTooltip = this._types[eventTypeId].markerTooltip;
+    }
+
+    if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) {
+      lineStyle = 'dashed'; //default line style
+    } else {
+      lineStyle = this._types[eventTypeId].lineStyle.toLowerCase();
+    }
+
+    if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) {
+      lineWidth = 1; //default line width
+    } else {
+      lineWidth = this._types[eventTypeId].lineWidth;
+    }
+
+    let topOffset = xaxis.options.eventSectionHeight || 0;
+    topOffset = topOffset / 3;
+
+    top = o.top + this._plot.height() + topOffset;
+    left = xaxis.p2c(event.min) + o.left;
+
+    let line = $('<div class="events_line flot-temp-elem"></div>')
+      .css({
+        position: 'absolute',
+        opacity: 0.8,
+        left: left + 'px',
+        top: 8,
+        width: lineWidth + 'px',
+        height: this._plot.height() + topOffset * 0.8,
+        'border-left-width': lineWidth + 'px',
+        'border-left-style': lineStyle,
+        'border-left-color': color,
+        color: color,
+      })
+      .appendTo(container);
+
+    if (markerShow) {
+      let marker = $('<div class="events_marker"></div>').css({
+        position: 'absolute',
+        left: -markerSize - Math.round(lineWidth / 2) + 'px',
+        'font-size': 0,
+        'line-height': 0,
+        width: 0,
+        height: 0,
+        'border-left': markerSize + 'px solid transparent',
+        'border-right': markerSize + 'px solid transparent',
+      });
+
+      marker.appendTo(line);
+
+      if (
+        this._types[eventTypeId] &&
+        this._types[eventTypeId].position &&
+        this._types[eventTypeId].position.toUpperCase() === 'BOTTOM'
+      ) {
+        marker.css({
+          top: top - markerSize - 8 + 'px',
+          'border-top': 'none',
+          'border-bottom': markerSize + 'px solid ' + color,
+        });
+      } else {
+        marker.css({
+          top: '0px',
+          'border-top': markerSize + 'px solid ' + color,
+          'border-bottom': 'none',
+        });
+      }
+
+      marker.data({
+        event: event,
+      });
+
+      let mouseenter = function() {
+        createAnnotationToolip(marker, $(this).data('event'), that._plot);
+      };
+
+      if (event.editModel) {
+        createEditPopover(marker, event.editModel, that._plot);
+      }
+
+      let mouseleave = function() {
+        that._plot.clearSelection();
+      };
+
+      if (markerTooltip) {
+        marker.css({ cursor: 'help' });
+        marker.hover(mouseenter, mouseleave);
+      }
+    }
+
+    let drawableEvent = new DrawableEvent(
+      line,
+      function drawFunc(obj) {
+        obj.show();
+      },
+      function(obj) {
+        obj.remove();
+      },
+      function(obj, position) {
+        obj.css({
+          top: position.top,
+          left: position.left,
+        });
+      },
+      left,
+      top,
+      line.width(),
+      line.height()
+    );
+
+    return drawableEvent;
+  }
+
+  /**
+   * create a DOM element for the given region
+   */
+  _buildRegDiv(event) {
+    let that = this;
+
+    let container = this._plot.getPlaceholder();
+    let o = this._plot.getPlotOffset();
+    let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
+    let top, left, lineWidth, regionWidth, lineStyle, color, markerTooltip;
+
+    // map the eventType to a types object
+    let eventTypeId = event.eventType;
+
+    if (this._types === null || !this._types[eventTypeId] || !this._types[eventTypeId].color) {
+      color = '#666';
+    } else {
+      color = this._types[eventTypeId].color;
+    }
+
+    if (this._types === null || !this._types[eventTypeId] || this._types[eventTypeId].markerTooltip === undefined) {
+      markerTooltip = true;
+    } else {
+      markerTooltip = this._types[eventTypeId].markerTooltip;
+    }
+
+    if (this._types == null || !this._types[eventTypeId] || this._types[eventTypeId].lineWidth === undefined) {
+      lineWidth = 1; //default line width
+    } else {
+      lineWidth = this._types[eventTypeId].lineWidth;
+    }
+
+    if (this._types == null || !this._types[eventTypeId] || !this._types[eventTypeId].lineStyle) {
+      lineStyle = 'dashed'; //default line style
+    } else {
+      lineStyle = this._types[eventTypeId].lineStyle.toLowerCase();
+    }
+
+    let topOffset = 2;
+    top = o.top + this._plot.height() + topOffset;
+
+    let timeFrom = Math.min(event.min, event.timeEnd);
+    let timeTo = Math.max(event.min, event.timeEnd);
+    left = xaxis.p2c(timeFrom) + o.left;
+    let right = xaxis.p2c(timeTo) + o.left;
+    regionWidth = right - left;
+
+    _.each([left, right], position => {
+      let line = $('<div class="events_line flot-temp-elem"></div>').css({
+        position: 'absolute',
+        opacity: 0.8,
+        left: position + 'px',
+        top: 8,
+        width: lineWidth + 'px',
+        height: this._plot.height() + topOffset,
+        'border-left-width': lineWidth + 'px',
+        'border-left-style': lineStyle,
+        'border-left-color': color,
+        color: color,
+      });
+      line.appendTo(container);
+    });
+
+    let region = $('<div class="events_marker region_marker flot-temp-elem"></div>').css({
+      position: 'absolute',
+      opacity: 0.5,
+      left: left + 'px',
+      top: top,
+      width: Math.round(regionWidth + lineWidth) + 'px',
+      height: '0.5rem',
+      'border-left-color': color,
+      color: color,
+      'background-color': color,
+    });
+    region.appendTo(container);
+
+    region.data({
+      event: event,
+    });
+
+    let mouseenter = function() {
+      createAnnotationToolip(region, $(this).data('event'), that._plot);
+    };
+
+    if (event.editModel) {
+      createEditPopover(region, event.editModel, that._plot);
+    }
+
+    let mouseleave = function() {
+      that._plot.clearSelection();
+    };
+
+    if (markerTooltip) {
+      region.css({ cursor: 'help' });
+      region.hover(mouseenter, mouseleave);
+    }
+
+    let drawableEvent = new DrawableEvent(
+      region,
+      function drawFunc(obj) {
+        obj.show();
+      },
+      function(obj) {
+        obj.remove();
+      },
+      function(obj, position) {
+        obj.css({
+          top: position.top,
+          left: position.left,
+        });
+      },
+      left,
+      top,
+      region.width(),
+      region.height()
+    );
+
+    return drawableEvent;
+  }
+
+  /**
+   * check if the event is inside visible range
+   */
+  _insidePlot(x) {
+    let xaxis = this._plot.getXAxes()[this._plot.getOptions().events.xaxis - 1];
+    let xc = xaxis.p2c(x);
+    return xc > 0 && xc < xaxis.p2c(xaxis.max);
+  }
+}
+
+/**
+ * initialize the plugin for the given plot
+ */
+
+/** @ngInject */
+export function init(plot) {
+  /*jshint validthis:true */
+  let that = this;
+  let eventMarkers = new EventMarkers(plot);
+
+  plot.getEvents = function() {
+    return eventMarkers._events;
+  };
+
+  plot.hideEvents = function() {
+    $.each(eventMarkers._events, (index, event) => {
+      event
+        .visual()
+        .getObject()
+        .hide();
+    });
+  };
+
+  plot.showEvents = function() {
+    plot.hideEvents();
+    $.each(eventMarkers._events, (index, event) => {
+      event.hide();
+    });
+
+    that.eventMarkers.drawEvents();
+  };
+
+  // change events on an existing plot
+  plot.setEvents = function(events) {
+    if (eventMarkers.eventsEnabled) {
+      eventMarkers.setupEvents(events);
+    }
+  };
+
+  plot.hooks.processOptions.push(function(plot, options) {
+    // enable the plugin
+    if (options.events.data != null) {
+      eventMarkers.eventsEnabled = true;
+    }
+  });
+
+  plot.hooks.draw.push(function(plot) {
+    let options = plot.getOptions();
+
+    if (eventMarkers.eventsEnabled) {
+      // check for first run
+      if (eventMarkers.getEvents().length < 1) {
+        eventMarkers.setTypes(options.events.types);
+        eventMarkers.setupEvents(options.events.data);
+      } else {
+        eventMarkers.updateEvents();
+      }
+    }
+
+    eventMarkers.drawEvents();
+  });
+}
+
+let defaultOptions = {
+  events: {
+    data: null,
+    types: null,
+    xaxis: 1,
+    position: 'BOTTOM',
+  },
+};
+
+$.plot.plugins.push({
+  init: init,
+  options: defaultOptions,
+  name: 'events',
+  version: '0.2.5',
+});

+ 94 - 0
public/app/plugins/panel/graph/specs/graph_ctrl.jest.ts

@@ -0,0 +1,94 @@
+import moment from 'moment';
+import { GraphCtrl } from '../module';
+
+jest.mock('../graph', () => ({}));
+
+describe('GraphCtrl', () => {
+  let injector = {
+    get: () => {
+      return {
+        timeRange: () => {
+          return {
+            from: '',
+            to: '',
+          };
+        },
+      };
+    },
+  };
+
+  let scope = {
+    $on: () => {},
+  };
+
+  GraphCtrl.prototype.panel = {
+    events: {
+      on: () => {},
+    },
+    gridPos: {
+      w: 100,
+    },
+  };
+
+  let ctx = <any>{};
+
+  beforeEach(() => {
+    ctx.ctrl = new GraphCtrl(scope, injector, {});
+    ctx.ctrl.annotationsPromise = Promise.resolve({});
+    ctx.ctrl.updateTimeRange();
+  });
+
+  describe('when time series are outside range', () => {
+    beforeEach(() => {
+      var data = [
+        {
+          target: 'test.cpu1',
+          datapoints: [[45, 1234567890], [60, 1234567899]],
+        },
+      ];
+
+      ctx.ctrl.range = { from: moment().valueOf(), to: moment().valueOf() };
+      ctx.ctrl.onDataReceived(data);
+    });
+
+    it('should set datapointsOutside', () => {
+      expect(ctx.ctrl.dataWarning.title).toBe('Data points outside time range');
+    });
+  });
+
+  describe('when time series are inside range', () => {
+    beforeEach(() => {
+      var range = {
+        from: moment()
+          .subtract(1, 'days')
+          .valueOf(),
+        to: moment().valueOf(),
+      };
+
+      var data = [
+        {
+          target: 'test.cpu1',
+          datapoints: [[45, range.from + 1000], [60, range.from + 10000]],
+        },
+      ];
+
+      ctx.ctrl.range = range;
+      ctx.ctrl.onDataReceived(data);
+    });
+
+    it('should set datapointsOutside', () => {
+      expect(ctx.ctrl.dataWarning).toBe(null);
+    });
+  });
+
+  describe('datapointsCount given 2 series', () => {
+    beforeEach(() => {
+      var data = [{ target: 'test.cpu1', datapoints: [] }, { target: 'test.cpu2', datapoints: [] }];
+      ctx.ctrl.onDataReceived(data);
+    });
+
+    it('should set datapointsCount warning', () => {
+      expect(ctx.ctrl.dataWarning.title).toBe('No data points');
+    });
+  });
+});

+ 0 - 78
public/app/plugins/panel/graph/specs/graph_ctrl_specs.ts

@@ -1,78 +0,0 @@
-import { describe, beforeEach, it, expect, angularMocks } from '../../../../../test/lib/common';
-
-import moment from 'moment';
-import { GraphCtrl } from '../module';
-import helpers from '../../../../../test/specs/helpers';
-
-describe('GraphCtrl', function() {
-  var ctx = new helpers.ControllerTestContext();
-
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(angularMocks.module('grafana.controllers'));
-  beforeEach(
-    angularMocks.module(function($compileProvider) {
-      $compileProvider.preAssignBindingsEnabled(true);
-    })
-  );
-
-  beforeEach(ctx.providePhase());
-  beforeEach(ctx.createPanelController(GraphCtrl));
-  beforeEach(() => {
-    ctx.ctrl.annotationsPromise = Promise.resolve({});
-    ctx.ctrl.updateTimeRange();
-  });
-
-  describe('when time series are outside range', function() {
-    beforeEach(function() {
-      var data = [
-        {
-          target: 'test.cpu1',
-          datapoints: [[45, 1234567890], [60, 1234567899]],
-        },
-      ];
-
-      ctx.ctrl.range = { from: moment().valueOf(), to: moment().valueOf() };
-      ctx.ctrl.onDataReceived(data);
-    });
-
-    it('should set datapointsOutside', function() {
-      expect(ctx.ctrl.dataWarning.title).to.be('Data points outside time range');
-    });
-  });
-
-  describe('when time series are inside range', function() {
-    beforeEach(function() {
-      var range = {
-        from: moment()
-          .subtract(1, 'days')
-          .valueOf(),
-        to: moment().valueOf(),
-      };
-
-      var data = [
-        {
-          target: 'test.cpu1',
-          datapoints: [[45, range.from + 1000], [60, range.from + 10000]],
-        },
-      ];
-
-      ctx.ctrl.range = range;
-      ctx.ctrl.onDataReceived(data);
-    });
-
-    it('should set datapointsOutside', function() {
-      expect(ctx.ctrl.dataWarning).to.be(null);
-    });
-  });
-
-  describe('datapointsCount given 2 series', function() {
-    beforeEach(function() {
-      var data = [{ target: 'test.cpu1', datapoints: [] }, { target: 'test.cpu2', datapoints: [] }];
-      ctx.ctrl.onDataReceived(data);
-    });
-
-    it('should set datapointsCount warning', function() {
-      expect(ctx.ctrl.dataWarning.title).to.be('No data points');
-    });
-  });
-});

+ 2 - 2
public/app/plugins/panel/singlestat/editor.html

@@ -56,10 +56,10 @@
     <h5 class="section-heading">Coloring</h5>
     <h5 class="section-heading">Coloring</h5>
     <div class="gf-form-inline">
     <div class="gf-form-inline">
       <gf-form-switch class="gf-form" label-class="width-8" label="Background" checked="ctrl.panel.colorBackground" on-change="ctrl.render()"></gf-form-switch>
       <gf-form-switch class="gf-form" label-class="width-8" label="Background" checked="ctrl.panel.colorBackground" on-change="ctrl.render()"></gf-form-switch>
-      <gf-form-switch class="gf-form" label-class="width-4" label="Value" checked="ctrl.panel.colorValue" on-change="ctrl.render()"></gf-form-switch>
+      <gf-form-switch class="gf-form" label-class="width-6" label="Value" checked="ctrl.panel.colorValue" on-change="ctrl.render()"></gf-form-switch>
     </div>
     </div>
     <div class="gf-form-inline">
     <div class="gf-form-inline">
-      <gf-form-switch class="gf-form" label-class="width-6" label="Prefix" checked="ctrl.panel.colorPrefix" on-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></gf-form-switch>
+      <gf-form-switch class="gf-form" label-class="width-8" label="Prefix" checked="ctrl.panel.colorPrefix" on-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></gf-form-switch>
       <gf-form-switch class="gf-form" label-class="width-6" label="Postfix" checked="ctrl.panel.colorPostfix" on-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></gf-form-switch>
       <gf-form-switch class="gf-form" label-class="width-6" label="Postfix" checked="ctrl.panel.colorPostfix" on-change="ctrl.render()" ng-disabled="!ctrl.canModifyText()"></gf-form-switch>
     </div>
     </div>
     <div class="gf-form-inline">
     <div class="gf-form-inline">

+ 1 - 1
public/app/routes/routes.ts

@@ -112,7 +112,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
       controller: 'FolderDashboardsCtrl',
       controller: 'FolderDashboardsCtrl',
       controllerAs: 'ctrl',
       controllerAs: 'ctrl',
     })
     })
-    .when('/explore/:initial?', {
+    .when('/explore', {
       template: '<react-container />',
       template: '<react-container />',
       resolve: {
       resolve: {
         roles: () => ['Editor', 'Admin'],
         roles: () => ['Editor', 'Admin'],

+ 0 - 36
public/sass/_variables.dark.scss

@@ -93,24 +93,14 @@ $headings-color: darken($white, 11%);
 $abbr-border-color: $gray-3 !default;
 $abbr-border-color: $gray-3 !default;
 $text-muted: $text-color-weak;
 $text-muted: $text-color-weak;
 
 
-$blockquote-small-color: $gray-3 !default;
-$blockquote-border-color: $gray-4 !default;
-
 $hr-border-color: rgba(0, 0, 0, 0.1) !default;
 $hr-border-color: rgba(0, 0, 0, 0.1) !default;
 
 
-// Components
-$component-active-color: #fff !default;
-$component-active-bg: $brand-primary !default;
-
 // Panel
 // Panel
 // -------------------------
 // -------------------------
 $panel-bg: #212124;
 $panel-bg: #212124;
 $panel-border-color: $dark-1;
 $panel-border-color: $dark-1;
 $panel-border: solid 1px $panel-border-color;
 $panel-border: solid 1px $panel-border-color;
-$panel-drop-zone-bg: repeating-linear-gradient(-128deg, #111, #111 10px, #191919 10px, #222 20px);
 $panel-header-hover-bg: $dark-4;
 $panel-header-hover-bg: $dark-4;
-$panel-header-menu-hover-bg: $dark-5;
-$panel-edit-shadow: 0 -30px 30px -30px $black;
 
 
 // page header
 // page header
 $page-header-bg: linear-gradient(90deg, #292a2d, black);
 $page-header-bg: linear-gradient(90deg, #292a2d, black);
@@ -205,7 +195,6 @@ $input-box-shadow-focus: rgba(102, 175, 233, 0.6);
 $input-color-placeholder: $gray-1 !default;
 $input-color-placeholder: $gray-1 !default;
 $input-label-bg: $gray-blue;
 $input-label-bg: $gray-blue;
 $input-label-border-color: $dark-3;
 $input-label-border-color: $dark-3;
-$input-invalid-border-color: lighten($red, 5%);
 
 
 // Search
 // Search
 $search-shadow: 0 0 30px 0 $black;
 $search-shadow: 0 0 30px 0 $black;
@@ -223,7 +212,6 @@ $dropdownBorder: rgba(0, 0, 0, 0.2);
 $dropdownDividerTop: transparent;
 $dropdownDividerTop: transparent;
 $dropdownDividerBottom: #444;
 $dropdownDividerBottom: #444;
 $dropdownDivider: $dropdownDividerBottom;
 $dropdownDivider: $dropdownDividerBottom;
-$dropdownTitle: $link-color-disabled;
 
 
 $dropdownLinkColor: $text-color;
 $dropdownLinkColor: $text-color;
 $dropdownLinkColorHover: $white;
 $dropdownLinkColorHover: $white;
@@ -232,8 +220,6 @@ $dropdownLinkColorActive: $white;
 $dropdownLinkBackgroundActive: $dark-4;
 $dropdownLinkBackgroundActive: $dark-4;
 $dropdownLinkBackgroundHover: $dark-4;
 $dropdownLinkBackgroundHover: $dark-4;
 
 
-$dropdown-link-color: $gray-3;
-
 // COMPONENT VARIABLES
 // COMPONENT VARIABLES
 // --------------------------------------------------
 // --------------------------------------------------
 
 
@@ -246,22 +232,13 @@ $horizontalComponentOffset: 180px;
 
 
 // Wells
 // Wells
 // -------------------------
 // -------------------------
-$wellBackground: #131517;
 
 
 $navbarHeight: 55px;
 $navbarHeight: 55px;
-$navbarBackgroundHighlight: $dark-3;
 $navbarBackground: $panel-bg;
 $navbarBackground: $panel-bg;
 $navbarBorder: 1px solid $dark-3;
 $navbarBorder: 1px solid $dark-3;
 $navbarShadow: 0 0 20px black;
 $navbarShadow: 0 0 20px black;
 
 
-$navbarText: $gray-4;
 $navbarLinkColor: $gray-4;
 $navbarLinkColor: $gray-4;
-$navbarLinkColorHover: $white;
-$navbarLinkColorActive: $navbarLinkColorHover;
-$navbarLinkBackgroundHover: transparent;
-$navbarLinkBackgroundActive: $navbarBackground;
-$navbarBrandColor: $link-color;
-$navbarDropdownShadow: inset 0px 4px 10px -4px $body-bg;
 
 
 $navbarButtonBackground: $navbarBackground;
 $navbarButtonBackground: $navbarBackground;
 $navbarButtonBackgroundHighlight: $body-bg;
 $navbarButtonBackgroundHighlight: $body-bg;
@@ -275,20 +252,15 @@ $side-menu-bg-mobile: $side-menu-bg;
 $side-menu-item-hover-bg: $dark-2;
 $side-menu-item-hover-bg: $dark-2;
 $side-menu-shadow: 0 0 20px black;
 $side-menu-shadow: 0 0 20px black;
 $side-menu-link-color: $link-color;
 $side-menu-link-color: $link-color;
-$breadcrumb-hover-hl: #111;
 
 
 // Menu dropdowns
 // Menu dropdowns
 // -------------------------
 // -------------------------
 $menu-dropdown-bg: $body-bg;
 $menu-dropdown-bg: $body-bg;
 $menu-dropdown-hover-bg: $dark-2;
 $menu-dropdown-hover-bg: $dark-2;
-$menu-dropdown-border-color: $dark-3;
 $menu-dropdown-shadow: 5px 5px 20px -5px $black;
 $menu-dropdown-shadow: 5px 5px 20px -5px $black;
 
 
 // Breadcrumb
 // Breadcrumb
 // -------------------------
 // -------------------------
-$page-nav-bg: $black;
-$page-nav-shadow: 5px 5px 20px -5px $black;
-$page-nav-breadcrumb-color: $gray-3;
 
 
 // Tabs
 // Tabs
 // -------------------------
 // -------------------------
@@ -296,9 +268,6 @@ $tab-border-color: $dark-4;
 
 
 // Pagination
 // Pagination
 // -------------------------
 // -------------------------
-$paginationBackground: $body-bg;
-$paginationBorder: transparent;
-$paginationActiveBackground: $blue;
 
 
 // Form states and alerts
 // Form states and alerts
 // -------------------------
 // -------------------------
@@ -343,10 +312,6 @@ $info-box-color: $gray-4;
 $footer-link-color: $gray-2;
 $footer-link-color: $gray-2;
 $footer-link-hover: $gray-4;
 $footer-link-hover: $gray-4;
 
 
-// collapse box
-$collapse-box-body-border: $dark-5;
-$collapse-box-body-error-border: $red;
-
 // json-explorer
 // json-explorer
 $json-explorer-default-color: $text-color;
 $json-explorer-default-color: $text-color;
 $json-explorer-string-color: #23d662;
 $json-explorer-string-color: #23d662;
@@ -357,7 +322,6 @@ $json-explorer-undefined-color: rgb(239, 143, 190);
 $json-explorer-function-color: #fd48cb;
 $json-explorer-function-color: #fd48cb;
 $json-explorer-rotate-time: 100ms;
 $json-explorer-rotate-time: 100ms;
 $json-explorer-toggler-opacity: 0.6;
 $json-explorer-toggler-opacity: 0.6;
-$json-explorer-toggler-color: #45376f;
 $json-explorer-bracket-color: #9494ff;
 $json-explorer-bracket-color: #9494ff;
 $json-explorer-key-color: #23a0db;
 $json-explorer-key-color: #23a0db;
 $json-explorer-url-color: #027bff;
 $json-explorer-url-color: #027bff;

+ 0 - 31
public/sass/_variables.light.scss

@@ -90,25 +90,15 @@ $headings-color: $text-color;
 $abbr-border-color: $gray-2 !default;
 $abbr-border-color: $gray-2 !default;
 $text-muted: $text-color-weak;
 $text-muted: $text-color-weak;
 
 
-$blockquote-small-color: $gray-2 !default;
-$blockquote-border-color: $gray-3 !default;
-
 $hr-border-color: $dark-3 !default;
 $hr-border-color: $dark-3 !default;
 
 
-// Components
-$component-active-color: $white !default;
-$component-active-bg: $brand-primary !default;
-
 // Panel
 // Panel
 // -------------------------
 // -------------------------
 
 
 $panel-bg: $white;
 $panel-bg: $white;
 $panel-border-color: $gray-5;
 $panel-border-color: $gray-5;
 $panel-border: solid 1px $panel-border-color;
 $panel-border: solid 1px $panel-border-color;
-$panel-drop-zone-bg: repeating-linear-gradient(-128deg, $body-bg, $body-bg 10px, $gray-6 10px, $gray-6 20px);
 $panel-header-hover-bg: $gray-6;
 $panel-header-hover-bg: $gray-6;
-$panel-header-menu-hover-bg: $gray-4;
-$panel-edit-shadow: 0 0 30px 20px $black;
 
 
 // Page header
 // Page header
 $page-header-bg: linear-gradient(90deg, $white, $gray-7);
 $page-header-bg: linear-gradient(90deg, $white, $gray-7);
@@ -201,7 +191,6 @@ $input-box-shadow-focus: $blue !default;
 $input-color-placeholder: $gray-4 !default;
 $input-color-placeholder: $gray-4 !default;
 $input-label-bg: $gray-5;
 $input-label-bg: $gray-5;
 $input-label-border-color: $gray-5;
 $input-label-border-color: $gray-5;
-$input-invalid-border-color: lighten($red, 5%);
 
 
 // Sidemenu
 // Sidemenu
 // -------------------------
 // -------------------------
@@ -215,15 +204,10 @@ $side-menu-link-color: $gray-6;
 // -------------------------
 // -------------------------
 $menu-dropdown-bg: $gray-7;
 $menu-dropdown-bg: $gray-7;
 $menu-dropdown-hover-bg: $gray-6;
 $menu-dropdown-hover-bg: $gray-6;
-$menu-dropdown-border-color: $gray-4;
 $menu-dropdown-shadow: 5px 5px 10px -5px $gray-1;
 $menu-dropdown-shadow: 5px 5px 10px -5px $gray-1;
 
 
 // Breadcrumb
 // Breadcrumb
 // -------------------------
 // -------------------------
-$page-nav-bg: $gray-5;
-$page-nav-shadow: 5px 5px 20px -5px $gray-4;
-$page-nav-breadcrumb-color: $black;
-$breadcrumb-hover-hl: #d9dadd;
 
 
 // Tabs
 // Tabs
 // -------------------------
 // -------------------------
@@ -245,7 +229,6 @@ $dropdownBorder: $gray-4;
 $dropdownDividerTop: $gray-6;
 $dropdownDividerTop: $gray-6;
 $dropdownDividerBottom: $white;
 $dropdownDividerBottom: $white;
 $dropdownDivider: $dropdownDividerTop;
 $dropdownDivider: $dropdownDividerTop;
-$dropdownTitle: $gray-3;
 
 
 $dropdownLinkColor: $dark-3;
 $dropdownLinkColor: $dark-3;
 $dropdownLinkColorHover: $link-color;
 $dropdownLinkColorHover: $link-color;
@@ -271,24 +254,16 @@ $horizontalComponentOffset: 180px;
 
 
 // Wells
 // Wells
 // -------------------------
 // -------------------------
-$wellBackground: $gray-3;
 
 
 // Navbar
 // Navbar
 // -------------------------
 // -------------------------
 
 
 $navbarHeight: 52px;
 $navbarHeight: 52px;
-$navbarBackgroundHighlight: $white;
 $navbarBackground: $white;
 $navbarBackground: $white;
 $navbarBorder: 1px solid $gray-4;
 $navbarBorder: 1px solid $gray-4;
 $navbarShadow: 0 0 3px #c1c1c1;
 $navbarShadow: 0 0 3px #c1c1c1;
 
 
-$navbarText: #444;
 $navbarLinkColor: #444;
 $navbarLinkColor: #444;
-$navbarLinkColorHover: #000;
-$navbarLinkColorActive: #333;
-$navbarLinkBackgroundHover: transparent;
-$navbarLinkBackgroundActive: darken($navbarBackground, 6.5%);
-$navbarDropdownShadow: inset 0px 4px 7px -4px darken($body-bg, 20%);
 
 
 $navbarBrandColor: $navbarLinkColor;
 $navbarBrandColor: $navbarLinkColor;
 
 
@@ -299,9 +274,6 @@ $navbar-button-border: $gray-4;
 
 
 // Pagination
 // Pagination
 // -------------------------
 // -------------------------
-$paginationBackground: $gray-2;
-$paginationBorder: transparent;
-$paginationActiveBackground: $blue;
 
 
 // Form states and alerts
 // Form states and alerts
 // -------------------------
 // -------------------------
@@ -346,8 +318,6 @@ $footer-link-color: $gray-3;
 $footer-link-hover: $dark-5;
 $footer-link-hover: $dark-5;
 
 
 // collapse box
 // collapse box
-$collapse-box-body-border: $gray-4;
-$collapse-box-body-error-border: $red;
 
 
 // json explorer
 // json explorer
 $json-explorer-default-color: black;
 $json-explorer-default-color: black;
@@ -359,7 +329,6 @@ $json-explorer-undefined-color: rgb(202, 11, 105);
 $json-explorer-function-color: #ff20ed;
 $json-explorer-function-color: #ff20ed;
 $json-explorer-rotate-time: 100ms;
 $json-explorer-rotate-time: 100ms;
 $json-explorer-toggler-opacity: 0.6;
 $json-explorer-toggler-opacity: 0.6;
-$json-explorer-toggler-color: #45376f;
 $json-explorer-bracket-color: blue;
 $json-explorer-bracket-color: blue;
 $json-explorer-key-color: #00008b;
 $json-explorer-key-color: #00008b;
 $json-explorer-url-color: blue;
 $json-explorer-url-color: blue;

+ 9 - 36
public/sass/_variables.scss

@@ -3,13 +3,7 @@
 // Quickly modify global styling by enabling or disabling optional features.
 // Quickly modify global styling by enabling or disabling optional features.
 
 
 $enable-flex: true !default;
 $enable-flex: true !default;
-$enable-rounded: true !default;
-$enable-shadows: false !default;
-$enable-gradients: false !default;
-$enable-transitions: false !default;
 $enable-hover-media-query: false !default;
 $enable-hover-media-query: false !default;
-$enable-grid-classes: true !default;
-$enable-print-styles: true !default;
 
 
 // Spacing
 // Spacing
 //
 //
@@ -53,9 +47,9 @@ $enable-flex: true;
 // Typography
 // Typography
 // -------------------------
 // -------------------------
 
 
-$font-family-sans-serif: "Roboto", Helvetica, Arial, sans-serif;
-$font-family-serif: Georgia, "Times New Roman", Times, serif;
-$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
+$font-family-sans-serif: 'Roboto', Helvetica, Arial, sans-serif;
+$font-family-serif: Georgia, 'Times New Roman', Times, serif;
+$font-family-monospace: Menlo, Monaco, Consolas, 'Courier New', monospace;
 $font-family-base: $font-family-sans-serif !default;
 $font-family-base: $font-family-sans-serif !default;
 
 
 $font-size-root: 14px !default;
 $font-size-root: 14px !default;
@@ -90,16 +84,12 @@ $lead-font-size: 1.25rem !default;
 $lead-font-weight: 300 !default;
 $lead-font-weight: 300 !default;
 
 
 $headings-margin-bottom: ($spacer / 2) !default;
 $headings-margin-bottom: ($spacer / 2) !default;
-$headings-font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif;
+$headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
 $headings-font-weight: 400 !default;
 $headings-font-weight: 400 !default;
 $headings-line-height: 1.1 !default;
 $headings-line-height: 1.1 !default;
 
 
-$blockquote-font-size: ($font-size-base * 1.25) !default;
-$blockquote-border-width: 0.25rem !default;
-
 $hr-border-width: $border-width !default;
 $hr-border-width: $border-width !default;
 $dt-font-weight: bold !default;
 $dt-font-weight: bold !default;
-$list-inline-padding: 5px !default;
 
 
 // Components
 // Components
 //
 //
@@ -112,9 +102,6 @@ $border-radius: 3px !default;
 $border-radius-lg: 5px !default;
 $border-radius-lg: 5px !default;
 $border-radius-sm: 2px!default;
 $border-radius-sm: 2px!default;
 
 
-$caret-width: 0.3em !default;
-$caret-width-lg: $caret-width !default;
-
 // Page
 // Page
 
 
 $page-sidebar-width: 11rem;
 $page-sidebar-width: 11rem;
@@ -130,7 +117,6 @@ $link-hover-decoration: none !default;
 // Customizes the `.table` component with basic values, each used across all table variations.
 // Customizes the `.table` component with basic values, each used across all table variations.
 
 
 $table-cell-padding: 4px 10px !default;
 $table-cell-padding: 4px 10px !default;
-$table-sm-cell-padding: 0.3rem !default;
 
 
 // Forms
 // Forms
 $input-padding-x: 10px !default;
 $input-padding-x: 10px !default;
@@ -139,31 +125,18 @@ $input-line-height: 18px !default;
 
 
 $input-btn-border-width: 1px;
 $input-btn-border-width: 1px;
 $input-border-radius: 0 $border-radius $border-radius 0 !default;
 $input-border-radius: 0 $border-radius $border-radius 0 !default;
-$input-border-radius-lg: 0 $border-radius-lg $border-radius-lg 0 !default;
 $input-border-radius-sm: 0 $border-radius-sm $border-radius-sm 0 !default;
 $input-border-radius-sm: 0 $border-radius-sm $border-radius-sm 0 !default;
 
 
 $label-border-radius: $border-radius 0 0 $border-radius !default;
 $label-border-radius: $border-radius 0 0 $border-radius !default;
-$label-border-radius-lg: $border-radius-lg 0 0 $border-radius-lg !default;
 $label-border-radius-sm: $border-radius-sm 0 0 $border-radius-sm !default;
 $label-border-radius-sm: $border-radius-sm 0 0 $border-radius-sm !default;
 
 
-$input-padding-x-sm: 7px !default;
 $input-padding-y-sm: 4px !default;
 $input-padding-y-sm: 4px !default;
 
 
 $input-padding-x-lg: 20px !default;
 $input-padding-x-lg: 20px !default;
 $input-padding-y-lg: 10px !default;
 $input-padding-y-lg: 10px !default;
 
 
-$input-height: (($font-size-base * $line-height-base) + ($input-padding-y * 2))
-  !default;
-$input-height-lg: (
-     ($font-size-lg * $line-height-lg) + ($input-padding-y-lg * 2)
-  )
-  !default;
-$input-height-sm: (
-     ($font-size-sm * $line-height-sm) + ($input-padding-y-sm * 2)
-  )
-  !default;
+$input-height: (($font-size-base * $line-height-base) + ($input-padding-y * 2)) !default;
 
 
-$form-group-margin-bottom: $spacer-y !default;
 $gf-form-margin: 0.2rem;
 $gf-form-margin: 0.2rem;
 
 
 $cursor-disabled: not-allowed !default;
 $cursor-disabled: not-allowed !default;
@@ -221,9 +194,9 @@ $panel-padding: 0px 10px 5px 10px;
 $tabs-padding: 10px 15px 9px;
 $tabs-padding: 10px 15px 9px;
 
 
 $external-services: (
 $external-services: (
-    github: (bgColor: #464646, borderColor: #393939, icon: ""),
-    google: (bgColor: #e84d3c, borderColor: #b83e31, icon: ""),
-    grafanacom: (bgColor: inherit, borderColor: #393939, icon: ""),
-    oauth: (bgColor: inherit, borderColor: #393939, icon: "")
+    github: (bgColor: #464646, borderColor: #393939, icon: ''),
+    google: (bgColor: #e84d3c, borderColor: #b83e31, icon: ''),
+    grafanacom: (bgColor: inherit, borderColor: #393939, icon: ''),
+    oauth: (bgColor: inherit, borderColor: #393939, icon: '')
   )
   )
   !default;
   !default;

+ 7 - 3
public/sass/base/_type.scss

@@ -24,7 +24,7 @@ small {
   font-size: 85%;
   font-size: 85%;
 }
 }
 strong {
 strong {
-  font-weight: bold;
+  font-weight: $font-weight-semi-bold;
 }
 }
 em {
 em {
   font-style: italic;
   font-style: italic;
@@ -249,7 +249,7 @@ dd {
   line-height: $line-height-base;
   line-height: $line-height-base;
 }
 }
 dt {
 dt {
-  font-weight: bold;
+  font-weight: $font-weight-semi-bold;
 }
 }
 dd {
 dd {
   margin-left: $line-height-base / 2;
   margin-left: $line-height-base / 2;
@@ -376,7 +376,7 @@ a.external-link {
       padding: $spacer*0.5 $spacer;
       padding: $spacer*0.5 $spacer;
     }
     }
     th {
     th {
-      font-weight: normal;
+      font-weight: $font-weight-semi-bold;
       background: $table-bg-accent;
       background: $table-bg-accent;
     }
     }
   }
   }
@@ -415,3 +415,7 @@ a.external-link {
   color: $yellow;
   color: $yellow;
   padding: 0;
   padding: 0;
 }
 }
+
+th {
+  font-weight: $font-weight-semi-bold;
+}

+ 1 - 0
public/sass/pages/_dashboard.scss

@@ -16,6 +16,7 @@ div.flot-text {
   height: 100%;
   height: 100%;
 
 
   &--solo {
   &--solo {
+    margin: 0;
     .panel-container {
     .panel-container {
       border: none;
       border: none;
       z-index: $zindex-sidemenu + 1;
       z-index: $zindex-sidemenu + 1;

+ 41 - 0
public/sass/pages/_explore.scss

@@ -60,6 +60,10 @@
     flex-wrap: wrap;
     flex-wrap: wrap;
   }
   }
 
 
+  .datasource-picker {
+    min-width: 10rem;
+  }
+
   .timepicker {
   .timepicker {
     display: flex;
     display: flex;
 
 
@@ -93,3 +97,40 @@
 .query-row-tools {
 .query-row-tools {
   width: 4rem;
   width: 4rem;
 }
 }
+
+.explore {
+  .logs {
+    .logs-entries {
+      display: grid;
+      grid-column-gap: 1rem;
+      grid-row-gap: 0.1rem;
+      grid-template-columns: 4px minmax(100px, max-content) 1fr;
+      font-family: $font-family-monospace;
+    }
+
+    .logs-row-match-highlight {
+      background-color: lighten($blue, 20%);
+    }
+
+    .logs-row-level {
+      background-color: transparent;
+      margin: 6px 0;
+      border-radius: 2px;
+      opacity: 0.8;
+    }
+
+    .logs-row-level-crit,
+    .logs-row-level-error,
+    .logs-row-level-err {
+      background-color: $red;
+    }
+
+    .logs-row-level-warn {
+      background-color: $orange;
+    }
+
+    .logs-row-level-info {
+      background-color: $green;
+    }
+  }
+}

+ 2 - 2
vendor/github.com/aws/aws-sdk-go/aws/client/client.go

@@ -91,6 +91,6 @@ func (c *Client) AddDebugHandlers() {
 		return
 		return
 	}
 	}
 
 
-	c.Handlers.Send.PushFrontNamed(request.NamedHandler{Name: "awssdk.client.LogRequest", Fn: logRequest})
-	c.Handlers.Send.PushBackNamed(request.NamedHandler{Name: "awssdk.client.LogResponse", Fn: logResponse})
+	c.Handlers.Send.PushFrontNamed(LogHTTPRequestHandler)
+	c.Handlers.Send.PushBackNamed(LogHTTPResponseHandler)
 }
 }

+ 87 - 15
vendor/github.com/aws/aws-sdk-go/aws/client/logger.go

@@ -44,12 +44,22 @@ func (reader *teeReaderCloser) Close() error {
 	return reader.Source.Close()
 	return reader.Source.Close()
 }
 }
 
 
+// LogHTTPRequestHandler is a SDK request handler to log the HTTP request sent
+// to a service. Will include the HTTP request body if the LogLevel of the
+// request matches LogDebugWithHTTPBody.
+var LogHTTPRequestHandler = request.NamedHandler{
+	Name: "awssdk.client.LogRequest",
+	Fn:   logRequest,
+}
+
 func logRequest(r *request.Request) {
 func logRequest(r *request.Request) {
 	logBody := r.Config.LogLevel.Matches(aws.LogDebugWithHTTPBody)
 	logBody := r.Config.LogLevel.Matches(aws.LogDebugWithHTTPBody)
 	bodySeekable := aws.IsReaderSeekable(r.Body)
 	bodySeekable := aws.IsReaderSeekable(r.Body)
-	dumpedBody, err := httputil.DumpRequestOut(r.HTTPRequest, logBody)
+
+	b, err := httputil.DumpRequestOut(r.HTTPRequest, logBody)
 	if err != nil {
 	if err != nil {
-		r.Config.Logger.Log(fmt.Sprintf(logReqErrMsg, r.ClientInfo.ServiceName, r.Operation.Name, err))
+		r.Config.Logger.Log(fmt.Sprintf(logReqErrMsg,
+			r.ClientInfo.ServiceName, r.Operation.Name, err))
 		return
 		return
 	}
 	}
 
 
@@ -63,7 +73,28 @@ func logRequest(r *request.Request) {
 		r.ResetBody()
 		r.ResetBody()
 	}
 	}
 
 
-	r.Config.Logger.Log(fmt.Sprintf(logReqMsg, r.ClientInfo.ServiceName, r.Operation.Name, string(dumpedBody)))
+	r.Config.Logger.Log(fmt.Sprintf(logReqMsg,
+		r.ClientInfo.ServiceName, r.Operation.Name, string(b)))
+}
+
+// LogHTTPRequestHeaderHandler is a SDK request handler to log the HTTP request sent
+// to a service. Will only log the HTTP request's headers. The request payload
+// will not be read.
+var LogHTTPRequestHeaderHandler = request.NamedHandler{
+	Name: "awssdk.client.LogRequestHeader",
+	Fn:   logRequestHeader,
+}
+
+func logRequestHeader(r *request.Request) {
+	b, err := httputil.DumpRequestOut(r.HTTPRequest, false)
+	if err != nil {
+		r.Config.Logger.Log(fmt.Sprintf(logReqErrMsg,
+			r.ClientInfo.ServiceName, r.Operation.Name, err))
+		return
+	}
+
+	r.Config.Logger.Log(fmt.Sprintf(logReqMsg,
+		r.ClientInfo.ServiceName, r.Operation.Name, string(b)))
 }
 }
 
 
 const logRespMsg = `DEBUG: Response %s/%s Details:
 const logRespMsg = `DEBUG: Response %s/%s Details:
@@ -76,27 +107,44 @@ const logRespErrMsg = `DEBUG ERROR: Response %s/%s:
 %s
 %s
 -----------------------------------------------------`
 -----------------------------------------------------`
 
 
+// LogHTTPResponseHandler is a SDK request handler to log the HTTP response
+// received from a service. Will include the HTTP response body if the LogLevel
+// of the request matches LogDebugWithHTTPBody.
+var LogHTTPResponseHandler = request.NamedHandler{
+	Name: "awssdk.client.LogResponse",
+	Fn:   logResponse,
+}
+
 func logResponse(r *request.Request) {
 func logResponse(r *request.Request) {
 	lw := &logWriter{r.Config.Logger, bytes.NewBuffer(nil)}
 	lw := &logWriter{r.Config.Logger, bytes.NewBuffer(nil)}
-	r.HTTPResponse.Body = &teeReaderCloser{
-		Reader: io.TeeReader(r.HTTPResponse.Body, lw),
-		Source: r.HTTPResponse.Body,
+
+	logBody := r.Config.LogLevel.Matches(aws.LogDebugWithHTTPBody)
+	if logBody {
+		r.HTTPResponse.Body = &teeReaderCloser{
+			Reader: io.TeeReader(r.HTTPResponse.Body, lw),
+			Source: r.HTTPResponse.Body,
+		}
 	}
 	}
 
 
 	handlerFn := func(req *request.Request) {
 	handlerFn := func(req *request.Request) {
-		body, err := httputil.DumpResponse(req.HTTPResponse, false)
+		b, err := httputil.DumpResponse(req.HTTPResponse, false)
 		if err != nil {
 		if err != nil {
-			lw.Logger.Log(fmt.Sprintf(logRespErrMsg, req.ClientInfo.ServiceName, req.Operation.Name, err))
+			lw.Logger.Log(fmt.Sprintf(logRespErrMsg,
+				req.ClientInfo.ServiceName, req.Operation.Name, err))
 			return
 			return
 		}
 		}
 
 
-		b, err := ioutil.ReadAll(lw.buf)
-		if err != nil {
-			lw.Logger.Log(fmt.Sprintf(logRespErrMsg, req.ClientInfo.ServiceName, req.Operation.Name, err))
-			return
-		}
-		lw.Logger.Log(fmt.Sprintf(logRespMsg, req.ClientInfo.ServiceName, req.Operation.Name, string(body)))
-		if req.Config.LogLevel.Matches(aws.LogDebugWithHTTPBody) {
+		lw.Logger.Log(fmt.Sprintf(logRespMsg,
+			req.ClientInfo.ServiceName, req.Operation.Name, string(b)))
+
+		if logBody {
+			b, err := ioutil.ReadAll(lw.buf)
+			if err != nil {
+				lw.Logger.Log(fmt.Sprintf(logRespErrMsg,
+					req.ClientInfo.ServiceName, req.Operation.Name, err))
+				return
+			}
+
 			lw.Logger.Log(string(b))
 			lw.Logger.Log(string(b))
 		}
 		}
 	}
 	}
@@ -110,3 +158,27 @@ func logResponse(r *request.Request) {
 		Name: handlerName, Fn: handlerFn,
 		Name: handlerName, Fn: handlerFn,
 	})
 	})
 }
 }
+
+// LogHTTPResponseHeaderHandler is a SDK request handler to log the HTTP
+// response received from a service. Will only log the HTTP response's headers.
+// The response payload will not be read.
+var LogHTTPResponseHeaderHandler = request.NamedHandler{
+	Name: "awssdk.client.LogResponseHeader",
+	Fn:   logResponseHeader,
+}
+
+func logResponseHeader(r *request.Request) {
+	if r.Config.Logger == nil {
+		return
+	}
+
+	b, err := httputil.DumpResponse(r.HTTPResponse, false)
+	if err != nil {
+		r.Config.Logger.Log(fmt.Sprintf(logRespErrMsg,
+			r.ClientInfo.ServiceName, r.Operation.Name, err))
+		return
+	}
+
+	r.Config.Logger.Log(fmt.Sprintf(logRespMsg,
+		r.ClientInfo.ServiceName, r.Operation.Name, string(b)))
+}

+ 1 - 0
vendor/github.com/aws/aws-sdk-go/aws/client/metadata/client_info.go

@@ -3,6 +3,7 @@ package metadata
 // ClientInfo wraps immutable data from the client.Client structure.
 // ClientInfo wraps immutable data from the client.Client structure.
 type ClientInfo struct {
 type ClientInfo struct {
 	ServiceName   string
 	ServiceName   string
+	ServiceID     string
 	APIVersion    string
 	APIVersion    string
 	Endpoint      string
 	Endpoint      string
 	SigningName   string
 	SigningName   string

+ 15 - 3
vendor/github.com/aws/aws-sdk-go/aws/credentials/credentials.go

@@ -178,7 +178,8 @@ func (e *Expiry) IsExpired() bool {
 type Credentials struct {
 type Credentials struct {
 	creds        Value
 	creds        Value
 	forceRefresh bool
 	forceRefresh bool
-	m            sync.Mutex
+
+	m sync.RWMutex
 
 
 	provider Provider
 	provider Provider
 }
 }
@@ -201,6 +202,17 @@ func NewCredentials(provider Provider) *Credentials {
 // If Credentials.Expire() was called the credentials Value will be force
 // If Credentials.Expire() was called the credentials Value will be force
 // expired, and the next call to Get() will cause them to be refreshed.
 // expired, and the next call to Get() will cause them to be refreshed.
 func (c *Credentials) Get() (Value, error) {
 func (c *Credentials) Get() (Value, error) {
+	// Check the cached credentials first with just the read lock.
+	c.m.RLock()
+	if !c.isExpired() {
+		creds := c.creds
+		c.m.RUnlock()
+		return creds, nil
+	}
+	c.m.RUnlock()
+
+	// Credentials are expired need to retrieve the credentials taking the full
+	// lock.
 	c.m.Lock()
 	c.m.Lock()
 	defer c.m.Unlock()
 	defer c.m.Unlock()
 
 
@@ -234,8 +246,8 @@ func (c *Credentials) Expire() {
 // If the Credentials were forced to be expired with Expire() this will
 // If the Credentials were forced to be expired with Expire() this will
 // reflect that override.
 // reflect that override.
 func (c *Credentials) IsExpired() bool {
 func (c *Credentials) IsExpired() bool {
-	c.m.Lock()
-	defer c.m.Unlock()
+	c.m.RLock()
+	defer c.m.RUnlock()
 
 
 	return c.isExpired()
 	return c.isExpired()
 }
 }

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