فهرست منبع

Merge remote-tracking branch 'grafana/master' into alpha-text2

* grafana/master: (57 commits)
  changelog: adds note for #8253
  use default min interval of 1m for sql datasources
  changelog: adds note about closing #15608
  Fixed scrollbar not visible due to content being added a bit after mount, fixes #15711
  Added comment to Docker file
  moving
  style: add gicon-shield to sidemenu class Closes #15591
  remove `UseBool` since we use `AllCols`
  fix: Move chunk splitting from prod to common so we get the same files in dev as prod
  fix: update datasource in componentDidUpdate Closes #15751
  changelog: add notes about closing #15739
  Moved Server Admin and children to separate menu item on Side Menu (#15592)
  update version to 6.1.0-pre
  Viewers with viewers_can_edit should be able to access /explore (#15787)
  Fixed scrolling issue that caused scroll to be locked to the bottom of a long dashboard, fixes #15712
  reordered import
  Wrapperd playlist controls in clickoutsidewrapper
  Turn off verbose output from tar extraction when building docker files, fixes #15528
  Hide time info switch when no time options are specified
  Made sure that DataSourceOption displays value and fires onChange/onBlur events (#15757)
  ...
ryan 6 سال پیش
والد
کامیت
c9396b2889
73فایلهای تغییر یافته به همراه550 افزوده شده و 287 حذف شده
  1. 2 2
      .circleci/config.yml
  2. 22 0
      CHANGELOG.md
  3. 0 2
      devenv/docker/blocks/influxdb/docker-compose.yaml
  4. 1 0
      docs/sources/features/datasources/prometheus.md
  5. 19 19
      docs/sources/guides/whats-new-in-v6-0.md
  6. 1 0
      docs/sources/http_api/user.md
  7. 2 1
      docs/versions.json
  8. 2 2
      package.json
  9. 18 7
      packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx
  10. 5 3
      packages/grafana-ui/src/components/UnitPicker/UnitPicker.tsx
  11. 1 0
      packages/grafana-ui/src/components/index.ts
  12. 11 1
      packages/grafana-ui/src/types/datasource.ts
  13. 2 2
      packages/grafana-ui/src/types/panel.ts
  14. 1 1
      packages/grafana-ui/src/utils/valueFormats/categories.ts
  15. 2 1
      packaging/docker/Dockerfile
  16. 14 14
      pkg/api/api.go
  17. 17 24
      pkg/api/index.go
  18. 4 4
      pkg/api/org_invite.go
  19. 2 0
      pkg/api/playlist_play.go
  20. 1 0
      pkg/cmd/grafana-server/server.go
  21. 7 1
      pkg/middleware/auth.go
  22. 1 0
      pkg/services/search/models.go
  23. 11 6
      pkg/services/sqlstore/datasource.go
  24. 3 0
      pkg/services/sqlstore/migrations/datasource_mig.go
  25. 1 1
      pkg/tsdb/cloudwatch/metric_find_query.go
  26. 10 0
      public/app/core/specs/file_export.test.ts
  27. 55 0
      public/app/core/utils/errors.test.ts
  28. 1 1
      public/app/core/utils/errors.ts
  29. 14 13
      public/app/core/utils/file_export.ts
  30. 1 1
      public/app/features/admin/AdminEditOrgCtrl.ts
  31. 1 1
      public/app/features/admin/AdminEditUserCtrl.ts
  32. 1 1
      public/app/features/admin/AdminListOrgsCtrl.ts
  33. 1 1
      public/app/features/admin/AdminListUsersCtrl.ts
  34. 1 1
      public/app/features/admin/StyleGuideCtrl.ts
  35. 2 2
      public/app/features/admin/index.ts
  36. 23 20
      public/app/features/dashboard/components/DashNav/DashNav.tsx
  37. 7 1
      public/app/features/dashboard/containers/DashboardPage.tsx
  38. 4 0
      public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap
  39. 4 1
      public/app/features/dashboard/dashgrid/DataPanel.tsx
  40. 3 2
      public/app/features/dashboard/dashgrid/PanelChrome.tsx
  41. 2 1
      public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx
  42. 3 2
      public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx
  43. 9 8
      public/app/features/dashboard/panel_editor/DataSourceOption.tsx
  44. 1 1
      public/app/features/dashboard/panel_editor/EditorTabBody.tsx
  45. 89 69
      public/app/features/dashboard/panel_editor/QueryOptions.tsx
  46. 1 1
      public/app/features/dashboard/panel_editor/VisualizationTab.tsx
  47. 2 2
      public/app/features/dashboard/state/DashboardModel.ts
  48. 2 2
      public/app/features/dashboard/state/PanelModel.ts
  49. 2 1
      public/app/features/datasources/settings/ButtonRow.tsx
  50. 9 3
      public/app/features/datasources/settings/DataSourceSettingsPage.tsx
  51. 1 1
      public/app/features/org/NewOrgCtrl.ts
  52. 1 1
      public/app/plugins/datasource/mssql/datasource.ts
  53. 1 1
      public/app/plugins/datasource/mysql/datasource.ts
  54. 1 1
      public/app/plugins/datasource/postgres/datasource.ts
  55. 18 0
      public/app/plugins/datasource/prometheus/datasource.ts
  56. 15 0
      public/app/plugins/datasource/prometheus/metric_find_query.ts
  57. 18 0
      public/app/plugins/datasource/prometheus/specs/metric_find_query.test.ts
  58. 7 4
      public/app/plugins/panel/gauge/GaugeOptionsBox.tsx
  59. 3 3
      public/app/plugins/panel/gauge/GaugePanel.tsx
  60. 5 5
      public/app/plugins/panel/gauge/GaugePanelEditor.tsx
  61. 1 2
      public/app/plugins/panel/gauge/SingleStatValueEditor.tsx
  62. 1 1
      public/app/plugins/panel/graph/tab_display.html
  63. 3 3
      public/app/plugins/panel/graph2/GraphPanelEditor.tsx
  64. 4 4
      public/app/routes/GrafanaCtrl.ts
  65. 3 0
      public/sass/base/_icons.scss
  66. 3 0
      scripts/build/update_repo/init-deb-repo.sh
  67. 17 0
      scripts/circle-test-mysql.sh
  68. 17 0
      scripts/circle-test-postgres.sh
  69. 13 1
      scripts/webpack/webpack.common.js
  70. 0 19
      scripts/webpack/webpack.dev.js
  71. 0 11
      scripts/webpack/webpack.prod.js
  72. 2 4
      style_guides/frontend.md
  73. 18 0
      yarn.lock

+ 2 - 2
.circleci/config.yml

@@ -35,7 +35,7 @@ jobs:
         - run: cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h 127.0.0.1 -P 3306 -u root -prootpass
         - run: cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h 127.0.0.1 -P 3306 -u root -prootpass
         - run:
         - run:
             name: mysql integration tests
             name: mysql integration tests
-            command: 'GRAFANA_TEST_DB=mysql go test ./pkg/services/sqlstore/... ./pkg/tsdb/mysql/... '
+            command: './scripts/circle-test-mysql.sh'
 
 
   postgres-integration-test:
   postgres-integration-test:
     docker:
     docker:
@@ -54,7 +54,7 @@ jobs:
         - run: 'PGPASSWORD=grafanatest psql -p 5432 -h 127.0.0.1 -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql'
         - run: 'PGPASSWORD=grafanatest psql -p 5432 -h 127.0.0.1 -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql'
         - run:
         - run:
             name: postgres integration tests
             name: postgres integration tests
-            command: 'GRAFANA_TEST_DB=postgres go test ./pkg/services/sqlstore/... ./pkg/tsdb/postgres/...'
+            command: './scripts/circle-test-postgres.sh'
 
 
   codespell:
   codespell:
     docker:
     docker:

+ 22 - 0
CHANGELOG.md

@@ -1,3 +1,25 @@
+# 6.1.0 (unreleased)
+
+### New Features
+* **Prometheus**: adhoc filter support [#8253](https://github.com/grafana/grafana/issues/8253), thx [@mtanda](https://github.com/mtanda)
+
+### Minor
+* **Cloudwatch**: Add AWS RDS MaximumUsedTransactionIDs metric [#15077](https://github.com/grafana/grafana/pull/15077), thx [@activeshadow](https://github.com/activeshadow)
+
+
+### Bug Fixes
+* **Api**: Invalid org invite code [#10506](https://github.com/grafana/grafana/issues/10506)
+* **Datasource**: Handles nil jsondata field gracefully [#14239](https://github.com/grafana/grafana/issues/14239)
+* **Gauge**: Interpolate scoped variables in repeated gauges [#15739](https://github.com/grafana/grafana/issues/15739)
+* **Datasource**: Empty user/password was not updated when updating datasources [#15608](https://github.com/grafana/grafana/pull/15608), thx [@Maddin-619](https://github.com/Maddin-619)
+
+# 6.0.1 (unreleased)
+
+### Bug Fixes
+* **Metrics**: Fixes broken usagestats metrics for /metrics [#15651](https://github.com/grafana/grafana/issues/15651)
+* **Dashboard**: Fixes kiosk mode should have &kiosk appended to the url [#15765](https://github.com/grafana/grafana/issues/15765)
+* **Dashboard**: Fixes kiosk=tv mode with autofitpanels should respect header [#15650](https://github.com/grafana/grafana/issues/15650)
+
 # 6.0.0 stable (2019-02-25)
 # 6.0.0 stable (2019-02-25)
 
 
 ### Bug Fixes
 ### Bug Fixes

+ 0 - 2
devenv/docker/blocks/influxdb/docker-compose.yaml

@@ -1,5 +1,3 @@
-version: '2'
-services:
   influxdb:
   influxdb:
     image: influxdb:latest
     image: influxdb:latest
     container_name: influxdb
     container_name: influxdb

+ 1 - 0
docs/sources/features/datasources/prometheus.md

@@ -68,6 +68,7 @@ provides the following functions you can use in the `Query` input field.
 
 
 Name | Description
 Name | Description
 ---- | --------
 ---- | --------
+*label_names()* | Returns a list of label names.
 *label_values(label)* | Returns a list of label values for the `label` in every metric.
 *label_values(label)* | Returns a list of label values for the `label` in every metric.
 *label_values(metric, label)* | Returns a list of label values for the `label` in the specified metric.
 *label_values(metric, label)* | Returns a list of label values for the `label` in the specified metric.
 *metrics(metric)* | Returns a list of metrics matching the specified `metric` regex.
 *metrics(metric)* | Returns a list of metrics matching the specified `metric` regex.

+ 19 - 19
docs/sources/guides/whats-new-in-v6-0.md

@@ -12,7 +12,7 @@ weight = -11
 
 
 # What's New in Grafana v6.0
 # What's New in Grafana v6.0
 
 
-This update to Grafana introduces a new way of exploring your data, support for log data and tons of other features.
+This update to Grafana introduces a new way of exploring your data, support for log data, and tons of other features.
 
 
 The main highlights are:
 The main highlights are:
 
 
@@ -25,24 +25,24 @@ The main highlights are:
 - [Azure Monitor]({{< relref "#azure-monitor-datasource" >}}) plugin is ported from being an external plugin to being a core datasource
 - [Azure Monitor]({{< relref "#azure-monitor-datasource" >}}) plugin is ported from being an external plugin to being a core datasource
 - [React Plugin]({{< relref "#react-panels-query-editors" >}}) support enables an easier way to build plugins.
 - [React Plugin]({{< relref "#react-panels-query-editors" >}}) support enables an easier way to build plugins.
 - [Named Colors]({{< relref "#named-colors" >}}) in our new improved color picker.
 - [Named Colors]({{< relref "#named-colors" >}}) in our new improved color picker.
-- [Removal of user session storage]({{< relref "#easier-to-deploy-improved-security" >}}) makes Grafana easier to deploy & improves security.
+- [Removal of user session storage]({{< relref "#easier-to-deploy-improved-security" >}}) makes Grafana easier to deploy and improves security.
 
 
 ## Explore
 ## Explore
 
 
 {{< docs-imagebox img="/img/docs/v60/explore_prometheus.png" max-width="800px" class="docs-image--right" caption="Screenshot of the new Explore option in the panel menu" >}}
 {{< docs-imagebox img="/img/docs/v60/explore_prometheus.png" max-width="800px" class="docs-image--right" caption="Screenshot of the new Explore option in the panel menu" >}}
 
 
-Grafana's dashboard UI is all about building dashboards for visualization. **Explore** strips away all the dashboard and panel options so that you can focus on the query & metric exploration. Iterate until you have a working query and then think about building a dashboard. You can also jump from a dashboard panel into **Explore** and from there do some ad-hoc query exporation with the panel queries as a starting point.
+Grafana's dashboard UI is all about building dashboards for visualization. **Explore** strips away all the dashboard and panel options so that you can focus on the query and metric exploration. Iterate until you have a working query and then think about building a dashboard. You can also jump from a dashboard panel into **Explore** and from there do some ad-hoc query exporation with the panel queries as a starting point.
 
 
 For infrastructure monitoring and incident response, you no longer need to switch to other tools to debug what went wrong. **Explore** allows you to dig deeper into your metrics and logs to find the cause. Grafana's new logging datasource, [Loki](https://github.com/grafana/loki) is tightly integrated into Explore and allows you to correlate metrics and logs by viewing them side-by-side.
 For infrastructure monitoring and incident response, you no longer need to switch to other tools to debug what went wrong. **Explore** allows you to dig deeper into your metrics and logs to find the cause. Grafana's new logging datasource, [Loki](https://github.com/grafana/loki) is tightly integrated into Explore and allows you to correlate metrics and logs by viewing them side-by-side.
 
 
 **Explore** is a new paradigm for Grafana. It creates a new interactive debugging workflow that integrates two pillars
 **Explore** is a new paradigm for Grafana. It creates a new interactive debugging workflow that integrates two pillars
-of observability - metrics and logs. Explore works with every datasource but for Prometheus we have customized the
+of observabilitymetrics and logs. Explore works with every datasource but for Prometheus we have customized the
 query editor and the experience to provide the best possible exploration UX.
 query editor and the experience to provide the best possible exploration UX.
 
 
 ### Explore and Prometheus
 ### Explore and Prometheus
 
 
 Explore features a new [Prometheus query editor](/features/explore/#prometheus-specific-features). This new editor has improved autocomplete, metric tree selector,
 Explore features a new [Prometheus query editor](/features/explore/#prometheus-specific-features). This new editor has improved autocomplete, metric tree selector,
-integrations with the Explore table view for easy label filtering and useful query hints that can automatically apply
+integrations with the Explore table view for easy label filtering, and useful query hints that can automatically apply
 functions to your query. There is also integration between Prometheus and Grafana Loki (see more about Loki below) that
 functions to your query. There is also integration between Prometheus and Grafana Loki (see more about Loki below) that
 enabled jumping between metrics query and logs query with preserved label filters.
 enabled jumping between metrics query and logs query with preserved label filters.
 
 
@@ -78,8 +78,8 @@ for other log sources to Explore and the next planned integration is Elasticsear
 ## New Panel Editor
 ## New Panel Editor
 
 
 Grafana v6.0 has a completely redesigned UX around editing panels. You can now resize the visualization area if you want
 Grafana v6.0 has a completely redesigned UX around editing panels. You can now resize the visualization area if you want
-more space for queries & options and vice versa. You can now also change visualization (panel type) from within the new
-panel edit mode. No need to add a new panel to try out different visualizations! Checkout the
+more space for queries/options and vice versa. You can now also change visualization (panel type) from within the new
+panel edit mode. No need to add a new panel to try out different visualizations! Check out the
 video below to see the new Panel Editor in action.
 video below to see the new Panel Editor in action.
 
 
 <div class="medium-6 columns">
 <div class="medium-6 columns">
@@ -94,7 +94,7 @@ video below to see the new Panel Editor in action.
 ### Gauge Panel
 ### Gauge Panel
 
 
 We have created a new separate Gauge panel as we felt having this visualization be a hidden option in the Singlestat panel
 We have created a new separate Gauge panel as we felt having this visualization be a hidden option in the Singlestat panel
-was not ideal. When it supports 100% of the Singlestat Gauge features we plan to add a migration so all
+was not ideal. When it supports 100% of the Singlestat Gauge features, we plan to add a migration so all
 singlestats that use it become Gauge panels instead. This new panel contains a new **Threshold** editor that we will
 singlestats that use it become Gauge panels instead. This new panel contains a new **Threshold** editor that we will
 continue to refine and start using in other panels.
 continue to refine and start using in other panels.
 
 
@@ -105,7 +105,7 @@ continue to refine and start using in other panels.
 ### React Panels & Query Editors
 ### React Panels & Query Editors
 
 
 A major part of all the work that has gone into Grafana v6.0 has been on the migration to React. This investment
 A major part of all the work that has gone into Grafana v6.0 has been on the migration to React. This investment
-is part of the future proofing of Grafana's code base and ecosystem. Starting in v6.0 **Panels** and **Data
+is part of the future-proofing of Grafana's code base and ecosystem. Starting in v6.0 **Panels** and **Data
 source** plugins can be written in React using our published `@grafana/ui` sdk library. More information on this
 source** plugins can be written in React using our published `@grafana/ui` sdk library. More information on this
 will be shared soon.
 will be shared soon.
 
 
@@ -120,7 +120,7 @@ To get started read the guide: [Using Google Stackdriver in Grafana](/features/d
 
 
 ## Azure Monitor Datasource
 ## Azure Monitor Datasource
 
 
-One of the goals of the Grafana v6.0 release is to add support for the three major clouds. Amazon Cloudwatch has been a core datasource for years and Google Stackdriver is also now supported. We developed an external plugin for Azure Monitor last year and for this release the [plugin](https://grafana.com/plugins/grafana-azure-monitor-datasource) is being moved into Grafana to be one of the built-in datasources. For users of the external plugin, Grafana will automatically start using the built-in version. As a core datasource, the Azure Monitor datasource is able to get alerting support, in the 6.0 release alerting is supported for the Azure Monitor service, with the rest to follow.
+One of the goals of the Grafana v6.0 release is to add support for the three major clouds. Amazon CloudWatch has been a core datasource for years and Google Stackdriver is also now supported. We developed an external plugin for Azure Monitor last year and for this release the [plugin](https://grafana.com/plugins/grafana-azure-monitor-datasource) is being moved into Grafana to be one of the built-in datasources. For users of the external plugin, Grafana will automatically start using the built-in version. As a core datasource, the Azure Monitor datasource is able to get alerting support, in the 6.0 release alerting is supported for the Azure Monitor service, with the rest to follow.
 
 
 The Azure Monitor datasource integrates four Azure services with Grafana - Azure Monitor, Azure Log Analytics, Azure Application Insights and Azure Application Insights Analytics.
 The Azure Monitor datasource integrates four Azure services with Grafana - Azure Monitor, Azure Log Analytics, Azure Application Insights and Azure Application Insights Analytics.
 
 
@@ -128,15 +128,15 @@ Please read [Using Azure Monitor in Grafana documentation](/features/datasources
 
 
 ## Provisioning support for alert notifiers
 ## Provisioning support for alert notifiers
 
 
-Grafana now added support for provisioning alert notifiers from configuration files. Allowing operators to provision notifiers without using the UI or the API. A new field called `uid` has been introduced which is a string identifier that the administrator can set themselves. Same kind of identifier used for dashboards since v5.0. This feature makes it possible to use the same notifier configuration in multiple environments and refer to notifiers in dashboard json by a string identifier instead of the numeric id which depends on insert order and how many notifiers that exists in the instance.
+Grafana now has support for provisioning alert notifiers from configuration files, allowing operators to provision notifiers without using the UI or the API. A new field called `uid` has been introduced which is a string identifier that the administrator can set themselves. This is the same kind of identifier used for dashboards since v5.0. This feature makes it possible to use the same notifier configuration in multiple environments and refer to notifiers in dashboard json by a string identifier instead of the numeric id which depends on insert order and how many notifiers exist in the instance.
 
 
 ## Easier to deploy & improved security
 ## Easier to deploy & improved security
 
 
-Grafana 6.0 removes the need of configuring and setup of additional storage for [user sessions](/tutorials/ha_setup/#user-sessions). This should make it easier to deploy and operate Grafana in a
-high availability setup and/or if you're using a stateless user session storage like Redis, Memcache, Postgres or MySQL.
+Grafana 6.0 removes the need to configure and set up additional storage for [user sessions](/tutorials/ha_setup/#user-sessions). This should make it easier to deploy and operate Grafana in a
+high availability setup and/or if you're using a stateless user session store like Redis, Memcache, Postgres or MySQL.
 
 
-Instead of user sessions a solution based on short-lived tokens that are rotated frequently have been implemented. This also replaces the old "remember me cookie"
-solution, which allowed a user to be logged in between browser sessions, and which have been subject to several security holes throughout the years.
+Instead of user sessions, we've implemented a solution based on short-lived tokens that are rotated frequently. This also replaces the old "remember me cookie"
+solution, which allowed a user to be logged in between browser sessions and which have been subject to several security holes throughout the years.
 Read more about the short-lived token solution and how to configure it [here](/auth/overview/#login-and-short-lived-tokens).
 Read more about the short-lived token solution and how to configure it [here](/auth/overview/#login-and-short-lived-tokens).
 
 
 > Please note that due to these changes, all users will be required to login upon next visit after upgrade.
 > Please note that due to these changes, all users will be required to login upon next visit after upgrade.
@@ -146,15 +146,15 @@ Besides these changes we have also made security improvements regarding Cross-Si
 * Cookies are per default using the [SameSite](/installation/configuration/#cookie-samesite) attribute to protect against CSRF attacks
 * Cookies are per default using the [SameSite](/installation/configuration/#cookie-samesite) attribute to protect against CSRF attacks
 * Script tags in text panels are per default [disabled](/installation/configuration/#disable-sanitize-html) to protect against XSS attacks
 * Script tags in text panels are per default [disabled](/installation/configuration/#disable-sanitize-html) to protect against XSS attacks
 
 
-> If you're using [Auth Proxy Authentication](/auth/auth-proxy/) you still need to have user sessions setup and configured
-but our goal is to remove this requirements in a near future.
+> If you're using [Auth Proxy Authentication](/auth/auth-proxy/) you still need to have user sessions set up and configured
+but our goal is to remove this requirement in the near future.
 
 
 ## Named Colors
 ## Named Colors
 
 
 {{< docs-imagebox img="/img/docs/v60/named_colors.png" max-width="400px" class="docs-image--right" caption="Named Colors" >}}
 {{< docs-imagebox img="/img/docs/v60/named_colors.png" max-width="400px" class="docs-image--right" caption="Named Colors" >}}
 
 
 We have updated the color picker to show named colors and primary colors. We hope this will improve accessibility and
 We have updated the color picker to show named colors and primary colors. We hope this will improve accessibility and
-helps making colors more consistent across dashboards. We hope to do more in this color picker in the future, like show
+helps making colors more consistent across dashboards. We hope to do more in this color picker in the future, like showing
 colors used in the dashboard.
 colors used in the dashboard.
 
 
 Named colors also enables Grafana to adapt colors to the current theme.
 Named colors also enables Grafana to adapt colors to the current theme.
@@ -163,7 +163,7 @@ Named colors also enables Grafana to adapt colors to the current theme.
 
 
 ## Other features
 ## Other features
 
 
-- The ElasticSearch datasource now supports [bucket script pipeline aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-bucket-script-aggregation.html). This gives the ability to do per bucket computations like the difference or ratio between two metrics.
+- The ElasticSearch datasource now supports [bucket script pipeline aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-bucket-script-aggregation.html). This gives the ability to do per-bucket computations like the difference or ratio between two metrics.
 - Support for Google Hangouts Chat alert notifications
 - Support for Google Hangouts Chat alert notifications
 - New built in template variables for the current time range in `$__from` and `$__to`
 - New built in template variables for the current time range in `$__from` and `$__to`
 
 

+ 1 - 0
docs/sources/http_api/user.md

@@ -156,6 +156,7 @@ HTTP/1.1 200
 Content-Type: application/json
 Content-Type: application/json
 
 
 {
 {
+  "id": 1,
   "email": "user@mygraf.com",
   "email": "user@mygraf.com",
   "name": "admin",
   "name": "admin",
   "login": "admin",
   "login": "admin",

+ 2 - 1
docs/versions.json

@@ -1,5 +1,6 @@
 [
 [
-  { "version": "v5.4", "path": "/", "archived": false, "current": true },
+  { "version": "v6.0", "path": "/", "archived": false, "current": true },
+  { "version": "v5.4", "path": "/v5.4", "archived": true },
   { "version": "v5.3", "path": "/v5.3", "archived": true },
   { "version": "v5.3", "path": "/v5.3", "archived": true },
   { "version": "v5.2", "path": "/v5.2", "archived": true },
   { "version": "v5.2", "path": "/v5.2", "archived": true },
   { "version": "v5.1", "path": "/v5.1", "archived": true },
   { "version": "v5.1", "path": "/v5.1", "archived": true },

+ 2 - 2
package.json

@@ -5,7 +5,7 @@
     "company": "Grafana Labs"
     "company": "Grafana Labs"
   },
   },
   "name": "grafana",
   "name": "grafana",
-  "version": "6.0.0-pre3",
+  "version": "6.1.0-pre",
   "repository": {
   "repository": {
     "type": "git",
     "type": "git",
     "url": "http://github.com/grafana/grafana.git"
     "url": "http://github.com/grafana/grafana.git"
@@ -162,7 +162,7 @@
   "license": "Apache-2.0",
   "license": "Apache-2.0",
   "dependencies": {
   "dependencies": {
     "@babel/polyfill": "^7.0.0",
     "@babel/polyfill": "^7.0.0",
-    "@torkelo/react-select": "2.1.1",
+    "@torkelo/react-select": "2.4.1",
     "@types/reselect": "^2.2.0",
     "@types/reselect": "^2.2.0",
     "angular": "1.6.6",
     "angular": "1.6.6",
     "angular-bindonce": "0.3.1",
     "angular-bindonce": "0.3.1",

+ 18 - 7
packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx

@@ -1,4 +1,4 @@
-import React, { PureComponent } from 'react';
+import React, { Component } from 'react';
 import isNil from 'lodash/isNil';
 import isNil from 'lodash/isNil';
 import classNames from 'classnames';
 import classNames from 'classnames';
 import Scrollbars from 'react-custom-scrollbars';
 import Scrollbars from 'react-custom-scrollbars';
@@ -15,12 +15,13 @@ interface Props {
   scrollTop?: number;
   scrollTop?: number;
   setScrollTop: (event: any) => void;
   setScrollTop: (event: any) => void;
   autoHeightMin?: number | string;
   autoHeightMin?: number | string;
+  updateAfterMountMs?: number;
 }
 }
 
 
 /**
 /**
  * Wraps component into <Scrollbars> component from `react-custom-scrollbars`
  * Wraps component into <Scrollbars> component from `react-custom-scrollbars`
  */
  */
-export class CustomScrollbar extends PureComponent<Props> {
+export class CustomScrollbar extends Component<Props> {
   static defaultProps: Partial<Props> = {
   static defaultProps: Partial<Props> = {
     autoHide: false,
     autoHide: false,
     autoHideTimeout: 200,
     autoHideTimeout: 200,
@@ -42,16 +43,26 @@ export class CustomScrollbar extends PureComponent<Props> {
     const ref = this.ref.current;
     const ref = this.ref.current;
 
 
     if (ref && !isNil(this.props.scrollTop)) {
     if (ref && !isNil(this.props.scrollTop)) {
-      if (this.props.scrollTop > 10000) {
-        ref.scrollToBottom();
-      } else {
-        ref.scrollTop(this.props.scrollTop);
-      }
+      ref.scrollTop(this.props.scrollTop);
     }
     }
   }
   }
 
 
   componentDidMount() {
   componentDidMount() {
     this.updateScroll();
     this.updateScroll();
+
+    // this logic is to make scrollbar visible when content is added body after mount
+    if (this.props.updateAfterMountMs) {
+      setTimeout(() => this.updateAfterMount(), this.props.updateAfterMountMs);
+    }
+  }
+
+  updateAfterMount() {
+    if (this.ref && this.ref.current) {
+      const scrollbar = this.ref.current as any;
+      if (scrollbar.update) {
+        scrollbar.update();
+      }
+    }
   }
   }
 
 
   componentDidUpdate() {
   componentDidUpdate() {

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

@@ -1,6 +1,8 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
-import { getValueFormats } from '@grafana/ui';
-import { Select } from '@grafana/ui';
+
+import { Select } from '..';
+
+import { getValueFormats } from '../../utils';
 
 
 interface Props {
 interface Props {
   onChange: (item: any) => void;
   onChange: (item: any) => void;
@@ -8,7 +10,7 @@ interface Props {
   width?: number;
   width?: number;
 }
 }
 
 
-export default class UnitPicker extends PureComponent<Props> {
+export class UnitPicker extends PureComponent<Props> {
   static defaultProps = {
   static defaultProps = {
     width: 12,
     width: 12,
   };
   };

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

@@ -26,3 +26,4 @@ export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';
 export { Gauge } from './Gauge/Gauge';
 export { Gauge } from './Gauge/Gauge';
 export { Switch } from './Switch/Switch';
 export { Switch } from './Switch/Switch';
 export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult';
 export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult';
+export { UnitPicker } from './UnitPicker/UnitPicker';

+ 11 - 1
packages/grafana-ui/src/types/datasource.ts

@@ -39,6 +39,16 @@ export interface DataQueryError {
   statusText?: string;
   statusText?: string;
 }
 }
 
 
+export interface ScopedVar {
+  text: any;
+  value: any;
+  [key: string]: any;
+}
+
+export interface ScopedVars {
+  [key: string]: ScopedVar;
+}
+
 export interface DataQueryOptions<TQuery extends DataQuery = DataQuery> {
 export interface DataQueryOptions<TQuery extends DataQuery = DataQuery> {
   timezone: string;
   timezone: string;
   range: TimeRange;
   range: TimeRange;
@@ -50,7 +60,7 @@ export interface DataQueryOptions<TQuery extends DataQuery = DataQuery> {
   interval: string;
   interval: string;
   intervalMs: number;
   intervalMs: number;
   maxDataPoints: number;
   maxDataPoints: number;
-  scopedVars: object;
+  scopedVars: ScopedVars;
 }
 }
 
 
 export interface QueryFix {
 export interface QueryFix {

+ 2 - 2
packages/grafana-ui/src/types/panel.ts

@@ -12,7 +12,7 @@ export interface PanelProps<T = any> {
   renderCounter: number;
   renderCounter: number;
   width: number;
   width: number;
   height: number;
   height: number;
-  onInterpolate: InterpolateFunction;
+  replaceVariables: InterpolateFunction;
 }
 }
 
 
 export interface PanelData {
 export interface PanelData {
@@ -22,7 +22,7 @@ export interface PanelData {
 
 
 export interface PanelEditorProps<T = any> {
 export interface PanelEditorProps<T = any> {
   options: T;
   options: T;
-  onChange: (options: T) => void;
+  onOptionsChange: (options: T) => void;
 }
 }
 
 
 export class ReactPanelPlugin<TOptions = any> {
 export class ReactPanelPlugin<TOptions = any> {

+ 1 - 1
packages/grafana-ui/src/utils/valueFormats/categories.ts

@@ -125,7 +125,7 @@ export const getCategories = (): ValueFormatCategory[] => [
   {
   {
     name: 'Data (Metric)',
     name: 'Data (Metric)',
     formats: [
     formats: [
-      { name: 'bits', id: 'decbits', fn: decimalSIPrefix('d') },
+      { name: 'bits', id: 'decbits', fn: decimalSIPrefix('b') },
       { name: 'bytes', id: 'decbytes', fn: decimalSIPrefix('B') },
       { name: 'bytes', id: 'decbytes', fn: decimalSIPrefix('B') },
       { name: 'kilobytes', id: 'deckbytes', fn: decimalSIPrefix('B', 1) },
       { name: 'kilobytes', id: 'deckbytes', fn: decimalSIPrefix('B', 1) },
       { name: 'megabytes', id: 'decmbytes', fn: decimalSIPrefix('B', 2) },
       { name: 'megabytes', id: 'decmbytes', fn: decimalSIPrefix('B', 2) },

+ 2 - 1
packaging/docker/Dockerfile

@@ -9,7 +9,8 @@ RUN apt-get update && apt-get install -qq -y tar && \
 
 
 COPY ${GRAFANA_TGZ} /tmp/grafana.tar.gz
 COPY ${GRAFANA_TGZ} /tmp/grafana.tar.gz
 
 
-RUN mkdir /tmp/grafana && tar xfvz /tmp/grafana.tar.gz --strip-components=1 -C /tmp/grafana
+# Change to tar xfzv to make tar print every file it extracts
+RUN mkdir /tmp/grafana && tar xfz /tmp/grafana.tar.gz --strip-components=1 -C /tmp/grafana
 
 
 ARG BASE_IMAGE=debian:stretch-slim
 ARG BASE_IMAGE=debian:stretch-slim
 FROM ${BASE_IMAGE}
 FROM ${BASE_IMAGE}

+ 14 - 14
pkg/api/api.go

@@ -33,17 +33,17 @@ func (hs *HTTPServer) registerRoutes() {
 	r.Get("/profile/", reqSignedIn, hs.Index)
 	r.Get("/profile/", reqSignedIn, hs.Index)
 	r.Get("/profile/password", reqSignedIn, hs.Index)
 	r.Get("/profile/password", reqSignedIn, hs.Index)
 	r.Get("/profile/switch-org/:id", reqSignedIn, hs.ChangeActiveOrgAndRedirectToHome)
 	r.Get("/profile/switch-org/:id", reqSignedIn, hs.ChangeActiveOrgAndRedirectToHome)
-	r.Get("/org/", reqSignedIn, hs.Index)
-	r.Get("/org/new", reqSignedIn, hs.Index)
-	r.Get("/datasources/", reqSignedIn, hs.Index)
-	r.Get("/datasources/new", reqSignedIn, hs.Index)
-	r.Get("/datasources/edit/*", reqSignedIn, hs.Index)
-	r.Get("/org/users", reqSignedIn, hs.Index)
-	r.Get("/org/users/new", reqSignedIn, hs.Index)
-	r.Get("/org/users/invite", reqSignedIn, hs.Index)
-	r.Get("/org/teams", reqSignedIn, hs.Index)
-	r.Get("/org/teams/*", reqSignedIn, hs.Index)
-	r.Get("/org/apikeys/", reqSignedIn, hs.Index)
+	r.Get("/org/", reqOrgAdmin, hs.Index)
+	r.Get("/org/new", reqGrafanaAdmin, hs.Index)
+	r.Get("/datasources/", reqOrgAdmin, hs.Index)
+	r.Get("/datasources/new", reqOrgAdmin, hs.Index)
+	r.Get("/datasources/edit/*", reqOrgAdmin, hs.Index)
+	r.Get("/org/users", reqOrgAdmin, hs.Index)
+	r.Get("/org/users/new", reqOrgAdmin, hs.Index)
+	r.Get("/org/users/invite", reqOrgAdmin, hs.Index)
+	r.Get("/org/teams", reqOrgAdmin, hs.Index)
+	r.Get("/org/teams/*", reqOrgAdmin, hs.Index)
+	r.Get("/org/apikeys/", reqOrgAdmin, hs.Index)
 	r.Get("/dashboard/import/", reqSignedIn, hs.Index)
 	r.Get("/dashboard/import/", reqSignedIn, hs.Index)
 	r.Get("/configuration", reqGrafanaAdmin, hs.Index)
 	r.Get("/configuration", reqGrafanaAdmin, hs.Index)
 	r.Get("/admin", reqGrafanaAdmin, hs.Index)
 	r.Get("/admin", reqGrafanaAdmin, hs.Index)
@@ -73,12 +73,12 @@ func (hs *HTTPServer) registerRoutes() {
 	r.Get("/dashboards/", reqSignedIn, hs.Index)
 	r.Get("/dashboards/", reqSignedIn, hs.Index)
 	r.Get("/dashboards/*", reqSignedIn, hs.Index)
 	r.Get("/dashboards/*", reqSignedIn, hs.Index)
 
 
-	r.Get("/explore", reqEditorRole, hs.Index)
+	r.Get("/explore", reqSignedIn, middleware.EnsureEditorOrViewerCanEdit, hs.Index)
 
 
 	r.Get("/playlists/", reqSignedIn, hs.Index)
 	r.Get("/playlists/", reqSignedIn, hs.Index)
 	r.Get("/playlists/*", reqSignedIn, hs.Index)
 	r.Get("/playlists/*", reqSignedIn, hs.Index)
-	r.Get("/alerting/", reqSignedIn, hs.Index)
-	r.Get("/alerting/*", reqSignedIn, hs.Index)
+	r.Get("/alerting/", reqEditorRole, hs.Index)
+	r.Get("/alerting/*", reqEditorRole, hs.Index)
 
 
 	// sign up
 	// sign up
 	r.Get("/signup", hs.Index)
 	r.Get("/signup", hs.Index)

+ 17 - 24
pkg/api/index.go

@@ -307,33 +307,26 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
 			}
 			}
 		}
 		}
 
 
-		if c.OrgRole == m.ROLE_ADMIN && c.IsGrafanaAdmin {
-			cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
-				Divider: true, HideFromTabs: true, Id: "admin-divider", Text: "Text",
-			})
-		}
-
-		if c.IsGrafanaAdmin {
-			cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
-				Text:         "Server Admin",
-				HideFromTabs: true,
-				SubTitle:     "Manage all users & orgs",
-				Id:           "admin",
-				Icon:         "gicon gicon-shield",
-				Url:          setting.AppSubUrl + "/admin/users",
-				Children: []*dtos.NavLink{
-					{Text: "Users", Id: "global-users", Url: setting.AppSubUrl + "/admin/users", Icon: "gicon gicon-user"},
-					{Text: "Orgs", Id: "global-orgs", Url: setting.AppSubUrl + "/admin/orgs", Icon: "gicon gicon-org"},
-					{Text: "Settings", Id: "server-settings", Url: setting.AppSubUrl + "/admin/settings", Icon: "gicon gicon-preferences"},
-					{Text: "Stats", Id: "server-stats", Url: setting.AppSubUrl + "/admin/stats", Icon: "fa fa-fw fa-bar-chart"},
-					{Text: "Style Guide", Id: "styleguide", Url: setting.AppSubUrl + "/styleguide", Icon: "fa fa-fw fa-eyedropper"},
-				},
-			})
-		}
-
 		data.NavTree = append(data.NavTree, cfgNode)
 		data.NavTree = append(data.NavTree, cfgNode)
 	}
 	}
 
 
+	if c.IsGrafanaAdmin {
+		data.NavTree = append(data.NavTree, &dtos.NavLink{
+			Text:         "Server Admin",
+			SubTitle:     "Manage all users & orgs",
+			HideFromTabs: true,
+			Id:           "admin",
+			Icon:         "gicon gicon-shield",
+			Url:          setting.AppSubUrl + "/admin/users",
+			Children: []*dtos.NavLink{
+				{Text: "Users", Id: "global-users", Url: setting.AppSubUrl + "/admin/users", Icon: "gicon gicon-user"},
+				{Text: "Orgs", Id: "global-orgs", Url: setting.AppSubUrl + "/admin/orgs", Icon: "gicon gicon-org"},
+				{Text: "Settings", Id: "server-settings", Url: setting.AppSubUrl + "/admin/settings", Icon: "gicon gicon-preferences"},
+				{Text: "Stats", Id: "server-stats", Url: setting.AppSubUrl + "/admin/stats", Icon: "fa fa-fw fa-bar-chart"},
+			},
+		})
+	}
+
 	data.NavTree = append(data.NavTree, &dtos.NavLink{
 	data.NavTree = append(data.NavTree, &dtos.NavLink{
 		Text:         "Help",
 		Text:         "Help",
 		SubTitle:     fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, setting.BuildVersion, setting.BuildCommit),
 		SubTitle:     fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, setting.BuildVersion, setting.BuildCommit),

+ 4 - 4
pkg/api/org_invite.go

@@ -27,6 +27,10 @@ func GetPendingOrgInvites(c *m.ReqContext) Response {
 }
 }
 
 
 func AddOrgInvite(c *m.ReqContext, inviteDto dtos.AddInviteForm) Response {
 func AddOrgInvite(c *m.ReqContext, inviteDto dtos.AddInviteForm) Response {
+	if setting.DisableLoginForm {
+		return Error(400, "Cannot invite when login is disabled.", nil)
+	}
+
 	if !inviteDto.Role.IsValid() {
 	if !inviteDto.Role.IsValid() {
 		return Error(400, "Invalid role specified", nil)
 		return Error(400, "Invalid role specified", nil)
 	}
 	}
@@ -37,10 +41,6 @@ func AddOrgInvite(c *m.ReqContext, inviteDto dtos.AddInviteForm) Response {
 		if err != m.ErrUserNotFound {
 		if err != m.ErrUserNotFound {
 			return Error(500, "Failed to query db for existing user check", err)
 			return Error(500, "Failed to query db for existing user check", err)
 		}
 		}
-
-		if setting.DisableLoginForm {
-			return Error(401, "User could not be found", nil)
-		}
 	} else {
 	} else {
 		return inviteExistingUserToOrg(c, userQuery.Result, &inviteDto)
 		return inviteExistingUserToOrg(c, userQuery.Result, &inviteDto)
 	}
 	}

+ 2 - 0
pkg/api/playlist_play.go

@@ -52,8 +52,10 @@ func populateDashboardsByTag(orgID int64, signedInUser *m.SignedInUser, dashboar
 			for _, item := range searchQuery.Result {
 			for _, item := range searchQuery.Result {
 				result = append(result, dtos.PlaylistDashboard{
 				result = append(result, dtos.PlaylistDashboard{
 					Id:    item.Id,
 					Id:    item.Id,
+					Slug:  item.Slug,
 					Title: item.Title,
 					Title: item.Title,
 					Uri:   item.Uri,
 					Uri:   item.Uri,
+					Url:   m.GetDashboardUrl(item.Uid, item.Slug),
 					Order: dashboardTagOrder[tag],
 					Order: dashboardTagOrder[tag],
 				})
 				})
 			}
 			}

+ 1 - 0
pkg/cmd/grafana-server/server.go

@@ -31,6 +31,7 @@ import (
 	_ "github.com/grafana/grafana/pkg/infra/metrics"
 	_ "github.com/grafana/grafana/pkg/infra/metrics"
 	_ "github.com/grafana/grafana/pkg/infra/serverlock"
 	_ "github.com/grafana/grafana/pkg/infra/serverlock"
 	_ "github.com/grafana/grafana/pkg/infra/tracing"
 	_ "github.com/grafana/grafana/pkg/infra/tracing"
+	_ "github.com/grafana/grafana/pkg/infra/usagestats"
 	_ "github.com/grafana/grafana/pkg/plugins"
 	_ "github.com/grafana/grafana/pkg/plugins"
 	_ "github.com/grafana/grafana/pkg/services/alerting"
 	_ "github.com/grafana/grafana/pkg/services/alerting"
 	_ "github.com/grafana/grafana/pkg/services/auth"
 	_ "github.com/grafana/grafana/pkg/services/auth"

+ 7 - 1
pkg/middleware/auth.go

@@ -4,7 +4,7 @@ import (
 	"net/url"
 	"net/url"
 	"strings"
 	"strings"
 
 
-	"gopkg.in/macaron.v1"
+	macaron "gopkg.in/macaron.v1"
 
 
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
@@ -52,6 +52,12 @@ func notAuthorized(c *m.ReqContext) {
 	c.Redirect(setting.AppSubUrl + "/login")
 	c.Redirect(setting.AppSubUrl + "/login")
 }
 }
 
 
+func EnsureEditorOrViewerCanEdit(c *m.ReqContext) {
+	if !c.SignedInUser.HasRole(m.ROLE_EDITOR) && !setting.ViewersCanEdit {
+		accessForbidden(c)
+	}
+}
+
 func RoleAuth(roles ...m.RoleType) macaron.Handler {
 func RoleAuth(roles ...m.RoleType) macaron.Handler {
 	return func(c *m.ReqContext) {
 	return func(c *m.ReqContext) {
 		ok := false
 		ok := false

+ 1 - 0
pkg/services/search/models.go

@@ -17,6 +17,7 @@ type Hit struct {
 	Title       string   `json:"title"`
 	Title       string   `json:"title"`
 	Uri         string   `json:"uri"`
 	Uri         string   `json:"uri"`
 	Url         string   `json:"url"`
 	Url         string   `json:"url"`
+	Slug        string   `json:"slug"`
 	Type        HitType  `json:"type"`
 	Type        HitType  `json:"type"`
 	Tags        []string `json:"tags"`
 	Tags        []string `json:"tags"`
 	IsStarred   bool     `json:"isStarred"`
 	IsStarred   bool     `json:"isStarred"`

+ 11 - 6
pkg/services/sqlstore/datasource.go

@@ -3,6 +3,8 @@ package sqlstore
 import (
 import (
 	"time"
 	"time"
 
 
+	"github.com/grafana/grafana/pkg/components/simplejson"
+
 	"github.com/go-xorm/xorm"
 	"github.com/go-xorm/xorm"
 
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
@@ -95,6 +97,10 @@ func AddDataSource(cmd *m.AddDataSourceCommand) error {
 			return m.ErrDataSourceNameExists
 			return m.ErrDataSourceNameExists
 		}
 		}
 
 
+		if cmd.JsonData == nil {
+			cmd.JsonData = simplejson.New()
+		}
+
 		ds := &m.DataSource{
 		ds := &m.DataSource{
 			OrgId:             cmd.OrgId,
 			OrgId:             cmd.OrgId,
 			Name:              cmd.Name,
 			Name:              cmd.Name,
@@ -142,6 +148,10 @@ func updateIsDefaultFlag(ds *m.DataSource, sess *DBSession) error {
 
 
 func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
 func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
 	return inTransaction(func(sess *DBSession) error {
 	return inTransaction(func(sess *DBSession) error {
+		if cmd.JsonData == nil {
+			cmd.JsonData = simplejson.New()
+		}
+
 		ds := &m.DataSource{
 		ds := &m.DataSource{
 			Id:                cmd.Id,
 			Id:                cmd.Id,
 			OrgId:             cmd.OrgId,
 			OrgId:             cmd.OrgId,
@@ -164,11 +174,6 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
 			Version:           cmd.Version + 1,
 			Version:           cmd.Version + 1,
 		}
 		}
 
 
-		sess.UseBool("is_default")
-		sess.UseBool("basic_auth")
-		sess.UseBool("with_credentials")
-		sess.UseBool("read_only")
-
 		var updateSession *xorm.Session
 		var updateSession *xorm.Session
 		if cmd.Version != 0 {
 		if cmd.Version != 0 {
 			// the reason we allow cmd.version > db.version is make it possible for people to force
 			// the reason we allow cmd.version > db.version is make it possible for people to force
@@ -180,7 +185,7 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
 			updateSession = sess.Where("id=? and org_id=?", ds.Id, ds.OrgId)
 			updateSession = sess.Where("id=? and org_id=?", ds.Id, ds.OrgId)
 		}
 		}
 
 
-		affected, err := updateSession.Update(ds)
+		affected, err := updateSession.AllCols().Omit("created").Update(ds)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}

+ 3 - 0
pkg/services/sqlstore/migrations/datasource_mig.go

@@ -130,4 +130,7 @@ func addDataSourceMigration(mg *Migrator) {
 
 
 	const migrateLoggingToLoki = `UPDATE data_source SET type = 'loki' WHERE type = 'logging'`
 	const migrateLoggingToLoki = `UPDATE data_source SET type = 'loki' WHERE type = 'logging'`
 	mg.AddMigration("Migrate logging ds to loki ds", NewRawSqlMigration(migrateLoggingToLoki))
 	mg.AddMigration("Migrate logging ds to loki ds", NewRawSqlMigration(migrateLoggingToLoki))
+
+	const setEmptyJSONWhereNullJSON = `UPDATE data_source SET json_data = '{}' WHERE json_data is null`
+	mg.AddMigration("Update json_data with nulls", NewRawSqlMigration(setEmptyJSONWhereNullJSON))
 }
 }

+ 1 - 1
pkg/tsdb/cloudwatch/metric_find_query.go

@@ -101,7 +101,7 @@ func init() {
 		"AWS/NetworkELB":       {"ActiveFlowCount", "ConsumedLCUs", "HealthyHostCount", "NewFlowCount", "ProcessedBytes", "TCP_Client_Reset_Count", "TCP_ELB_Reset_Count", "TCP_Target_Reset_Count", "UnHealthyHostCount"},
 		"AWS/NetworkELB":       {"ActiveFlowCount", "ConsumedLCUs", "HealthyHostCount", "NewFlowCount", "ProcessedBytes", "TCP_Client_Reset_Count", "TCP_ELB_Reset_Count", "TCP_Target_Reset_Count", "UnHealthyHostCount"},
 		"AWS/OpsWorks":         {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"},
 		"AWS/OpsWorks":         {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"},
 		"AWS/Redshift":         {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "QueriesCompletedPerSecond", "QueryDuration", "QueryRuntimeBreakdown", "ReadIOPS", "ReadLatency", "ReadThroughput", "WLMQueriesCompletedPerSecond", "WLMQueryDuration", "WLMQueueLength", "WriteIOPS", "WriteLatency", "WriteThroughput"},
 		"AWS/Redshift":         {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "QueriesCompletedPerSecond", "QueryDuration", "QueryRuntimeBreakdown", "ReadIOPS", "ReadLatency", "ReadThroughput", "WLMQueriesCompletedPerSecond", "WLMQueryDuration", "WLMQueueLength", "WriteIOPS", "WriteLatency", "WriteThroughput"},
-		"AWS/RDS":              {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "ServerlessDatabaseCapacity", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
+		"AWS/RDS":              {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "MaximumUsedTransactionIDs", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "ServerlessDatabaseCapacity", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"},
 		"AWS/Route53":          {"ChildHealthCheckHealthyCount", "HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"},
 		"AWS/Route53":          {"ChildHealthCheckHealthyCount", "HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"},
 		"AWS/S3":               {"BucketSizeBytes", "NumberOfObjects", "AllRequests", "GetRequests", "PutRequests", "DeleteRequests", "HeadRequests", "PostRequests", "ListRequests", "BytesDownloaded", "BytesUploaded", "4xxErrors", "5xxErrors", "FirstByteLatency", "TotalRequestLatency"},
 		"AWS/S3":               {"BucketSizeBytes", "NumberOfObjects", "AllRequests", "GetRequests", "PutRequests", "DeleteRequests", "HeadRequests", "PostRequests", "ListRequests", "BytesDownloaded", "BytesUploaded", "4xxErrors", "5xxErrors", "FirstByteLatency", "TotalRequestLatency"},
 		"AWS/SES":              {"Bounce", "Complaint", "Delivery", "Reject", "Send", "Reputation.BounceRate", "Reputation.ComplaintRate"},
 		"AWS/SES":              {"Bounce", "Complaint", "Delivery", "Reject", "Send", "Reputation.BounceRate", "Reputation.ComplaintRate"},

+ 10 - 0
public/app/core/specs/file_export.test.ts

@@ -60,6 +60,16 @@ describe('file_export', () => {
 
 
       expect(text).toBe(expectedText);
       expect(text).toBe(expectedText);
     });
     });
+
+    it('should not modify series.datapoints', () => {
+      const expectedSeries1DataPoints = ctx.seriesList[0].datapoints.slice();
+      const expectedSeries2DataPoints = ctx.seriesList[1].datapoints.slice();
+
+      fileExport.convertSeriesListToCsvColumns(ctx.seriesList, ctx.timeFormat);
+
+      expect(expectedSeries1DataPoints).toEqual(ctx.seriesList[0].datapoints);
+      expect(expectedSeries2DataPoints).toEqual(ctx.seriesList[1].datapoints);
+    });
   });
   });
 
 
   describe('when exporting table data to csv', () => {
   describe('when exporting table data to csv', () => {

+ 55 - 0
public/app/core/utils/errors.test.ts

@@ -0,0 +1,55 @@
+import { getMessageFromError } from 'app/core/utils/errors';
+
+describe('errors functions', () => {
+  let message;
+
+  describe('when getMessageFromError gets an error string', () => {
+    beforeEach(() => {
+      message = getMessageFromError('error string');
+    });
+
+    it('should return the string', () => {
+      expect(message).toBe('error string');
+    });
+  });
+
+  describe('when getMessageFromError gets an error object with message field', () => {
+    beforeEach(() => {
+      message = getMessageFromError({ message: 'error string' });
+    });
+
+    it('should return the message text', () => {
+      expect(message).toBe('error string');
+    });
+  });
+
+  describe('when getMessageFromError gets an error object with data.message field', () => {
+    beforeEach(() => {
+      message = getMessageFromError({ data: { message: 'error string' } });
+    });
+
+    it('should return the message text', () => {
+      expect(message).toBe('error string');
+    });
+  });
+
+  describe('when getMessageFromError gets an error object with statusText field', () => {
+    beforeEach(() => {
+      message = getMessageFromError({ statusText: 'error string' });
+    });
+
+    it('should return the statusText text', () => {
+      expect(message).toBe('error string');
+    });
+  });
+
+  describe('when getMessageFromError gets an error object', () => {
+    beforeEach(() => {
+      message = getMessageFromError({ customError: 'error string' });
+    });
+
+    it('should return the stringified error', () => {
+      expect(message).toBe('{"customError":"error string"}');
+    });
+  });
+});

+ 1 - 1
public/app/core/utils/errors.ts

@@ -13,5 +13,5 @@ export function getMessageFromError(err: any): string | null {
     }
     }
   }
   }
 
 
-  return null;
+  return err;
 }
 }

+ 14 - 13
public/app/core/utils/file_export.ts

@@ -88,18 +88,18 @@ export function convertSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAU
       )
       )
     );
     );
   // process data
   // process data
-  seriesList = mergeSeriesByTime(seriesList);
+  const extendedDatapointsList = mergeSeriesByTime(seriesList);
 
 
   // make text
   // make text
-  for (let i = 0; i < seriesList[0].datapoints.length; i += 1) {
-    const timestamp = moment(seriesList[0].datapoints[i][POINT_TIME_INDEX]).format(dateTimeFormat);
+  for (let i = 0; i < extendedDatapointsList[0].length; i += 1) {
+    const timestamp = moment(extendedDatapointsList[0][i][POINT_TIME_INDEX]).format(dateTimeFormat);
     text += formatRow(
     text += formatRow(
       [timestamp].concat(
       [timestamp].concat(
-        seriesList.map(series => {
-          return series.datapoints[i][POINT_VALUE_INDEX];
+        extendedDatapointsList.map(datapoints => {
+          return datapoints[i][POINT_VALUE_INDEX];
         })
         })
       ),
       ),
-      i < seriesList[0].datapoints.length - 1
+      i < extendedDatapointsList[0].length - 1
     );
     );
   }
   }
 
 
@@ -120,22 +120,23 @@ function mergeSeriesByTime(seriesList) {
   }
   }
   timestamps = sortedUniq(timestamps.sort());
   timestamps = sortedUniq(timestamps.sort());
 
 
+  const result = [];
   for (let i = 0; i < seriesList.length; i++) {
   for (let i = 0; i < seriesList.length; i++) {
     const seriesPoints = seriesList[i].datapoints;
     const seriesPoints = seriesList[i].datapoints;
     const seriesTimestamps = seriesPoints.map(p => p[POINT_TIME_INDEX]);
     const seriesTimestamps = seriesPoints.map(p => p[POINT_TIME_INDEX]);
-    const extendedSeries = [];
-    let pointIndex;
+    const extendedDatapoints = [];
     for (let j = 0; j < timestamps.length; j++) {
     for (let j = 0; j < timestamps.length; j++) {
-      pointIndex = sortedIndexOf(seriesTimestamps, timestamps[j]);
+      const timestamp = timestamps[j];
+      const pointIndex = sortedIndexOf(seriesTimestamps, timestamp);
       if (pointIndex !== -1) {
       if (pointIndex !== -1) {
-        extendedSeries.push(seriesPoints[pointIndex]);
+        extendedDatapoints.push(seriesPoints[pointIndex]);
       } else {
       } else {
-        extendedSeries.push([null, timestamps[j]]);
+        extendedDatapoints.push([null, timestamp]);
       }
       }
     }
     }
-    seriesList[i].datapoints = extendedSeries;
+    result.push(extendedDatapoints);
   }
   }
-  return seriesList;
+  return result;
 }
 }
 
 
 export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {
 export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) {

+ 1 - 1
public/app/features/admin/AdminEditOrgCtrl.ts

@@ -2,7 +2,7 @@ export default class AdminEditOrgCtrl {
   /** @ngInject */
   /** @ngInject */
   constructor($scope, $routeParams, backendSrv, $location, navModelSrv) {
   constructor($scope, $routeParams, backendSrv, $location, navModelSrv) {
     $scope.init = () => {
     $scope.init = () => {
-      $scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-orgs', 1);
+      $scope.navModel = navModelSrv.getNav('admin', 'global-orgs', 0);
 
 
       if ($routeParams.id) {
       if ($routeParams.id) {
         $scope.getOrg($routeParams.id);
         $scope.getOrg($routeParams.id);

+ 1 - 1
public/app/features/admin/AdminEditUserCtrl.ts

@@ -6,7 +6,7 @@ export default class AdminEditUserCtrl {
     $scope.user = {};
     $scope.user = {};
     $scope.newOrg = { name: '', role: 'Editor' };
     $scope.newOrg = { name: '', role: 'Editor' };
     $scope.permissions = {};
     $scope.permissions = {};
-    $scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-users', 1);
+    $scope.navModel = navModelSrv.getNav('admin', 'global-users', 0);
 
 
     $scope.init = () => {
     $scope.init = () => {
       if ($routeParams.id) {
       if ($routeParams.id) {

+ 1 - 1
public/app/features/admin/AdminListOrgsCtrl.ts

@@ -2,7 +2,7 @@ export default class AdminListOrgsCtrl {
   /** @ngInject */
   /** @ngInject */
   constructor($scope, backendSrv, navModelSrv) {
   constructor($scope, backendSrv, navModelSrv) {
     $scope.init = () => {
     $scope.init = () => {
-      $scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-orgs', 1);
+      $scope.navModel = navModelSrv.getNav('admin', 'global-orgs', 0);
       $scope.getOrgs();
       $scope.getOrgs();
     };
     };
 
 

+ 1 - 1
public/app/features/admin/AdminListUsersCtrl.ts

@@ -10,7 +10,7 @@ export default class AdminListUsersCtrl {
 
 
   /** @ngInject */
   /** @ngInject */
   constructor(private $scope, private backendSrv, navModelSrv) {
   constructor(private $scope, private backendSrv, navModelSrv) {
-    this.navModel = navModelSrv.getNav('cfg', 'admin', 'global-users', 1);
+    this.navModel = navModelSrv.getNav('admin', 'global-users', 0);
     this.query = '';
     this.query = '';
     this.getUsers();
     this.getUsers();
   }
   }

+ 1 - 1
public/app/features/admin/StyleGuideCtrl.ts

@@ -9,7 +9,7 @@ export default class StyleGuideCtrl {
 
 
   /** @ngInject */
   /** @ngInject */
   constructor(private $routeParams, private backendSrv, navModelSrv) {
   constructor(private $routeParams, private backendSrv, navModelSrv) {
-    this.navModel = navModelSrv.getNav('cfg', 'admin', 'styleguide', 1);
+    this.navModel = navModelSrv.getNav('admin', 'styleguide', 0);
     this.theme = config.bootData.user.lightTheme ? 'light' : 'dark';
     this.theme = config.bootData.user.lightTheme ? 'light' : 'dark';
   }
   }
 
 

+ 2 - 2
public/app/features/admin/index.ts

@@ -11,7 +11,7 @@ class AdminSettingsCtrl {
 
 
   /** @ngInject */
   /** @ngInject */
   constructor($scope, backendSrv, navModelSrv) {
   constructor($scope, backendSrv, navModelSrv) {
-    this.navModel = navModelSrv.getNav('cfg', 'admin', 'server-settings', 1);
+    this.navModel = navModelSrv.getNav('admin', 'server-settings', 0);
 
 
     backendSrv.get('/api/admin/settings').then(settings => {
     backendSrv.get('/api/admin/settings').then(settings => {
       $scope.settings = settings;
       $scope.settings = settings;
@@ -24,7 +24,7 @@ class AdminHomeCtrl {
 
 
   /** @ngInject */
   /** @ngInject */
   constructor(navModelSrv) {
   constructor(navModelSrv) {
-    this.navModel = navModelSrv.getNav('cfg', 'admin', 1);
+    this.navModel = navModelSrv.getNav('admin', 0);
   }
   }
 }
 }
 
 

+ 23 - 20
public/app/features/dashboard/components/DashNav/DashNav.tsx

@@ -8,6 +8,7 @@ import { appEvents } from 'app/core/app_events';
 import { PlaylistSrv } from 'app/features/playlist/playlist_srv';
 import { PlaylistSrv } from 'app/features/playlist/playlist_srv';
 
 
 // Components
 // Components
+import { ClickOutsideWrapper } from 'app/core/components/ClickOutsideWrapper/ClickOutsideWrapper';
 import { DashNavButton } from './DashNavButton';
 import { DashNavButton } from './DashNavButton';
 import { Tooltip } from '@grafana/ui';
 import { Tooltip } from '@grafana/ui';
 
 
@@ -173,26 +174,28 @@ export class DashNav extends PureComponent<Props> {
         {this.renderDashboardTitleSearchButton()}
         {this.renderDashboardTitleSearchButton()}
 
 
         {this.playlistSrv.isPlaying && (
         {this.playlistSrv.isPlaying && (
-          <div className="navbar-buttons navbar-buttons--playlist">
-            <DashNavButton
-              tooltip="Go to previous dashboard"
-              classSuffix="tight"
-              icon="fa fa-step-backward"
-              onClick={this.onPlaylistPrev}
-            />
-            <DashNavButton
-              tooltip="Stop playlist"
-              classSuffix="tight"
-              icon="fa fa-stop"
-              onClick={this.onPlaylistStop}
-            />
-            <DashNavButton
-              tooltip="Go to next dashboard"
-              classSuffix="tight"
-              icon="fa fa-forward"
-              onClick={this.onPlaylistNext}
-            />
-          </div>
+          <ClickOutsideWrapper onClick={this.onPlaylistStop}>
+            <div className="navbar-buttons navbar-buttons--playlist">
+              <DashNavButton
+                tooltip="Go to previous dashboard"
+                classSuffix="tight"
+                icon="fa fa-step-backward"
+                onClick={this.onPlaylistPrev}
+              />
+              <DashNavButton
+                tooltip="Stop playlist"
+                classSuffix="tight"
+                icon="fa fa-stop"
+                onClick={this.onPlaylistStop}
+              />
+              <DashNavButton
+                tooltip="Go to next dashboard"
+                classSuffix="tight"
+                icon="fa fa-forward"
+                onClick={this.onPlaylistNext}
+              />
+            </div>
+          </ClickOutsideWrapper>
         )}
         )}
 
 
         <div className="navbar-buttons navbar-buttons--actions">
         <div className="navbar-buttons navbar-buttons--actions">

+ 7 - 1
public/app/features/dashboard/containers/DashboardPage.tsx

@@ -268,7 +268,13 @@ export class DashboardPage extends PureComponent<Props, State> {
           onAddPanel={this.onAddPanel}
           onAddPanel={this.onAddPanel}
         />
         />
         <div className="scroll-canvas scroll-canvas--dashboard">
         <div className="scroll-canvas scroll-canvas--dashboard">
-          <CustomScrollbar autoHeightMin={'100%'} setScrollTop={this.setScrollTop} scrollTop={scrollTop}>
+          <CustomScrollbar
+            autoHeightMin={'100%'}
+            setScrollTop={this.setScrollTop}
+            scrollTop={scrollTop}
+            updateAfterMountMs={500}
+            className="custom-scrollbar--page"
+          >
             {editview && <DashboardSettings dashboard={dashboard} />}
             {editview && <DashboardSettings dashboard={dashboard} />}
 
 
             {initError && this.renderInitFailedState()}
             {initError && this.renderInitFailedState()}

+ 4 - 0
public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap

@@ -109,9 +109,11 @@ exports[`DashboardPage Dashboard init completed  Should render dashboard grid 1`
       autoHide={false}
       autoHide={false}
       autoHideDuration={200}
       autoHideDuration={200}
       autoHideTimeout={200}
       autoHideTimeout={200}
+      className="custom-scrollbar--page"
       hideTracksWhenNotNeeded={false}
       hideTracksWhenNotNeeded={false}
       scrollTop={0}
       scrollTop={0}
       setScrollTop={[Function]}
       setScrollTop={[Function]}
+      updateAfterMountMs={500}
     >
     >
       <div
       <div
         className="dashboard-container"
         className="dashboard-container"
@@ -344,9 +346,11 @@ exports[`DashboardPage When dashboard has editview url state should render setti
       autoHide={false}
       autoHide={false}
       autoHideDuration={200}
       autoHideDuration={200}
       autoHideTimeout={200}
       autoHideTimeout={200}
+      className="custom-scrollbar--page"
       hideTracksWhenNotNeeded={false}
       hideTracksWhenNotNeeded={false}
       scrollTop={0}
       scrollTop={0}
       setScrollTop={[Function]}
       setScrollTop={[Function]}
+      updateAfterMountMs={500}
     >
     >
       <DashboardSettings
       <DashboardSettings
         dashboard={
         dashboard={

+ 4 - 1
public/app/features/dashboard/dashgrid/DataPanel.tsx

@@ -15,6 +15,7 @@ import {
   TableData,
   TableData,
   TimeRange,
   TimeRange,
   TimeSeries,
   TimeSeries,
+  ScopedVars,
 } from '@grafana/ui';
 } from '@grafana/ui';
 
 
 interface RenderProps {
 interface RenderProps {
@@ -33,6 +34,7 @@ export interface Props {
   refreshCounter: number;
   refreshCounter: number;
   minInterval?: string;
   minInterval?: string;
   maxDataPoints?: number;
   maxDataPoints?: number;
+  scopedVars?: ScopedVars;
   children: (r: RenderProps) => JSX.Element;
   children: (r: RenderProps) => JSX.Element;
   onDataResponse?: (data: DataQueryResponse) => void;
   onDataResponse?: (data: DataQueryResponse) => void;
   onError: (message: string, error: DataQueryError) => void;
   onError: (message: string, error: DataQueryError) => void;
@@ -95,6 +97,7 @@ export class DataPanel extends Component<Props, State> {
       timeRange,
       timeRange,
       widthPixels,
       widthPixels,
       maxDataPoints,
       maxDataPoints,
+      scopedVars,
       onDataResponse,
       onDataResponse,
       onError,
       onError,
     } = this.props;
     } = this.props;
@@ -127,7 +130,7 @@ export class DataPanel extends Component<Props, State> {
         intervalMs: intervalRes.intervalMs,
         intervalMs: intervalRes.intervalMs,
         targets: queries,
         targets: queries,
         maxDataPoints: maxDataPoints || widthPixels,
         maxDataPoints: maxDataPoints || widthPixels,
-        scopedVars: {},
+        scopedVars: scopedVars || {},
         cacheTimeout: null,
         cacheTimeout: null,
       };
       };
 
 

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

@@ -85,7 +85,7 @@ export class PanelChrome extends PureComponent<Props, State> {
     });
     });
   };
   };
 
 
-  onInterpolate = (value: string, format?: string) => {
+  replaceVariables = (value: string, format?: string) => {
     return templateSrv.replace(value, this.props.panel.scopedVars, format);
     return templateSrv.replace(value, this.props.panel.scopedVars, format);
   };
   };
 
 
@@ -158,7 +158,7 @@ export class PanelChrome extends PureComponent<Props, State> {
           width={width - 2 * variables.panelhorizontalpadding}
           width={width - 2 * variables.panelhorizontalpadding}
           height={height - PANEL_HEADER_HEIGHT - variables.panelverticalpadding}
           height={height - PANEL_HEADER_HEIGHT - variables.panelverticalpadding}
           renderCounter={renderCounter}
           renderCounter={renderCounter}
-          onInterpolate={this.onInterpolate}
+          replaceVariables={this.replaceVariables}
         />
         />
       </div>
       </div>
     );
     );
@@ -179,6 +179,7 @@ export class PanelChrome extends PureComponent<Props, State> {
             isVisible={this.isVisible}
             isVisible={this.isVisible}
             widthPixels={width}
             widthPixels={width}
             refreshCounter={refreshCounter}
             refreshCounter={refreshCounter}
+            scopedVars={panel.scopedVars}
             onDataResponse={this.onDataResponse}
             onDataResponse={this.onDataResponse}
             onError={this.onDataError}
             onError={this.onDataError}
           >
           >

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

@@ -1,6 +1,7 @@
 import React, { Component } from 'react';
 import React, { Component } from 'react';
 import classNames from 'classnames';
 import classNames from 'classnames';
 import { isEqual } from 'lodash';
 import { isEqual } from 'lodash';
+import { ScopedVars } from '@grafana/ui';
 
 
 import PanelHeaderCorner from './PanelHeaderCorner';
 import PanelHeaderCorner from './PanelHeaderCorner';
 import { PanelHeaderMenu } from './PanelHeaderMenu';
 import { PanelHeaderMenu } from './PanelHeaderMenu';
@@ -16,7 +17,7 @@ export interface Props {
   timeInfo: string;
   timeInfo: string;
   title?: string;
   title?: string;
   description?: string;
   description?: string;
-  scopedVars?: string;
+  scopedVars?: ScopedVars;
   links?: [];
   links?: [];
   error?: string;
   error?: string;
   isFullscreen: boolean;
   isFullscreen: boolean;

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

@@ -1,6 +1,7 @@
 import React, { Component } from 'react';
 import React, { Component } from 'react';
 import Remarkable from 'remarkable';
 import Remarkable from 'remarkable';
-import { Tooltip } from '@grafana/ui';
+import { Tooltip, ScopedVars } from '@grafana/ui';
+
 import { PanelModel } from 'app/features/dashboard/state/PanelModel';
 import { PanelModel } from 'app/features/dashboard/state/PanelModel';
 import templateSrv from 'app/features/templating/template_srv';
 import templateSrv from 'app/features/templating/template_srv';
 import { LinkSrv } from 'app/features/panel/panellinks/link_srv';
 import { LinkSrv } from 'app/features/panel/panellinks/link_srv';
@@ -16,7 +17,7 @@ interface Props {
   panel: PanelModel;
   panel: PanelModel;
   title?: string;
   title?: string;
   description?: string;
   description?: string;
-  scopedVars?: string;
+  scopedVars?: ScopedVars;
   links?: [];
   links?: [];
   error?: string;
   error?: string;
 }
 }

+ 9 - 8
public/app/features/dashboard/panel_editor/DataSourceOption.tsx

@@ -1,16 +1,17 @@
-import React, { FC } from 'react';
+import React, { FC, ChangeEvent } from 'react';
 import { FormLabel } from '@grafana/ui';
 import { FormLabel } from '@grafana/ui';
 
 
 interface Props {
 interface Props {
   label: string;
   label: string;
   placeholder?: string;
   placeholder?: string;
-  name?: string;
-  value?: string;
-  onChange?: (evt: any) => void;
+  name: string;
+  value: string;
+  onBlur: (event: ChangeEvent<HTMLInputElement>) => void;
+  onChange: (event: ChangeEvent<HTMLInputElement>) => void;
   tooltipInfo?: any;
   tooltipInfo?: any;
 }
 }
 
 
-export const DataSourceOptions: FC<Props> = ({ label, placeholder, name, value, onChange, tooltipInfo }) => {
+export const DataSourceOption: FC<Props> = ({ label, placeholder, name, value, onBlur, onChange, tooltipInfo }) => {
   return (
   return (
     <div className="gf-form gf-form--flex-end">
     <div className="gf-form gf-form--flex-end">
       <FormLabel tooltip={tooltipInfo}>{label}</FormLabel>
       <FormLabel tooltip={tooltipInfo}>{label}</FormLabel>
@@ -20,10 +21,10 @@ export const DataSourceOptions: FC<Props> = ({ label, placeholder, name, value,
         placeholder={placeholder}
         placeholder={placeholder}
         name={name}
         name={name}
         spellCheck={false}
         spellCheck={false}
-        onBlur={evt => onChange(evt.target.value)}
+        onBlur={onBlur}
+        onChange={onChange}
+        value={value}
       />
       />
     </div>
     </div>
   );
   );
 };
 };
-
-export default DataSourceOptions;

+ 1 - 1
public/app/features/dashboard/panel_editor/EditorTabBody.tsx

@@ -118,7 +118,7 @@ export class EditorTabBody extends PureComponent<Props, State> {
           {toolbarItems.map(item => this.renderButton(item))}
           {toolbarItems.map(item => this.renderButton(item))}
         </div>
         </div>
         <div className="panel-editor__scroll">
         <div className="panel-editor__scroll">
-          <CustomScrollbar autoHide={false} scrollTop={scrollTop} setScrollTop={setScrollTop}>
+          <CustomScrollbar autoHide={false} scrollTop={scrollTop} setScrollTop={setScrollTop} updateAfterMountMs={300}>
             <div className="panel-editor__content">
             <div className="panel-editor__content">
               <FadeIn in={isOpen} duration={200} unmountOnExit={true}>
               <FadeIn in={isOpen} duration={200} unmountOnExit={true}>
                 {openView && this.renderOpenView(openView)}
                 {openView && this.renderOpenView(openView)}

+ 89 - 69
public/app/features/dashboard/panel_editor/QueryOptions.tsx

@@ -1,5 +1,5 @@
 // Libraries
 // Libraries
-import React, { PureComponent } from 'react';
+import React, { PureComponent, ChangeEvent, FocusEvent } from 'react';
 
 
 // Utils
 // Utils
 import { isValidTimeSpan } from 'app/core/utils/rangeutil';
 import { isValidTimeSpan } from 'app/core/utils/rangeutil';
@@ -9,7 +9,7 @@ import { Switch } from '@grafana/ui';
 import { Input } from 'app/core/components/Form';
 import { Input } from 'app/core/components/Form';
 import { EventsWithValidation } from 'app/core/components/Form/Input';
 import { EventsWithValidation } from 'app/core/components/Form/Input';
 import { InputStatus } from 'app/core/components/Form/Input';
 import { InputStatus } from 'app/core/components/Form/Input';
-import DataSourceOption from './DataSourceOption';
+import { DataSourceOption } from './DataSourceOption';
 import { FormLabel } from '@grafana/ui';
 import { FormLabel } from '@grafana/ui';
 
 
 // Types
 // Types
@@ -43,32 +43,79 @@ interface Props {
 interface State {
 interface State {
   relativeTime: string;
   relativeTime: string;
   timeShift: string;
   timeShift: string;
+  cacheTimeout: string;
+  maxDataPoints: string;
+  interval: string;
+  hideTimeOverride: boolean;
 }
 }
 
 
 export class QueryOptions extends PureComponent<Props, State> {
 export class QueryOptions extends PureComponent<Props, State> {
+  allOptions = {
+    cacheTimeout: {
+      label: 'Cache timeout',
+      placeholder: '60',
+      name: 'cacheTimeout',
+      tooltipInfo: (
+        <>
+          If your time series store has a query cache this option can override the default cache timeout. Specify a
+          numeric value in seconds.
+        </>
+      ),
+    },
+    maxDataPoints: {
+      label: 'Max data points',
+      placeholder: 'auto',
+      name: 'maxDataPoints',
+      tooltipInfo: (
+        <>
+          The maximum data points the query should return. For graphs this is automatically set to one data point per
+          pixel.
+        </>
+      ),
+    },
+    minInterval: {
+      label: 'Min time interval',
+      placeholder: '0',
+      name: 'minInterval',
+      panelKey: 'interval',
+      tooltipInfo: (
+        <>
+          A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example{' '}
+          <code>1m</code> if your data is written every minute. Access auto interval via variable{' '}
+          <code>$__interval</code> for time range string and <code>$__interval_ms</code> for numeric variable that can
+          be used in math expressions.
+        </>
+      ),
+    },
+  };
+
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
     this.state = {
     this.state = {
       relativeTime: props.panel.timeFrom || '',
       relativeTime: props.panel.timeFrom || '',
       timeShift: props.panel.timeShift || '',
       timeShift: props.panel.timeShift || '',
+      cacheTimeout: props.panel.cacheTimeout || '',
+      maxDataPoints: props.panel.maxDataPoints || '',
+      interval: props.panel.interval || '',
+      hideTimeOverride: props.panel.hideTimeOverride || false,
     };
     };
   }
   }
 
 
-  onRelativeTimeChange = event => {
+  onRelativeTimeChange = (event: ChangeEvent<HTMLInputElement>) => {
     this.setState({
     this.setState({
       relativeTime: event.target.value,
       relativeTime: event.target.value,
     });
     });
   };
   };
 
 
-  onTimeShiftChange = event => {
+  onTimeShiftChange = (event: ChangeEvent<HTMLInputElement>) => {
     this.setState({
     this.setState({
       timeShift: event.target.value,
       timeShift: event.target.value,
     });
     });
   };
   };
 
 
-  onOverrideTime = (evt, status: InputStatus) => {
-    const { value } = evt.target;
+  onOverrideTime = (event: FocusEvent<HTMLInputElement>, status: InputStatus) => {
+    const { value } = event.target;
     const { panel } = this.props;
     const { panel } = this.props;
     const emptyToNullValue = emptyToNull(value);
     const emptyToNullValue = emptyToNull(value);
     if (status === InputStatus.Valid && panel.timeFrom !== emptyToNullValue) {
     if (status === InputStatus.Valid && panel.timeFrom !== emptyToNullValue) {
@@ -77,8 +124,8 @@ export class QueryOptions extends PureComponent<Props, State> {
     }
     }
   };
   };
 
 
-  onTimeShift = (evt, status: InputStatus) => {
-    const { value } = evt.target;
+  onTimeShift = (event: FocusEvent<HTMLInputElement>, status: InputStatus) => {
+    const { value } = event.target;
     const { panel } = this.props;
     const { panel } = this.props;
     const emptyToNullValue = emptyToNull(value);
     const emptyToNullValue = emptyToNull(value);
     if (status === InputStatus.Valid && panel.timeShift !== emptyToNullValue) {
     if (status === InputStatus.Valid && panel.timeShift !== emptyToNullValue) {
@@ -89,77 +136,49 @@ export class QueryOptions extends PureComponent<Props, State> {
 
 
   onToggleTimeOverride = () => {
   onToggleTimeOverride = () => {
     const { panel } = this.props;
     const { panel } = this.props;
-    panel.hideTimeOverride = !panel.hideTimeOverride;
+    this.setState({ hideTimeOverride: !this.state.hideTimeOverride }, () => {
+      panel.hideTimeOverride = this.state.hideTimeOverride;
+      panel.refresh();
+    });
+  };
+
+  onDataSourceOptionBlur = (panelKey: string) => () => {
+    const { panel } = this.props;
+
+    panel[panelKey] = this.state[panelKey];
     panel.refresh();
     panel.refresh();
   };
   };
 
 
-  renderOptions() {
-    const { datasource, panel } = this.props;
+  onDataSourceOptionChange = (panelKey: string) => (event: ChangeEvent<HTMLInputElement>) => {
+    this.setState({ ...this.state, [panelKey]: event.target.value });
+  };
+
+  renderOptions = () => {
+    const { datasource } = this.props;
     const { queryOptions } = datasource.meta;
     const { queryOptions } = datasource.meta;
 
 
     if (!queryOptions) {
     if (!queryOptions) {
       return null;
       return null;
     }
     }
 
 
-    const onChangeFn = (panelKey: string) => {
-      return (value: string | number) => {
-        panel[panelKey] = value;
-        panel.refresh();
-      };
-    };
-
-    const allOptions = {
-      cacheTimeout: {
-        label: 'Cache timeout',
-        placeholder: '60',
-        name: 'cacheTimeout',
-        value: panel.cacheTimeout,
-        tooltipInfo: (
-          <>
-            If your time series store has a query cache this option can override the default cache timeout. Specify a
-            numeric value in seconds.
-          </>
-        ),
-      },
-      maxDataPoints: {
-        label: 'Max data points',
-        placeholder: 'auto',
-        name: 'maxDataPoints',
-        value: panel.maxDataPoints,
-        tooltipInfo: (
-          <>
-            The maximum data points the query should return. For graphs this is automatically set to one data point per
-            pixel.
-          </>
-        ),
-      },
-      minInterval: {
-        label: 'Min time interval',
-        placeholder: '0',
-        name: 'minInterval',
-        value: panel.interval,
-        panelKey: 'interval',
-        tooltipInfo: (
-          <>
-            A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example{' '}
-            <code>1m</code> if your data is written every minute. Access auto interval via variable{' '}
-            <code>$__interval</code> for time range string and <code>$__interval_ms</code> for numeric variable that can
-            be used in math expressions.
-          </>
-        ),
-      },
-    };
-
     return Object.keys(queryOptions).map(key => {
     return Object.keys(queryOptions).map(key => {
-      const options = allOptions[key];
-      return <DataSourceOption key={key} {...options} onChange={onChangeFn(allOptions[key].panelKey || key)} />;
+      const options = this.allOptions[key];
+      const panelKey = options.panelKey || key;
+      return (
+        <DataSourceOption
+          key={key}
+          {...options}
+          onChange={this.onDataSourceOptionChange(panelKey)}
+          onBlur={this.onDataSourceOptionBlur(panelKey)}
+          value={this.state[panelKey]}
+        />
+      );
     });
     });
-  }
+  };
 
 
   render() {
   render() {
-    const hideTimeOverride = this.props.panel.hideTimeOverride;
+    const { hideTimeOverride } = this.state;
     const { relativeTime, timeShift } = this.state;
     const { relativeTime, timeShift } = this.state;
-
     return (
     return (
       <div className="gf-form-inline">
       <div className="gf-form-inline">
         {this.renderOptions()}
         {this.renderOptions()}
@@ -191,10 +210,11 @@ export class QueryOptions extends PureComponent<Props, State> {
             value={timeShift}
             value={timeShift}
           />
           />
         </div>
         </div>
-
-        <div className="gf-form-inline">
-          <Switch label="Hide time info" checked={hideTimeOverride} onChange={this.onToggleTimeOverride} />
-        </div>
+        {(timeShift || relativeTime) && (
+          <div className="gf-form-inline">
+            <Switch label="Hide time info" checked={hideTimeOverride} onChange={this.onToggleTimeOverride} />
+          </div>
+        )}
       </div>
       </div>
     );
     );
   }
   }

+ 1 - 1
public/app/features/dashboard/panel_editor/VisualizationTab.tsx

@@ -66,7 +66,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
       const PanelEditor = plugin.exports.reactPanel.editor;
       const PanelEditor = plugin.exports.reactPanel.editor;
 
 
       if (PanelEditor) {
       if (PanelEditor) {
-        return <PanelEditor options={this.getReactPanelOptions()} onChange={this.onPanelOptionsChanged} />;
+        return <PanelEditor options={this.getReactPanelOptions()} onOptionsChange={this.onPanelOptionsChanged} />;
       }
       }
     }
     }
 
 

+ 2 - 2
public/app/features/dashboard/state/DashboardModel.ts

@@ -887,8 +887,8 @@ export class DashboardModel {
     }
     }
 
 
     // add back navbar height
     // add back navbar height
-    if (kioskMode === KIOSK_MODE_TV) {
-      visibleHeight += 55;
+    if (kioskMode && kioskMode !== KIOSK_MODE_TV) {
+      visibleHeight += navbarHeight;
     }
     }
 
 
     const visibleGridHeight = Math.floor(visibleHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN));
     const visibleGridHeight = Math.floor(visibleHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN));

+ 2 - 2
public/app/features/dashboard/state/PanelModel.ts

@@ -3,7 +3,7 @@ import _ from 'lodash';
 
 
 // Types
 // Types
 import { Emitter } from 'app/core/utils/emitter';
 import { Emitter } from 'app/core/utils/emitter';
-import { DataQuery, TimeSeries, Threshold } from '@grafana/ui';
+import { DataQuery, TimeSeries, Threshold, ScopedVars } from '@grafana/ui';
 import { TableData } from '@grafana/ui/src';
 import { TableData } from '@grafana/ui/src';
 
 
 export interface GridPos {
 export interface GridPos {
@@ -71,7 +71,7 @@ export class PanelModel {
   type: string;
   type: string;
   title: string;
   title: string;
   alert?: any;
   alert?: any;
-  scopedVars?: any;
+  scopedVars?: ScopedVars;
   repeat?: string;
   repeat?: string;
   repeatIteration?: number;
   repeatIteration?: number;
   repeatPanelId?: number;
   repeatPanelId?: number;

+ 2 - 1
public/app/features/datasources/settings/ButtonRow.tsx

@@ -1,4 +1,5 @@
 import React, { FC } from 'react';
 import React, { FC } from 'react';
+import config from 'app/core/config';
 
 
 export interface Props {
 export interface Props {
   isReadOnly: boolean;
   isReadOnly: boolean;
@@ -23,7 +24,7 @@ const ButtonRow: FC<Props> = ({ isReadOnly, onDelete, onSubmit, onTest }) => {
       <button type="submit" className="btn btn-danger" disabled={isReadOnly} onClick={onDelete}>
       <button type="submit" className="btn btn-danger" disabled={isReadOnly} onClick={onDelete}>
         Delete
         Delete
       </button>
       </button>
-      <a className="btn btn-inverse" href="/datasources">
+      <a className="btn btn-inverse" href={`${config.appSubUrl}/datasources`}>
         Back
         Back
       </a>
       </a>
     </div>
     </div>

+ 9 - 3
public/app/features/datasources/settings/DataSourceSettingsPage.tsx

@@ -64,6 +64,14 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
     await loadDataSource(pageId);
     await loadDataSource(pageId);
   }
   }
 
 
+  componentDidUpdate(prevProps: Props) {
+    const { dataSource } = this.props;
+
+    if (prevProps.dataSource !== dataSource) {
+      this.setState({ dataSource });
+    }
+  }
+
   onSubmit = async (evt: React.FormEvent<HTMLFormElement>) => {
   onSubmit = async (evt: React.FormEvent<HTMLFormElement>) => {
     evt.preventDefault();
     evt.preventDefault();
 
 
@@ -95,9 +103,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
   };
   };
 
 
   onModelChange = (dataSource: DataSourceSettings) => {
   onModelChange = (dataSource: DataSourceSettings) => {
-    this.setState({
-      dataSource: dataSource,
-    });
+    this.setState({ dataSource });
   };
   };
 
 
   isReadOnly() {
   isReadOnly() {

+ 1 - 1
public/app/features/org/NewOrgCtrl.ts

@@ -4,7 +4,7 @@ import config from 'app/core/config';
 export class NewOrgCtrl {
 export class NewOrgCtrl {
   /** @ngInject */
   /** @ngInject */
   constructor($scope, $http, backendSrv, navModelSrv) {
   constructor($scope, $http, backendSrv, navModelSrv) {
-    $scope.navModel = navModelSrv.getNav('cfg', 'admin', 'global-orgs', 1);
+    $scope.navModel = navModelSrv.getNav('admin', 'global-orgs', 0);
     $scope.newOrg = { name: '' };
     $scope.newOrg = { name: '' };
 
 
     $scope.createOrg = () => {
     $scope.createOrg = () => {

+ 1 - 1
public/app/plugins/datasource/mssql/datasource.ts

@@ -12,7 +12,7 @@ export class MssqlDatasource {
     this.name = instanceSettings.name;
     this.name = instanceSettings.name;
     this.id = instanceSettings.id;
     this.id = instanceSettings.id;
     this.responseParser = new ResponseParser(this.$q);
     this.responseParser = new ResponseParser(this.$q);
-    this.interval = (instanceSettings.jsonData || {}).timeInterval;
+    this.interval = (instanceSettings.jsonData || {}).timeInterval || '1m';
   }
   }
 
 
   interpolateVariable(value, variable) {
   interpolateVariable(value, variable) {

+ 1 - 1
public/app/plugins/datasource/mysql/datasource.ts

@@ -15,7 +15,7 @@ export class MysqlDatasource {
     this.id = instanceSettings.id;
     this.id = instanceSettings.id;
     this.responseParser = new ResponseParser(this.$q);
     this.responseParser = new ResponseParser(this.$q);
     this.queryModel = new MysqlQuery({});
     this.queryModel = new MysqlQuery({});
-    this.interval = (instanceSettings.jsonData || {}).timeInterval;
+    this.interval = (instanceSettings.jsonData || {}).timeInterval || '1m';
   }
   }
 
 
   interpolateVariable = (value, variable) => {
   interpolateVariable = (value, variable) => {

+ 1 - 1
public/app/plugins/datasource/postgres/datasource.ts

@@ -17,7 +17,7 @@ export class PostgresDatasource {
     this.jsonData = instanceSettings.jsonData;
     this.jsonData = instanceSettings.jsonData;
     this.responseParser = new ResponseParser(this.$q);
     this.responseParser = new ResponseParser(this.$q);
     this.queryModel = new PostgresQuery({});
     this.queryModel = new PostgresQuery({});
-    this.interval = (instanceSettings.jsonData || {}).timeInterval;
+    this.interval = (instanceSettings.jsonData || {}).timeInterval || '1m';
   }
   }
 
 
   interpolateVariable = (value, variable) => {
   interpolateVariable = (value, variable) => {

+ 18 - 0
public/app/plugins/datasource/prometheus/datasource.ts

@@ -379,6 +379,24 @@ export class PrometheusDatasource implements DataSourceApi<PromQuery> {
     });
     });
   }
   }
 
 
+  getTagKeys(options) {
+    const url = '/api/v1/labels';
+    return this.metadataRequest(url).then(result => {
+      return _.map(result.data.data, value => {
+        return { text: value };
+      });
+    });
+  }
+
+  getTagValues(options) {
+    const url = '/api/v1/label/' + options.key + '/values';
+    return this.metadataRequest(url).then(result => {
+      return _.map(result.data.data, value => {
+        return { text: value };
+      });
+    });
+  }
+
   testDatasource() {
   testDatasource() {
     const now = new Date().getTime();
     const now = new Date().getTime();
     return this.performInstantQuery({ expr: '1+1' }, now / 1000).then(response => {
     return this.performInstantQuery({ expr: '1+1' }, now / 1000).then(response => {

+ 15 - 0
public/app/plugins/datasource/prometheus/metric_find_query.ts

@@ -12,10 +12,16 @@ export default class PrometheusMetricFindQuery {
   }
   }
 
 
   process() {
   process() {
+    const labelNamesRegex = /^label_names\(\)\s*$/;
     const labelValuesRegex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]*)\)\s*$/;
     const labelValuesRegex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]*)\)\s*$/;
     const metricNamesRegex = /^metrics\((.+)\)\s*$/;
     const metricNamesRegex = /^metrics\((.+)\)\s*$/;
     const queryResultRegex = /^query_result\((.+)\)\s*$/;
     const queryResultRegex = /^query_result\((.+)\)\s*$/;
 
 
+    const labelNamesQuery = this.query.match(labelNamesRegex);
+    if (labelNamesQuery) {
+      return this.labelNamesQuery();
+    }
+
     const labelValuesQuery = this.query.match(labelValuesRegex);
     const labelValuesQuery = this.query.match(labelValuesRegex);
     if (labelValuesQuery) {
     if (labelValuesQuery) {
       if (labelValuesQuery[1]) {
       if (labelValuesQuery[1]) {
@@ -39,6 +45,15 @@ export default class PrometheusMetricFindQuery {
     return this.metricNameAndLabelsQuery(this.query);
     return this.metricNameAndLabelsQuery(this.query);
   }
   }
 
 
+  labelNamesQuery() {
+    const url = '/api/v1/labels';
+    return this.datasource.metadataRequest(url).then(result => {
+      return _.map(result.data.data, value => {
+        return { text: value };
+      });
+    });
+  }
+
   labelValuesQuery(label, metric) {
   labelValuesQuery(label, metric) {
     let url;
     let url;
 
 

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

@@ -42,6 +42,24 @@ describe('PrometheusMetricFindQuery', () => {
   });
   });
 
 
   describe('When performing metricFindQuery', () => {
   describe('When performing metricFindQuery', () => {
+    it('label_names() should generate label name search query', async () => {
+      const query = ctx.setupMetricFindQuery({
+        query: 'label_names()',
+        response: {
+          data: ['name1', 'name2', 'name3'],
+        },
+      });
+      const results = await query.process();
+
+      expect(results).toHaveLength(3);
+      expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledTimes(1);
+      expect(ctx.backendSrvMock.datasourceRequest).toHaveBeenCalledWith({
+        method: 'GET',
+        url: 'proxied/api/v1/labels',
+        silent: true,
+      });
+    });
+
     it('label_values(resource) should generate label search query', async () => {
     it('label_values(resource) should generate label search query', async () => {
       const query = ctx.setupMetricFindQuery({
       const query = ctx.setupMetricFindQuery({
         query: 'label_values(resource)',
         query: 'label_values(resource)',

+ 7 - 4
public/app/plugins/panel/gauge/GaugeOptionsBox.tsx

@@ -10,14 +10,17 @@ import { GaugeOptions } from './types';
 
 
 export class GaugeOptionsBox extends PureComponent<PanelEditorProps<GaugeOptions>> {
 export class GaugeOptionsBox extends PureComponent<PanelEditorProps<GaugeOptions>> {
   onToggleThresholdLabels = () =>
   onToggleThresholdLabels = () =>
-    this.props.onChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels });
+    this.props.onOptionsChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels });
 
 
   onToggleThresholdMarkers = () =>
   onToggleThresholdMarkers = () =>
-    this.props.onChange({ ...this.props.options, showThresholdMarkers: !this.props.options.showThresholdMarkers });
+    this.props.onOptionsChange({
+      ...this.props.options,
+      showThresholdMarkers: !this.props.options.showThresholdMarkers,
+    });
 
 
-  onMinValueChange = ({ target }) => this.props.onChange({ ...this.props.options, minValue: target.value });
+  onMinValueChange = ({ target }) => this.props.onOptionsChange({ ...this.props.options, minValue: target.value });
 
 
-  onMaxValueChange = ({ target }) => this.props.onChange({ ...this.props.options, maxValue: target.value });
+  onMaxValueChange = ({ target }) => this.props.onOptionsChange({ ...this.props.options, maxValue: target.value });
 
 
   render() {
   render() {
     const { options } = this.props;
     const { options } = this.props;

+ 3 - 3
public/app/plugins/panel/gauge/GaugePanel.tsx

@@ -15,11 +15,11 @@ interface Props extends PanelProps<GaugeOptions> {}
 
 
 export class GaugePanel extends PureComponent<Props> {
 export class GaugePanel extends PureComponent<Props> {
   render() {
   render() {
-    const { panelData, width, height, onInterpolate, options } = this.props;
+    const { panelData, width, height, replaceVariables, options } = this.props;
     const { valueOptions } = options;
     const { valueOptions } = options;
 
 
-    const prefix = onInterpolate(valueOptions.prefix);
-    const suffix = onInterpolate(valueOptions.suffix);
+    const prefix = replaceVariables(valueOptions.prefix);
+    const suffix = replaceVariables(valueOptions.suffix);
     let value: TimeSeriesValue;
     let value: TimeSeriesValue;
 
 
     if (panelData.timeSeries) {
     if (panelData.timeSeries) {

+ 5 - 5
public/app/plugins/panel/gauge/GaugePanelEditor.tsx

@@ -14,31 +14,31 @@ import { GaugeOptions, SingleStatValueOptions } from './types';
 
 
 export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOptions>> {
 export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOptions>> {
   onThresholdsChanged = (thresholds: Threshold[]) =>
   onThresholdsChanged = (thresholds: Threshold[]) =>
-    this.props.onChange({
+    this.props.onOptionsChange({
       ...this.props.options,
       ...this.props.options,
       thresholds,
       thresholds,
     });
     });
 
 
   onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
   onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
-    this.props.onChange({
+    this.props.onOptionsChange({
       ...this.props.options,
       ...this.props.options,
       valueMappings,
       valueMappings,
     });
     });
 
 
   onValueOptionsChanged = (valueOptions: SingleStatValueOptions) =>
   onValueOptionsChanged = (valueOptions: SingleStatValueOptions) =>
-    this.props.onChange({
+    this.props.onOptionsChange({
       ...this.props.options,
       ...this.props.options,
       valueOptions,
       valueOptions,
     });
     });
 
 
   render() {
   render() {
-    const { onChange, options } = this.props;
+    const { onOptionsChange, options } = this.props;
 
 
     return (
     return (
       <>
       <>
         <PanelOptionsGrid>
         <PanelOptionsGrid>
           <SingleStatValueEditor onChange={this.onValueOptionsChanged} options={options.valueOptions} />
           <SingleStatValueEditor onChange={this.onValueOptionsChanged} options={options.valueOptions} />
-          <GaugeOptionsBox onChange={onChange} options={options} />
+          <GaugeOptionsBox onOptionsChange={onOptionsChange} options={options} />
           <ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
           <ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
         </PanelOptionsGrid>
         </PanelOptionsGrid>
 
 

+ 1 - 2
public/app/plugins/panel/gauge/SingleStatValueEditor.tsx

@@ -2,8 +2,7 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 
 
 // Components
 // Components
-import UnitPicker from 'app/core/components/Select/UnitPicker';
-import { FormField, FormLabel, PanelOptionsGroup, Select } from '@grafana/ui';
+import { FormField, FormLabel, PanelOptionsGroup, Select, UnitPicker } from '@grafana/ui';
 
 
 // Types
 // Types
 import { SingleStatValueOptions } from './types';
 import { SingleStatValueOptions } from './types';

+ 1 - 1
public/app/plugins/panel/graph/tab_display.html

@@ -114,7 +114,7 @@
       label="Stack"
       label="Stack"
       label-class="width-7"
       label-class="width-7"
       checked="ctrl.panel.stack"
       checked="ctrl.panel.stack"
-      on-change="ctrl.refresh()"
+      on-change="ctrl.render()"
     >
     >
     </gf-form-switch>
     </gf-form-switch>
     <gf-form-switch
     <gf-form-switch

+ 3 - 3
public/app/plugins/panel/graph2/GraphPanelEditor.tsx

@@ -8,15 +8,15 @@ import { Options } from './types';
 
 
 export class GraphPanelEditor extends PureComponent<PanelEditorProps<Options>> {
 export class GraphPanelEditor extends PureComponent<PanelEditorProps<Options>> {
   onToggleLines = () => {
   onToggleLines = () => {
-    this.props.onChange({ ...this.props.options, showLines: !this.props.options.showLines });
+    this.props.onOptionsChange({ ...this.props.options, showLines: !this.props.options.showLines });
   };
   };
 
 
   onToggleBars = () => {
   onToggleBars = () => {
-    this.props.onChange({ ...this.props.options, showBars: !this.props.options.showBars });
+    this.props.onOptionsChange({ ...this.props.options, showBars: !this.props.options.showBars });
   };
   };
 
 
   onTogglePoints = () => {
   onTogglePoints = () => {
-    this.props.onChange({ ...this.props.options, showPoints: !this.props.options.showPoints });
+    this.props.onOptionsChange({ ...this.props.options, showPoints: !this.props.options.showPoints });
   };
   };
 
 
   render() {
   render() {

+ 4 - 4
public/app/routes/GrafanaCtrl.ts

@@ -75,7 +75,7 @@ export class GrafanaCtrl {
   }
   }
 }
 }
 
 
-function setViewModeBodyClass(body, mode: KioskUrlValue, sidemenuOpen: boolean) {
+function setViewModeBodyClass(body: JQuery, mode: KioskUrlValue, sidemenuOpen: boolean) {
   body.removeClass('view-mode--tv');
   body.removeClass('view-mode--tv');
   body.removeClass('view-mode--kiosk');
   body.removeClass('view-mode--kiosk');
   body.removeClass('view-mode--inactive');
   body.removeClass('view-mode--inactive');
@@ -174,8 +174,8 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
       });
       });
 
 
       // handle kiosk mode
       // handle kiosk mode
-      appEvents.on('toggle-kiosk-mode', options => {
-        const search = $location.search();
+      appEvents.on('toggle-kiosk-mode', (options: { exit?: boolean }) => {
+        const search: { kiosk?: KioskUrlValue } = $location.search();
 
 
         if (options && options.exit) {
         if (options && options.exit) {
           search.kiosk = '1';
           search.kiosk = '1';
@@ -197,7 +197,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
           }
           }
         }
         }
 
 
-        $location.search(search);
+        $timeout(() => $location.search(search));
         setViewModeBodyClass(body, search.kiosk, sidemenuOpen);
         setViewModeBodyClass(body, search.kiosk, sidemenuOpen);
       });
       });
 
 

+ 3 - 0
public/sass/base/_icons.scss

@@ -212,6 +212,9 @@
   .gicon-explore {
   .gicon-explore {
     background-image: url('../img/icons_dark_theme/icon_explore.svg');
     background-image: url('../img/icons_dark_theme/icon_explore.svg');
   }
   }
+  .gicon-shield {
+    background-image: url('../img/icons_dark_theme/icon_shield.svg');
+  }
 }
 }
 
 
 .fa--permissions-list {
 .fa--permissions-list {

+ 3 - 0
scripts/build/update_repo/init-deb-repo.sh

@@ -10,3 +10,6 @@ mkdir -p /deb-repo/db   \
 
 
 aptly repo create -distribution=stable -component=main grafana
 aptly repo create -distribution=stable -component=main grafana
 aptly repo create -distribution=beta -component=main beta
 aptly repo create -distribution=beta -component=main beta
+
+aptly publish repo -architectures=amd64,i386,arm64,armhf grafana filesystem:repo:grafana
+aptly publish repo -architectures=amd64,i386,arm64,armhf beta filesystem:repo:grafana

+ 17 - 0
scripts/circle-test-mysql.sh

@@ -0,0 +1,17 @@
+#!/bin/bash
+function exit_if_fail {
+    command=$@
+    echo "Executing '$command'"
+    eval $command
+    rc=$?
+    if [ $rc -ne 0 ]; then
+        echo "'$command' returned $rc."
+        exit $rc
+    fi
+}
+
+export GRAFANA_TEST_DB=mysql
+
+time for d in $(go list ./pkg/...); do
+  exit_if_fail go test -tags=integration $d
+done

+ 17 - 0
scripts/circle-test-postgres.sh

@@ -0,0 +1,17 @@
+#!/bin/bash
+function exit_if_fail {
+    command=$@
+    echo "Executing '$command'"
+    eval $command
+    rc=$?
+    if [ $rc -ne 0 ]; then
+        echo "'$command' returned $rc."
+        exit $rc
+    fi
+}
+
+export GRAFANA_TEST_DB=postgres
+
+time for d in $(go list ./pkg/...); do
+  exit_if_fail go test -tags=integration $d
+done

+ 13 - 1
scripts/webpack/webpack.common.js

@@ -10,7 +10,7 @@ module.exports = {
     path: path.resolve(__dirname, '../../public/build'),
     path: path.resolve(__dirname, '../../public/build'),
     filename: '[name].[hash].js',
     filename: '[name].[hash].js',
     // Keep publicPath relative for host.com/grafana/ deployments
     // Keep publicPath relative for host.com/grafana/ deployments
-    publicPath: "public/build/",
+    publicPath: 'public/build/',
   },
   },
   resolve: {
   resolve: {
     extensions: ['.ts', '.tsx', '.es6', '.js', '.json', '.svg'],
     extensions: ['.ts', '.tsx', '.es6', '.js', '.json', '.svg'],
@@ -61,6 +61,18 @@ module.exports = {
       }
       }
     ]
     ]
   },
   },
+  // https://webpack.js.org/plugins/split-chunks-plugin/#split-chunks-example-3
+  optimization: {
+    splitChunks: {
+      cacheGroups: {
+        commons: {
+          test: /[\\/]node_modules[\\/].*[jt]sx?$/,
+          name: 'vendor',
+          chunks: 'all'
+        }
+      }
+    }
+  },
   plugins: [
   plugins: [
     new ForkTsCheckerWebpackPlugin({
     new ForkTsCheckerWebpackPlugin({
       checkSyntacticErrors: true,
       checkSyntacticErrors: true,

+ 0 - 19
scripts/webpack/webpack.dev.js

@@ -58,25 +58,6 @@ module.exports = merge(common, {
     ]
     ]
   },
   },
 
 
-  optimization: {
-    splitChunks: {
-      cacheGroups: {
-        manifest: {
-          chunks: "initial",
-          test: "vendor",
-          name: "vendor",
-          enforce: true
-        },
-        vendor: {
-          chunks: "initial",
-          test: "vendor",
-          name: "vendor",
-          enforce: true
-        }
-      }
-    }
-  },
-
   plugins: [
   plugins: [
     new CleanWebpackPlugin('../../public/build', { allowExternal: true }),
     new CleanWebpackPlugin('../../public/build', { allowExternal: true }),
     new MiniCssExtractPlugin({
     new MiniCssExtractPlugin({

+ 0 - 11
scripts/webpack/webpack.prod.js

@@ -47,17 +47,7 @@ module.exports = merge(common, {
       })
       })
     ]
     ]
   },
   },
-
   optimization: {
   optimization: {
-    splitChunks: {
-      cacheGroups: {
-        commons: {
-          test: /[\\/]node_modules[\\/].*[jt]sx?$/,
-          name: "vendor",
-          chunks: "all"
-        }
-      }
-    },
     minimizer: [
     minimizer: [
       new UglifyJsPlugin({
       new UglifyJsPlugin({
         cache: true,
         cache: true,
@@ -67,7 +57,6 @@ module.exports = merge(common, {
       new OptimizeCSSAssetsPlugin({})
       new OptimizeCSSAssetsPlugin({})
     ]
     ]
   },
   },
-
   plugins: [
   plugins: [
     new MiniCssExtractPlugin({
     new MiniCssExtractPlugin({
       filename: "grafana.[name].[hash].css"
       filename: "grafana.[name].[hash].css"

+ 2 - 4
style_guides/frontend.md

@@ -21,10 +21,8 @@ Generally we follow the Airbnb  [React Style Guide](https://github.com/airbnb/ja
 
 
 * Components and types that needs to be used by external plugins needs to go into @grafana/ui
 * Components and types that needs to be used by external plugins needs to go into @grafana/ui
 * Components should get their own folder under features/xxx/components
 * Components should get their own folder under features/xxx/components
-  * Sub components can live in that component folders, so not small component needs their own folder
-  * Place test next to their component file (same dir)
-  * Mocks in __mocks__ dir
-  * Test utils in __tests__ dir
+  * Sub components can live in that component folders, so small component do not need their own folder
+  * Place test next to their component file (same dir)  
   * Component sass should live in the same folder as component code
   * Component sass should live in the same folder as component code
 * State logic & domain models should live in features/xxx/state
 * State logic & domain models should live in features/xxx/state
 * Containers (pages) can live in feature root features/xxx
 * Containers (pages) can live in feature root features/xxx

+ 18 - 0
yarn.lock

@@ -1501,6 +1501,19 @@
     react-input-autosize "^2.2.1"
     react-input-autosize "^2.2.1"
     react-transition-group "^2.2.1"
     react-transition-group "^2.2.1"
 
 
+"@torkelo/react-select@2.4.1":
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/@torkelo/react-select/-/react-select-2.4.1.tgz#fb7bcb8f7a12b3453bb817ca9a1294edecd1363b"
+  integrity sha512-x8798Y7WT4PSyNiEhk8JbsS5/EA+sxrObWkmfAnWNUJCDKoELWDCPrPBinRvITlCQYzLww5RaoNJutI5VBqKOQ==
+  dependencies:
+    classnames "^2.2.5"
+    emotion "^9.1.2"
+    memoize-one "^5.0.0"
+    prop-types "^15.6.0"
+    raf "^3.4.0"
+    react-input-autosize "^2.2.1"
+    react-transition-group "^2.2.1"
+
 "@types/chalk@^2.2.0":
 "@types/chalk@^2.2.0":
   version "2.2.0"
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/@types/chalk/-/chalk-2.2.0.tgz#b7f6e446f4511029ee8e3f43075fb5b73fbaa0ba"
   resolved "https://registry.yarnpkg.com/@types/chalk/-/chalk-2.2.0.tgz#b7f6e446f4511029ee8e3f43075fb5b73fbaa0ba"
@@ -11412,6 +11425,11 @@ memoize-one@^4.0.0:
   resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.1.0.tgz#a2387c58c03fff27ca390c31b764a79addf3f906"
   resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.1.0.tgz#a2387c58c03fff27ca390c31b764a79addf3f906"
   integrity sha512-2GApq0yI/b22J2j9rhbrAlsHb0Qcz+7yWxeLG8h+95sl1XPUgeLimQSOdur4Vw7cUhrBHwaUZxWFZueojqNRzA==
   integrity sha512-2GApq0yI/b22J2j9rhbrAlsHb0Qcz+7yWxeLG8h+95sl1XPUgeLimQSOdur4Vw7cUhrBHwaUZxWFZueojqNRzA==
 
 
+memoize-one@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.0.tgz#d55007dffefb8de7546659a1722a5d42e128286e"
+  integrity sha512-7g0+ejkOaI9w5x6LvQwmj68kUj6rxROywPSCqmclG/HBacmFnZqhVscQ8kovkn9FBCNJmOz6SY42+jnvZzDWdw==
+
 memory-fs@^0.4.0, memory-fs@~0.4.1:
 memory-fs@^0.4.0, memory-fs@~0.4.1:
   version "0.4.1"
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
   resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"