Explorar o código

Merge branch 'master' into add_google_hangouts_chat_notifier

* master: (322 commits)
  graphInterval needs to update after query execution, fixes #14364
  Explore: Parse initial dates
  Aligned styling of stats popover/box with rest of grafana & minor css refactoring
  Prometheus: Make result transformer more robust for empty responses
  Rebase fixes
  Explore: Logging line parsing and field stats
  fixed unit tests
  made unknown color theme aware and sync with graph color, some minor cleanup
  Explore: improve error handling
  use render props instead of cloneElement
  sort of a hacky way to figure if the small variation should be used for the label
  add basic button group component, using the the same label style as is
  explore logs styling
  wip: alternative level styling & hover effect
  wip: explore logs styling
  more detailed error message for loki
  If user login equals user email, only show the email once #14341
  UserPicker and TeamPicker should use min-width instead of fixed widths to avoid overflowing form buttons.  #14341
  wip: explore logs styling
  restoring monospace & making sure width are correct when hiding columns
  ...
bergquist %!s(int64=7) %!d(string=hai) anos
pai
achega
5d720674e3
Modificáronse 100 ficheiros con 1834 adicións e 505 borrados
  1. 11 0
      .babelrc
  2. 2 2
      .bra.toml
  3. 9 2
      .circleci/config.yml
  4. 46 15
      CHANGELOG.md
  5. 2 1
      Dockerfile
  6. 1 1
      Makefile
  7. 2 0
      README.md
  8. 2 2
      UPGRADING_DEPENDENCIES.md
  9. 2 0
      build.go
  10. 7 4
      conf/defaults.ini
  11. 1 1
      conf/provisioning/datasources/sample.yaml
  12. 3 0
      conf/sample.ini
  13. 670 245
      devenv/dev-dashboards/testdata_alerts.json
  14. 1 1
      devenv/docker/ha_test/docker-compose.yaml
  15. 1 0
      devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet
  16. 1 1
      docs/sources/administration/provisioning.md
  17. 5 1
      docs/sources/alerting/notifications.md
  18. 16 3
      docs/sources/alerting/rules.md
  19. 3 2
      docs/sources/auth/ldap.md
  20. 21 1
      docs/sources/auth/overview.md
  21. 2 2
      docs/sources/contribute/cla.md
  22. 1 1
      docs/sources/enterprise/index.md
  23. 3 3
      docs/sources/features/datasources/mysql.md
  24. 14 4
      docs/sources/features/datasources/stackdriver.md
  25. 1 1
      docs/sources/guides/whats-new-in-v4.md
  26. 83 0
      docs/sources/guides/whats-new-in-v5-4.md
  27. 4 1
      docs/sources/http_api/alerting.md
  28. 1 1
      docs/sources/http_api/index.md
  29. 39 39
      docs/sources/http_api/org.md
  30. 37 3
      docs/sources/http_api/user.md
  31. 3 3
      docs/sources/index.md
  32. 6 0
      docs/sources/installation/configuration.md
  33. 2 0
      docs/sources/reference/export_import.md
  34. 1 1
      docs/sources/reference/templating.md
  35. 2 1
      docs/versions.json
  36. 2 2
      latest.json
  37. 16 20
      package.json
  38. 1 1
      packaging/deb/init.d/grafana-server
  39. 1 0
      packaging/deb/systemd/grafana-server.service
  40. 2 1
      packaging/docker/Dockerfile
  41. 14 1
      packaging/docker/build-enterprise.sh
  42. 1 0
      packaging/docker/run.sh
  43. 1 1
      packaging/rpm/init.d/grafana-server
  44. 1 0
      packaging/rpm/systemd/grafana-server.service
  45. 6 0
      pkg/api/admin_users.go
  46. 50 0
      pkg/api/admin_users_test.go
  47. 1 1
      pkg/api/alerting.go
  48. 1 0
      pkg/api/api.go
  49. 19 0
      pkg/api/basic_auth.go
  50. 45 0
      pkg/api/basic_auth_test.go
  51. 0 4
      pkg/api/dashboard.go
  52. 0 1
      pkg/api/dashboard_test.go
  53. 9 0
      pkg/api/http_server.go
  54. 30 0
      pkg/api/http_server_test.go
  55. 22 0
      pkg/api/login.go
  56. 8 0
      pkg/api/password.go
  57. 3 3
      pkg/api/pluginproxy/ds_auth_provider.go
  58. 1 1
      pkg/api/pluginproxy/pluginproxy.go
  59. 11 3
      pkg/api/user.go
  60. 17 2
      pkg/cmd/grafana-server/main.go
  61. 26 6
      pkg/cmd/grafana-server/server.go
  62. 1 1
      pkg/components/dynmap/dynmap.go
  63. 1 1
      pkg/components/dynmap/dynmap_test.go
  64. 9 6
      pkg/metrics/metrics.go
  65. 3 0
      pkg/metrics/metrics_test.go
  66. 1 0
      pkg/middleware/recovery.go
  67. 10 3
      pkg/models/alert.go
  68. 0 1
      pkg/models/dashboards.go
  69. 2 1
      pkg/models/user.go
  70. 29 3
      pkg/services/alerting/eval_context.go
  71. 172 67
      pkg/services/alerting/eval_context_test.go
  72. 11 1
      pkg/services/alerting/extractor.go
  73. 13 7
      pkg/services/alerting/extractor_test.go
  74. 47 0
      pkg/services/alerting/notifiers/alertmanager_test.go
  75. 10 0
      pkg/services/alerting/notifiers/base.go
  76. 25 7
      pkg/services/alerting/notifiers/base_test.go
  77. 3 0
      pkg/services/alerting/result_handler.go
  78. 5 0
      pkg/services/alerting/rule.go
  79. 0 0
      pkg/services/alerting/testdata/collapsed-panels.json
  80. 0 0
      pkg/services/alerting/testdata/dash-without-id.json
  81. 1 0
      pkg/services/alerting/testdata/graphite-alert.json
  82. 0 0
      pkg/services/alerting/testdata/influxdb-alert.json
  83. 0 0
      pkg/services/alerting/testdata/panel-with-id-0.json
  84. 0 0
      pkg/services/alerting/testdata/panels-missing-id.json
  85. 0 0
      pkg/services/alerting/testdata/v5-dashboard.json
  86. 1 1
      pkg/services/dashboards/dashboard_service.go
  87. 5 4
      pkg/services/sqlstore/alert.go
  88. 2 2
      pkg/services/sqlstore/alert_test.go
  89. 4 0
      pkg/services/sqlstore/migrations/alert_mig.go
  90. 1 1
      pkg/services/sqlstore/org_test.go
  91. 6 6
      pkg/services/sqlstore/quota.go
  92. 65 0
      pkg/services/sqlstore/quota_test.go
  93. 25 1
      pkg/services/sqlstore/user.go
  94. 26 0
      pkg/services/sqlstore/user_test.go
  95. 9 0
      pkg/setting/setting.go
  96. 2 0
      pkg/social/google_oauth.go
  97. 25 2
      pkg/tsdb/cloudwatch/cloudwatch.go
  98. 22 0
      pkg/tsdb/cloudwatch/cloudwatch_test.go
  99. 4 0
      pkg/tsdb/cloudwatch/metric_find_query.go
  100. 1 1
      pkg/tsdb/elasticsearch/client/client.go

+ 11 - 0
.babelrc

@@ -0,0 +1,11 @@
+{
+  "presets": [
+    [
+      "@babel/preset-env",
+      {
+		  "targets": { "browsers": "last 3 versions" },
+		  "useBuiltIns": "entry"
+      }
+    ]
+  ]
+}

+ 2 - 2
.bra.toml

@@ -1,7 +1,7 @@
 [run]
 [run]
 init_cmds = [
 init_cmds = [
   ["go", "run", "build.go", "-dev", "build-server"],
   ["go", "run", "build.go", "-dev", "build-server"],
-	["./bin/grafana-server", "cfg:app_mode=development"]
+	["./bin/grafana-server", "-packaging=dev", "cfg:app_mode=development"]
 ]
 ]
 watch_all = true
 watch_all = true
 follow_symlinks = true
 follow_symlinks = true
@@ -14,5 +14,5 @@ watch_exts = [".go", ".ini", ".toml", ".template.html"]
 build_delay = 1500
 build_delay = 1500
 cmds = [
 cmds = [
   ["go", "run", "build.go", "-dev", "build-server"],
   ["go", "run", "build.go", "-dev", "build-server"],
-	["./bin/grafana-server", "cfg:app_mode=development"]
+	["./bin/grafana-server", "-packaging=dev", "cfg:app_mode=development"]
 ]
 ]

+ 9 - 2
.circleci/config.yml

@@ -162,8 +162,8 @@ jobs:
           name: Build Grafana.com master publisher
           name: Build Grafana.com master publisher
           command: 'go build -o scripts/publish scripts/build/publish.go'
           command: 'go build -o scripts/publish scripts/build/publish.go'
       - run:
       - run:
-          name: Build Grafana.com release publisher
-          command: 'cd scripts/build/release_publisher && go build -o release_publisher .'
+          name: Test and build Grafana.com release publisher
+          command: 'cd scripts/build/release_publisher && go test . && go build -o release_publisher .'
       - persist_to_workspace:
       - persist_to_workspace:
           root: .
           root: .
           paths:
           paths:
@@ -191,6 +191,9 @@ jobs:
       - run:
       - run:
           name: sha-sum packages
           name: sha-sum packages
           command: 'go run build.go sha-dist'
           command: 'go run build.go sha-dist'
+      - run:
+          name: Test Grafana.com release publisher
+          command: 'cd scripts/build/release_publisher && go test .'
       - persist_to_workspace:
       - persist_to_workspace:
           root: .
           root: .
           paths:
           paths:
@@ -359,6 +362,9 @@ jobs:
       - run:
       - run:
           name: deploy to gcp
           name: deploy to gcp
           command: '/opt/google-cloud-sdk/bin/gsutil cp ./enterprise-dist/* gs://$GCP_BUCKET_NAME/enterprise/release'
           command: '/opt/google-cloud-sdk/bin/gsutil cp ./enterprise-dist/* gs://$GCP_BUCKET_NAME/enterprise/release'
+      - run:
+          name: Deploy to Grafana.com
+          command: './scripts/build/publish.sh --enterprise'
 
 
   deploy-master:
   deploy-master:
     docker:
     docker:
@@ -507,6 +513,7 @@ workflows:
       - grafana-docker-release:
       - grafana-docker-release:
           requires:
           requires:
             - build-all
             - build-all
+            - build-all-enterprise
             - test-backend
             - test-backend
             - test-frontend
             - test-frontend
             - codespell
             - codespell

+ 46 - 15
CHANGELOG.md

@@ -1,7 +1,27 @@
-# 5.4.0 (unreleased)
+# 5.5.0 (unreleased)
+
+### Minor
+
+* **Elasticsearch**: Add support for offset in date histogram aggregation [#12653](https://github.com/grafana/grafana/issues/12653), thx [@mattiarossi](https://github.com/mattiarossi)
+* **Auth**: Prevent password reset when login form is disabled or either LDAP or Auth Proxy is enabled [#14246](https://github.com/grafana/grafana/issues/14246), thx [@SilverFire](https://github.com/SilverFire)
+* **Dataproxy**: Override incoming Authorization header [#13815](https://github.com/grafana/grafana/issues/13815), thx [@kornholi](https://github.com/kornholi)
+* **Admin**: Fix prevent removing last grafana admin permissions [#11067](https://github.com/grafana/grafana/issues/11067), thx [@danielbh](https://github.com/danielbh)
+
+# 5.4.0 (2018-12-03)
+
+* **Cloudwatch**: Fix invalid time range causes segmentation fault [#14150](https://github.com/grafana/grafana/issues/14150)
+* **Cloudwatch**: AWS/CodeBuild metrics and dimensions [#14167](https://github.com/grafana/grafana/issues/14167), thx [@mmcoltman](https://github.com/mmcoltman)
+* **MySQL**: Fix `$__timeFrom()` and `$__timeTo()` should respect local time zone [#14228](https://github.com/grafana/grafana/issues/14228)
+
+### 5.4.0-beta1 fixes
+* **Graph**: Fix legend always visible even if configured to be hidden [#14144](https://github.com/grafana/grafana/issues/14144)
+* **Elasticsearch**: Fix regression when using datasource version 6.0+ and alerting [#14175](https://github.com/grafana/grafana/pull/14175)
+
+# 5.4.0-beta1 (2018-11-20)
 
 
 ### New Features
 ### New Features
 
 
+* **Alerting**: Introduce alert debouncing with the `FOR` setting. [#7886](https://github.com/grafana/grafana/issues/7886) & [#6202](https://github.com/grafana/grafana/issues/6202)
 * **Alerting**: Option to disable OK alert notifications [#12330](https://github.com/grafana/grafana/issues/12330) & [#6696](https://github.com/grafana/grafana/issues/6696), thx [@davewat](https://github.com/davewat)
 * **Alerting**: Option to disable OK alert notifications [#12330](https://github.com/grafana/grafana/issues/12330) & [#6696](https://github.com/grafana/grafana/issues/6696), thx [@davewat](https://github.com/davewat)
 * **Postgres/MySQL/MSSQL**: Adds support for configuration of max open/idle connections and connection max lifetime. Also, panels with multiple SQL queries will now be executed concurrently [#11711](https://github.com/grafana/grafana/issues/11711), thx [@connection-reset](https://github.com/connection-reset)
 * **Postgres/MySQL/MSSQL**: Adds support for configuration of max open/idle connections and connection max lifetime. Also, panels with multiple SQL queries will now be executed concurrently [#11711](https://github.com/grafana/grafana/issues/11711), thx [@connection-reset](https://github.com/connection-reset)
 * **MySQL**: Graphical query builder [#13762](https://github.com/grafana/grafana/issues/13762), thx [svenklemm](https://github.com/svenklemm)
 * **MySQL**: Graphical query builder [#13762](https://github.com/grafana/grafana/issues/13762), thx [svenklemm](https://github.com/svenklemm)
@@ -10,29 +30,40 @@
 * **Stackdriver**: Not possible to authenticate using GCE metadata server [#13669](https://github.com/grafana/grafana/issues/13669)
 * **Stackdriver**: Not possible to authenticate using GCE metadata server [#13669](https://github.com/grafana/grafana/issues/13669)
 * **Teams**: Team preferences (theme, home dashboard, timezone) support [#12550](https://github.com/grafana/grafana/issues/12550)
 * **Teams**: Team preferences (theme, home dashboard, timezone) support [#12550](https://github.com/grafana/grafana/issues/12550)
 * **Graph**: Time regions support enabling highlight of weekdays and/or certain timespans [#5930](https://github.com/grafana/grafana/issues/5930)
 * **Graph**: Time regions support enabling highlight of weekdays and/or certain timespans [#5930](https://github.com/grafana/grafana/issues/5930)
+* **OAuth**: Automatic redirect to sign-in with OAuth [#11893](https://github.com/grafana/grafana/issues/11893), thx [@Nick-Triller](https://github.com/Nick-Triller)
+* **Stackdriver**: Template query editor [#13561](https://github.com/grafana/grafana/issues/13561)
 
 
 ### Minor
 ### Minor
 
 
+* **Security**: Upgrade macaron session package to fix security issue. [#14043](https://github.com/grafana/grafana/pull/14043)
 * **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda)
 * **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda)
 * **Cloudwatch**: AWS/Connect metrics and dimensions [#13970](https://github.com/grafana/grafana/pull/13970), thx [@zcoffy](https://github.com/zcoffy)
 * **Cloudwatch**: AWS/Connect metrics and dimensions [#13970](https://github.com/grafana/grafana/pull/13970), thx [@zcoffy](https://github.com/zcoffy)
+* **Cloudwatch**: CloudHSM metrics and dimensions [#14129](https://github.com/grafana/grafana/pull/14129), thx [@daktari](https://github.com/daktari)
+* **Cloudwatch**: Enable using variables in the stats field [#13810](https://github.com/grafana/grafana/issues/13810), thx [@mtanda](https://github.com/mtanda)
 * **Postgres**: Add delta window function to postgres query builder [#13925](https://github.com/grafana/grafana/issues/13925), thx [svenklemm](https://github.com/svenklemm)
 * **Postgres**: Add delta window function to postgres query builder [#13925](https://github.com/grafana/grafana/issues/13925), thx [svenklemm](https://github.com/svenklemm)
 * **Elasticsearch**: Fix switching to/from es raw document metric query [#6367](https://github.com/grafana/grafana/issues/6367)
 * **Elasticsearch**: Fix switching to/from es raw document metric query [#6367](https://github.com/grafana/grafana/issues/6367)
 * **Elasticsearch**: Fix deprecation warning about terms aggregation order key in Elasticsearch 6.x [#11977](https://github.com/grafana/grafana/issues/11977)
 * **Elasticsearch**: Fix deprecation warning about terms aggregation order key in Elasticsearch 6.x [#11977](https://github.com/grafana/grafana/issues/11977)
+* **Graph**: Render dots when no connecting line can be made [#13605](https://github.com/grafana/grafana/issues/13605), thx [@jsferrei](https://github.com/jsferrei)
 * **Table**: Fix CSS alpha background-color applied twice in table cell with link [#13606](https://github.com/grafana/grafana/issues/13606), thx [@grisme](https://github.com/grisme)
 * **Table**: Fix CSS alpha background-color applied twice in table cell with link [#13606](https://github.com/grafana/grafana/issues/13606), thx [@grisme](https://github.com/grisme)
+* **Singlestat**: Fix XSS in prefix/postfix [#13946](https://github.com/grafana/grafana/issues/13946), thx [@cinaglia](https://github.com/cinaglia)
 * **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg)
 * **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg)
 * **Alerting**: Increaste default duration for queries [#13945](https://github.com/grafana/grafana/pull/13945)
 * **Alerting**: Increaste default duration for queries [#13945](https://github.com/grafana/grafana/pull/13945)
 * **Alerting**: More options for the Slack Alert notifier [#13993](https://github.com/grafana/grafana/issues/13993), thx [@andreykaipov](https://github.com/andreykaipov)
 * **Alerting**: More options for the Slack Alert notifier [#13993](https://github.com/grafana/grafana/issues/13993), thx [@andreykaipov](https://github.com/andreykaipov)
 * **Alerting**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
 * **Alerting**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
+* **Alerting**: Increase Telegram captions length limit [#13876](https://github.com/grafana/grafana/pull/13876), thx [@skgsergio](https://github.com/skgsergio)
 * **Internal metrics**: Renamed `grafana_info` to `grafana_build_info` and added branch, goversion and revision [#13876](https://github.com/grafana/grafana/pull/13876)
 * **Internal metrics**: Renamed `grafana_info` to `grafana_build_info` and added branch, goversion and revision [#13876](https://github.com/grafana/grafana/pull/13876)
 * **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
 * **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
+* **OAuth**: Fix Google OAuth relies on email, not google account id [#13924](https://github.com/grafana/grafana/issues/13924), thx [@vinicyusmacedo](https://github.com/vinicyusmacedo)
+* **Dashboard**: Toggle legend using keyboard shortcut [#13655](https://github.com/grafana/grafana/issues/13655), thx [@davewat](https://github.com/davewat)
+* **Dashboard**: Fix render dashboard row drag handle only in edit mode [#13555](https://github.com/grafana/grafana/issues/13555), thx [@praveensastry](https://github.com/praveensastry)
+* **Teams**: Fix cannot select team if not included in initial search [#13425](https://github.com/grafana/grafana/issues/13425)
+* **Render**: Support full height screenshots using phantomjs render script [#13352](https://github.com/grafana/grafana/pull/13352), thx [@amuraru](https://github.com/amuraru)
+* **HTTP API**: Support retrieving teams by user [#14120](https://github.com/grafana/grafana/pull/14120), thx [@supercharlesliu](https://github.com/supercharlesliu)
+* **Metrics**: Add basic authentication to metrics endpoint [#13577](https://github.com/grafana/grafana/issues/13577), thx [@bobmshannon](https://github.com/bobmshannon)
 
 
 ### Breaking changes
 ### Breaking changes
 
 
-* Postgres/MySQL/MSSQL datasources now per default uses `max open connections` = `unlimited` (earlier 10), `max idle connections` = `2` (earlier 10) and `connection max lifetime` = `4` hours (earlier unlimited)
-
-# 5.3.5 (unreleased)
-
-* **Security**: Upgrade macaron session package to fix security issue. [#14043](https://github.com/grafana/grafana/pull/14043)
+* Postgres/MySQL/MSSQL datasources now per default uses `max open connections` = `unlimited` (earlier 10), `max idle connections` = `2` (earlier 10) and `connection max lifetime` = `4` hours (earlier unlimited).
 
 
 # 5.3.4 (2018-11-13)
 # 5.3.4 (2018-11-13)
 
 
@@ -140,7 +171,7 @@ See [security announcement](https://community.grafana.com/t/grafana-5-3-3-and-4-
 * **Alerting**: Fix rendering timeout which could cause notifications to not be sent due to rendering timing out [#12151](https://github.com/grafana/grafana/issues/12151)
 * **Alerting**: Fix rendering timeout which could cause notifications to not be sent due to rendering timing out [#12151](https://github.com/grafana/grafana/issues/12151)
 * **Docker**: Make it possible to set a specific plugin url [#12861](https://github.com/grafana/grafana/pull/12861), thx [ClementGautier](https://github.com/ClementGautier)
 * **Docker**: Make it possible to set a specific plugin url [#12861](https://github.com/grafana/grafana/pull/12861), thx [ClementGautier](https://github.com/ClementGautier)
 * **GrafanaCli**: Fixed issue with grafana-cli install plugin resulting in corrupt http response from source error. Fixes [#13079](https://github.com/grafana/grafana/issues/13079)
 * **GrafanaCli**: Fixed issue with grafana-cli install plugin resulting in corrupt http response from source error. Fixes [#13079](https://github.com/grafana/grafana/issues/13079)
-* **Provisioning**: Should allow one default datasource per organisation [#12229](https://github.com/grafana/grafana/issues/12229)
+* **Provisioning**: Should allow one default datasource per organization [#12229](https://github.com/grafana/grafana/issues/12229)
 * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)
 * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)
 * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)
 * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)
 * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
 * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
@@ -263,7 +294,7 @@ See [security announcement](https://community.grafana.com/t/grafana-5-2-3-and-4-
 * **Dashboard**: Prevent double-click when saving dashboard [#11963](https://github.com/grafana/grafana/issues/11963)
 * **Dashboard**: Prevent double-click when saving dashboard [#11963](https://github.com/grafana/grafana/issues/11963)
 * **Dashboard**: AutoFocus the add-panel search filter [#12189](https://github.com/grafana/grafana/pull/12189) thx [@ryantxu](https://github.com/ryantxu)
 * **Dashboard**: AutoFocus the add-panel search filter [#12189](https://github.com/grafana/grafana/pull/12189) thx [@ryantxu](https://github.com/ryantxu)
 * **Units**: W/m2 (energy), l/h (flow) and kPa (pressure) [#11233](https://github.com/grafana/grafana/pull/11233), thx [@flopp999](https://github.com/flopp999)
 * **Units**: W/m2 (energy), l/h (flow) and kPa (pressure) [#11233](https://github.com/grafana/grafana/pull/11233), thx [@flopp999](https://github.com/flopp999)
-* **Units**: Litre/min (flow) and milliLitre/min (flow) [#12282](https://github.com/grafana/grafana/pull/12282), thx [@flopp999](https://github.com/flopp999)
+* **Units**: Liter/min (flow) and milliLiter/min (flow) [#12282](https://github.com/grafana/grafana/pull/12282), thx [@flopp999](https://github.com/flopp999)
 * **Alerting**: Fix mobile notifications for Microsoft Teams alert notifier [#11484](https://github.com/grafana/grafana/pull/11484), thx [@manacker](https://github.com/manacker)
 * **Alerting**: Fix mobile notifications for Microsoft Teams alert notifier [#11484](https://github.com/grafana/grafana/pull/11484), thx [@manacker](https://github.com/manacker)
 * **Influxdb**: Add support for mode function [#12286](https://github.com/grafana/grafana/issues/12286)
 * **Influxdb**: Add support for mode function [#12286](https://github.com/grafana/grafana/issues/12286)
 * **Cloudwatch**: Fixes panic caused by bad timerange settings [#12199](https://github.com/grafana/grafana/issues/12199)
 * **Cloudwatch**: Fixes panic caused by bad timerange settings [#12199](https://github.com/grafana/grafana/issues/12199)
@@ -398,7 +429,7 @@ See [security announcement](https://community.grafana.com/t/grafana-5-2-3-and-4-
 * **Units**: Use B/s instead Bps for Bytes per second [#9342](https://github.com/grafana/grafana/pull/9342), thx [@mayli](https://github.com/mayli)
 * **Units**: Use B/s instead Bps for Bytes per second [#9342](https://github.com/grafana/grafana/pull/9342), thx [@mayli](https://github.com/mayli)
 * **Units**: Radiation units [#11001](https://github.com/grafana/grafana/issues/11001), thx [@victorclaessen](https://github.com/victorclaessen)
 * **Units**: Radiation units [#11001](https://github.com/grafana/grafana/issues/11001), thx [@victorclaessen](https://github.com/victorclaessen)
 * **Units**: Timeticks unit [#11183](https://github.com/grafana/grafana/pull/11183), thx [@jtyr](https://github.com/jtyr)
 * **Units**: Timeticks unit [#11183](https://github.com/grafana/grafana/pull/11183), thx [@jtyr](https://github.com/jtyr)
-* **Units**: Concentration units and "Normal cubic metre" [#11211](https://github.com/grafana/grafana/issues/11211), thx [@flopp999](https://github.com/flopp999)
+* **Units**: Concentration units and "Normal cubic meter" [#11211](https://github.com/grafana/grafana/issues/11211), thx [@flopp999](https://github.com/flopp999)
 * **Units**: New currency - Czech koruna [#11384](https://github.com/grafana/grafana/pull/11384), thx [@Rohlik](https://github.com/Rohlik)
 * **Units**: New currency - Czech koruna [#11384](https://github.com/grafana/grafana/pull/11384), thx [@Rohlik](https://github.com/Rohlik)
 * **Avatar**: Fix DISABLE_GRAVATAR option [#11095](https://github.com/grafana/grafana/issues/11095)
 * **Avatar**: Fix DISABLE_GRAVATAR option [#11095](https://github.com/grafana/grafana/issues/11095)
 * **Heatmap**: Disable log scale when using time time series buckets [#10792](https://github.com/grafana/grafana/issues/10792)
 * **Heatmap**: Disable log scale when using time time series buckets [#10792](https://github.com/grafana/grafana/issues/10792)
@@ -715,7 +746,7 @@ See [security announcement](https://community.grafana.com/t/grafana-5-2-3-and-4-
 ## Enhancements
 ## Enhancements
 
 
 * **GitHub OAuth**: Support for GitHub organizations with 100+ teams. [#8846](https://github.com/grafana/grafana/issues/8846), thx [@skwashd](https://github.com/skwashd)
 * **GitHub OAuth**: Support for GitHub organizations with 100+ teams. [#8846](https://github.com/grafana/grafana/issues/8846), thx [@skwashd](https://github.com/skwashd)
-* **Graphite**: Calls to Graphite api /metrics/find now include panel or dashboad time range (from & until) in most cases, [#8055](https://github.com/grafana/grafana/issues/8055)
+* **Graphite**: Calls to Graphite api /metrics/find now include panel or dashboard time range (from & until) in most cases, [#8055](https://github.com/grafana/grafana/issues/8055)
 * **Graphite**: Added new graphite 1.0 functions, available if you set version to 1.0.x in data source settings. New Functions: mapSeries, reduceSeries, isNonNull, groupByNodes, offsetToZero, grep, weightedAverage, removeEmptySeries, aggregateLine, averageOutsidePercentile, delay, exponentialMovingAverage, fallbackSeries, integralByInterval, interpolate, invert, linearRegression, movingMin, movingMax, movingSum, multiplySeriesWithWildcards, pow, powSeries, removeBetweenPercentile, squareRoot, timeSlice, closes [#8261](https://github.com/grafana/grafana/issues/8261)
 * **Graphite**: Added new graphite 1.0 functions, available if you set version to 1.0.x in data source settings. New Functions: mapSeries, reduceSeries, isNonNull, groupByNodes, offsetToZero, grep, weightedAverage, removeEmptySeries, aggregateLine, averageOutsidePercentile, delay, exponentialMovingAverage, fallbackSeries, integralByInterval, interpolate, invert, linearRegression, movingMin, movingMax, movingSum, multiplySeriesWithWildcards, pow, powSeries, removeBetweenPercentile, squareRoot, timeSlice, closes [#8261](https://github.com/grafana/grafana/issues/8261)
 - **Elasticsearch**: Ad-hoc filters now use query phrase match filters instead of term filters, works on non keyword/raw fields [#9095](https://github.com/grafana/grafana/issues/9095).
 - **Elasticsearch**: Ad-hoc filters now use query phrase match filters instead of term filters, works on non keyword/raw fields [#9095](https://github.com/grafana/grafana/issues/9095).
 
 
@@ -880,7 +911,7 @@ Pull Request: [#8472](https://github.com/grafana/grafana/pull/8472)
 * **InfluxDB**: Influxb Datasource test passes even if the Database doesn't exist [#7864](https://github.com/grafana/grafana/issues/7864)
 * **InfluxDB**: Influxb Datasource test passes even if the Database doesn't exist [#7864](https://github.com/grafana/grafana/issues/7864)
 * **Prometheus**: Displaying Prometheus annotations is incredibly slow [#7750](https://github.com/grafana/grafana/issues/7750), thx [@mtanda](https://github.com/mtanda)
 * **Prometheus**: Displaying Prometheus annotations is incredibly slow [#7750](https://github.com/grafana/grafana/issues/7750), thx [@mtanda](https://github.com/mtanda)
 * **Graphite**: grafana generates empty find query to graphite -> 422 Unprocessable Entity [#7740](https://github.com/grafana/grafana/issues/7740)
 * **Graphite**: grafana generates empty find query to graphite -> 422 Unprocessable Entity [#7740](https://github.com/grafana/grafana/issues/7740)
-* **Admin**: make organisation filter case insensitive [#8194](https://github.com/grafana/grafana/issues/8194), thx [@Alexander-N](https://github.com/Alexander-N)
+* **Admin**: make organization filter case insensitive [#8194](https://github.com/grafana/grafana/issues/8194), thx [@Alexander-N](https://github.com/Alexander-N)
 
 
 ## Changes
 ## Changes
 * **Elasticsearch**: Changed elasticsearch Terms aggregation to default to Min Doc Count to 1, and sort order to Top [#8321](https://github.com/grafana/grafana/issues/8321)
 * **Elasticsearch**: Changed elasticsearch Terms aggregation to default to Min Doc Count to 1, and sort order to Top [#8321](https://github.com/grafana/grafana/issues/8321)
@@ -1008,7 +1039,7 @@ Pull Request: [#8472](https://github.com/grafana/grafana/pull/8472)
 * **CLI**: Make it possible to reset the admin password using the grafana-cli. [#5479](https://github.com/grafana/grafana/issues/5479)
 * **CLI**: Make it possible to reset the admin password using the grafana-cli. [#5479](https://github.com/grafana/grafana/issues/5479)
 * **Influxdb**: Support multiple tags in InfluxDB annotations. [#4550](https://github.com/grafana/grafana/pull/4550), thx [@adrianlzt](https://github.com/adrianlzt)
 * **Influxdb**: Support multiple tags in InfluxDB annotations. [#4550](https://github.com/grafana/grafana/pull/4550), thx [@adrianlzt](https://github.com/adrianlzt)
 * **LDAP**:  Basic Auth now supports LDAP username and password, [#6940](https://github.com/grafana/grafana/pull/6940), thx [@utkarshcmu](https://github.com/utkarshcmu)
 * **LDAP**:  Basic Auth now supports LDAP username and password, [#6940](https://github.com/grafana/grafana/pull/6940), thx [@utkarshcmu](https://github.com/utkarshcmu)
-* **LDAP**: Now works with Auth Proxy, role and organisation mapping & sync will regularly be performed. [#6895](https://github.com/grafana/grafana/pull/6895), thx [@Seuf](https://github.com/seuf)
+* **LDAP**: Now works with Auth Proxy, role and organization mapping & sync will regularly be performed. [#6895](https://github.com/grafana/grafana/pull/6895), thx [@Seuf](https://github.com/seuf)
 * **Alerting**: Adds OK as no data option. [#6866](https://github.com/grafana/grafana/issues/6866)
 * **Alerting**: Adds OK as no data option. [#6866](https://github.com/grafana/grafana/issues/6866)
 * **Alert list**: Order alerts based on state. [#6676](https://github.com/grafana/grafana/issues/6676)
 * **Alert list**: Order alerts based on state. [#6676](https://github.com/grafana/grafana/issues/6676)
 * **Alerting**: Add api endpoint for pausing all alerts. [#6589](https://github.com/grafana/grafana/issues/6589)
 * **Alerting**: Add api endpoint for pausing all alerts. [#6589](https://github.com/grafana/grafana/issues/6589)
@@ -1147,7 +1178,7 @@ due to too many connections/file handles on the data source backend. This proble
 * **Scripts**: Use restart instead of start for deb package script, closes [#5282](https://github.com/grafana/grafana/pull/5282)
 * **Scripts**: Use restart instead of start for deb package script, closes [#5282](https://github.com/grafana/grafana/pull/5282)
 * **Logging**: Moved to structured logging lib, and moved to component specific level filters via config file, closes [#4590](https://github.com/grafana/grafana/issues/4590)
 * **Logging**: Moved to structured logging lib, and moved to component specific level filters via config file, closes [#4590](https://github.com/grafana/grafana/issues/4590)
 * **OpenTSDB**: Support nested template variables in tag_values function, closes [#4398](https://github.com/grafana/grafana/issues/4398)
 * **OpenTSDB**: Support nested template variables in tag_values function, closes [#4398](https://github.com/grafana/grafana/issues/4398)
-* **Datasource**: Pending data source requests are cancelled before new ones are issues (Graphite & Prometheus), closes [#5321](https://github.com/grafana/grafana/issues/5321)
+* **Datasource**: Pending data source requests are canceled before new ones are issues (Graphite & Prometheus), closes [#5321](https://github.com/grafana/grafana/issues/5321)
 
 
 ### Breaking changes
 ### Breaking changes
 * **Logging** : Changed default logging output format (now structured into message, and key value pairs, with logger key acting as component). You can also no change in config to json log output.
 * **Logging** : Changed default logging output format (now structured into message, and key value pairs, with logger key acting as component). You can also no change in config to json log output.
@@ -1851,7 +1882,7 @@ Read this [blog post](https://grafana.com/blog/2014/09/11/grafana-1.8.0-rc1-rele
 
 
 #### Fixes
 #### Fixes
 - [Issue #126](https://github.com/grafana/grafana/issues/126). Graphite query lexer change, can now handle regex parameters for aliasSub function
 - [Issue #126](https://github.com/grafana/grafana/issues/126). Graphite query lexer change, can now handle regex parameters for aliasSub function
-- [Issue #447](https://github.com/grafana/grafana/issues/447). Filter option loading when having muliple nested filters now works better. Options are now reloaded correctly and there are no multiple renders/refresh in between.
+- [Issue #447](https://github.com/grafana/grafana/issues/447). Filter option loading when having multiple nested filters now works better. Options are now reloaded correctly and there are no multiple renders/refresh in between.
 - [Issue #412](https://github.com/grafana/grafana/issues/412). After a filter option is changed and a nested template param is reloaded, if the current value exists after the options are reloaded the current selected value is kept.
 - [Issue #412](https://github.com/grafana/grafana/issues/412). After a filter option is changed and a nested template param is reloaded, if the current value exists after the options are reloaded the current selected value is kept.
 - [Issue #460](https://github.com/grafana/grafana/issues/460). Legend Current value did not display when value was zero
 - [Issue #460](https://github.com/grafana/grafana/issues/460). Legend Current value did not display when value was zero
 - [Issue #328](https://github.com/grafana/grafana/issues/328). Fix to series toggling bug that caused annotations to be hidden when toggling/hiding series.
 - [Issue #328](https://github.com/grafana/grafana/issues/328). Fix to series toggling bug that caused annotations to be hidden when toggling/hiding series.
@@ -1886,7 +1917,7 @@ Read this [blog post](https://grafana.com/blog/2014/09/11/grafana-1.8.0-rc1-rele
 - Graphite errors are now much easier to see and troubleshoot with the new inspector ([Issue #265](https://github.com/grafana/grafana/issues/265))
 - Graphite errors are now much easier to see and troubleshoot with the new inspector ([Issue #265](https://github.com/grafana/grafana/issues/265))
 - Use influxdb aliases to distinguish between multiple columns ([Issue #283](https://github.com/grafana/grafana/issues/283))
 - Use influxdb aliases to distinguish between multiple columns ([Issue #283](https://github.com/grafana/grafana/issues/283))
 - Correction to ms axis formater, now formats days correctly. ([Issue #189](https://github.com/grafana/grafana/issues/189))
 - Correction to ms axis formater, now formats days correctly. ([Issue #189](https://github.com/grafana/grafana/issues/189))
-- Css fix for Firefox and using top menu dropdowns in panel fullscren / edit mode ([Issue #106](https://github.com/grafana/grafana/issues/106))
+- Css fix for Firefox and using top menu dropdowns in panel fullscreen / edit mode ([Issue #106](https://github.com/grafana/grafana/issues/106))
 - Browser page title is now Grafana - {{dashboard title}} ([Issue #294](https://github.com/grafana/grafana/issues/294))
 - Browser page title is now Grafana - {{dashboard title}} ([Issue #294](https://github.com/grafana/grafana/issues/294))
 - Disable auto refresh zooming in (every time you change to an absolute time range), refresh will be restored when you change time range back to relative ([Issue #282](https://github.com/grafana/grafana/issues/282))
 - Disable auto refresh zooming in (every time you change to an absolute time range), refresh will be restored when you change time range back to relative ([Issue #282](https://github.com/grafana/grafana/issues/282))
 - More graphite functions
 - More graphite functions

+ 2 - 1
Dockerfile

@@ -50,7 +50,8 @@ ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bi
 
 
 WORKDIR $GF_PATHS_HOME
 WORKDIR $GF_PATHS_HOME
 
 
-RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates && \
+RUN apt-get update && apt-get upgrade -y && \
+    apt-get install -qq -y libfontconfig ca-certificates && \
     apt-get autoremove -y && \
     apt-get autoremove -y && \
     rm -rf /var/lib/apt/lists/*
     rm -rf /var/lib/apt/lists/*
 
 

+ 1 - 1
Makefile

@@ -25,7 +25,7 @@ build: build-go build-js
 
 
 build-docker-dev:
 build-docker-dev:
 	@echo "\033[92mInfo:\033[0m the frontend code is expected to be built already."
 	@echo "\033[92mInfo:\033[0m the frontend code is expected to be built already."
-	go run build.go -goos linux -pkg-arch amd64 ${OPT} build package-only latest
+	go run build.go -goos linux -pkg-arch amd64 ${OPT} build pkg-archive latest
 	cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
 	cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
 	cd packaging/docker && docker build --tag grafana/grafana:dev .
 	cd packaging/docker && docker build --tag grafana/grafana:dev .
 
 

+ 2 - 0
README.md

@@ -90,6 +90,8 @@ Choose this option to build on platforms other than linux/amd64 and/or not have
 
 
 The resulting image will be tagged as `grafana/grafana:dev`
 The resulting image will be tagged as `grafana/grafana:dev`
 
 
+Notice: If you are using Docker for MacOS, be sure to let limit of Memory bigger than 2 GiB (at docker -> Perferences -> Advanced), otherwize you may faild at `grunt build`
+
 ### Dev config
 ### Dev config
 
 
 Create a custom.ini in the conf directory to override default configuration options.
 Create a custom.ini in the conf directory to override default configuration options.

+ 2 - 2
UPGRADING_DEPENDENCIES.md

@@ -47,7 +47,7 @@ Our builds run on CircleCI through our build script.
 
 
 ### grafana/build-container
 ### grafana/build-container
 
 
-The main build step (in CircleCI) is built using a custom build container that comes pre-baked with some of the neccesary dependencies.
+The main build step (in CircleCI) is built using a custom build container that comes pre-baked with some of the necessary dependencies.
 
 
 Link: [grafana-build-container](https://github.com/grafana/grafana-build-container)
 Link: [grafana-build-container](https://github.com/grafana/grafana-build-container)
 
 
@@ -86,4 +86,4 @@ There is a Docker build for Grafana in the root of the project that allows anyon
 
 
 ### Local developer environments
 ### Local developer environments
 
 
-Please send out a notice in the grafana-dev slack channel when updating Go or Node.js to make it easier for everyone to update their local developer environments.
+Please send out a notice in the grafana-dev slack channel when updating Go or Node.js to make it easier for everyone to update their local developer environments.

+ 2 - 0
build.go

@@ -128,6 +128,8 @@ func main() {
 			if goos == linux {
 			if goos == linux {
 				createLinuxPackages()
 				createLinuxPackages()
 			}
 			}
+		case "pkg-archive":
+			grunt(gruntBuildArg("package")...)
 
 
 		case "pkg-rpm":
 		case "pkg-rpm":
 			grunt(gruntBuildArg("release")...)
 			grunt(gruntBuildArg("release")...)

+ 7 - 4
conf/defaults.ini

@@ -34,7 +34,7 @@ protocol = http
 # The ip address to bind to, empty will bind to all interfaces
 # The ip address to bind to, empty will bind to all interfaces
 http_addr =
 http_addr =
 
 
-# The http port  to use
+# The http port to use
 http_port = 3000
 http_port = 3000
 
 
 # The public facing domain name used to access grafana from a browser
 # The public facing domain name used to access grafana from a browser
@@ -166,7 +166,7 @@ google_tag_manager_id =
 # default admin user, created on startup
 # default admin user, created on startup
 admin_user = admin
 admin_user = admin
 
 
-# default admin password, can be changed before first start of grafana,  or in profile settings
+# default admin password, can be changed before first start of grafana, or in profile settings
 admin_password = admin
 admin_password = admin
 
 
 # used for signing
 # used for signing
@@ -372,7 +372,7 @@ templates_pattern = emails/*.html
 
 
 #################################### Logging ##########################
 #################################### Logging ##########################
 [log]
 [log]
-# Either "console", "file", "syslog". Default is console and  file
+# Either "console", "file", "syslog". Default is console and file
 # Use space to separate multiple modes, e.g. "console file"
 # Use space to separate multiple modes, e.g. "console file"
 mode = console file
 mode = console file
 
 
@@ -490,6 +490,10 @@ enabled = false
 enabled           = true
 enabled           = true
 interval_seconds  = 10
 interval_seconds  = 10
 
 
+#If both are set, basic auth will be required for the metrics endpoint.
+basic_auth_username =
+basic_auth_password =
+
 # Send internal Grafana metrics to graphite
 # Send internal Grafana metrics to graphite
 [metrics.graphite]
 [metrics.graphite]
 # Enable by setting the address setting (ex localhost:2003)
 # Enable by setting the address setting (ex localhost:2003)
@@ -561,4 +565,3 @@ enable_alpha = false
 
 
 [enterprise]
 [enterprise]
 license_path =
 license_path =
-

+ 1 - 1
conf/provisioning/datasources/sample.yaml

@@ -7,7 +7,7 @@ apiVersion: 1
 #     orgId: 1
 #     orgId: 1
 
 
 # # list of datasources to insert/update depending
 # # list of datasources to insert/update depending
-# # on what's available in the datbase
+# # on what's available in the database
 #datasources:
 #datasources:
 #   # <string, required> name of the datasource. Required
 #   # <string, required> name of the datasource. Required
 # - name: Graphite
 # - name: Graphite

+ 3 - 0
conf/sample.ini

@@ -145,6 +145,9 @@ log_queries =
 # Google Analytics universal tracking code, only enabled if you specify an id here
 # Google Analytics universal tracking code, only enabled if you specify an id here
 ;google_analytics_ua_id =
 ;google_analytics_ua_id =
 
 
+# Google Tag Manager ID, only enabled if you specify an id here
+;google_tag_manager_id =
+
 #################################### Security ####################################
 #################################### Security ####################################
 [security]
 [security]
 # default admin user, created on startup
 # default admin user, created on startup

+ 670 - 245
devenv/dev-dashboards/testdata_alerts.json

@@ -1,250 +1,681 @@
 {
 {
-  "revision": 2,
-  "title": "Alerting with TestData",
-  "tags": [
-    "grafana-test"
-  ],
-  "style": "dark",
-  "timezone": "browser",
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      }
+    ]
+  },
   "editable": true,
   "editable": true,
-  "hideControls": false,
-  "sharedCrosshair": false,
-  "rows": [
+  "gnetId": null,
+  "graphTooltip": 0,
+  "links": [],
+  "panels": [
     {
     {
-      "collapse": false,
+      "alert": {
+        "conditions": [
+          {
+            "evaluator": {
+              "params": [
+                60
+              ],
+              "type": "gt"
+            },
+            "query": {
+              "params": [
+                "A",
+                "5m",
+                "now"
+              ]
+            },
+            "reducer": {
+              "params": [],
+              "type": "avg"
+            },
+            "type": "query"
+          }
+        ],
+        "enabled": true,
+        "frequency": "60s",
+        "handler": 1,
+        "name": "TestData - Always OK",
+        "noDataState": "no_data",
+        "notifications": []
+      },
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
       "editable": true,
       "editable": true,
-      "height": 255.625,
-      "panels": [
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "id": 3,
+      "isNew": true,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "connected",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenario": "random_walk",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "1,20,90,30,5,0",
+          "target": ""
+        }
+      ],
+      "thresholds": [
+        {
+          "colorMode": "critical",
+          "fill": true,
+          "line": true,
+          "op": "gt",
+          "value": 60
+        }
+      ],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "Always OK",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": "",
+          "logBase": 1,
+          "max": "125",
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": null,
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "alert": {
+        "conditions": [
+          {
+            "evaluator": {
+              "params": [
+                177
+              ],
+              "type": "gt"
+            },
+            "query": {
+              "params": [
+                "A",
+                "5m",
+                "now"
+              ]
+            },
+            "reducer": {
+              "params": [],
+              "type": "avg"
+            },
+            "type": "query"
+          }
+        ],
+        "enabled": true,
+        "executionErrorState": "alerting",
+        "for": "0m",
+        "frequency": "60s",
+        "handler": 1,
+        "name": "TestData - Always Alerting",
+        "noDataState": "no_data",
+        "notifications": []
+      },
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 12,
+        "x": 12,
+        "y": 0
+      },
+      "id": 4,
+      "isNew": true,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "connected",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenario": "random_walk",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "200,445,100,150,200,220,190",
+          "target": ""
+        }
+      ],
+      "thresholds": [
+        {
+          "colorMode": "critical",
+          "fill": true,
+          "line": true,
+          "op": "gt",
+          "value": 177
+        }
+      ],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "Always Alerting",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": "",
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": "",
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "alert": {
+        "conditions": [
+          {
+            "evaluator": {
+              "params": [
+                1
+              ],
+              "type": "gt"
+            },
+            "operator": {
+              "type": "and"
+            },
+            "query": {
+              "params": [
+                "A",
+                "15m",
+                "now"
+              ]
+            },
+            "reducer": {
+              "params": [],
+              "type": "avg"
+            },
+            "type": "query"
+          }
+        ],
+        "executionErrorState": "alerting",
+        "for": "5m",
+        "frequency": "1m",
+        "handler": 1,
+        "name": "TestData - No data",
+        "noDataState": "no_data",
+        "notifications": []
+      },
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 12,
+        "x": 0,
+        "y": 7
+      },
+      "id": 5,
+      "isNew": true,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "connected",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenario": "random_walk",
+          "scenarioId": "no_data_points",
+          "stringInput": "",
+          "target": ""
+        }
+      ],
+      "thresholds": [
+        {
+          "colorMode": "critical",
+          "fill": true,
+          "line": true,
+          "op": "gt",
+          "value": 1
+        }
+      ],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "No data",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": "",
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
         {
         {
-          "alert": {
-            "conditions": [
-              {
-                "evaluator": {
-                  "params": [
-                    60
-                  ],
-                  "type": "gt"
-                },
-                "query": {
-                  "params": [
-                    "A",
-                    "5m",
-                    "now"
-                  ]
-                },
-                "reducer": {
-                  "params": [],
-                  "type": "avg"
-                },
-                "type": "query"
-              }
-            ],
-            "enabled": true,
-            "frequency": "60s",
-            "handler": 1,
-            "name": "TestData - Always OK",
-            "noDataState": "no_data",
-            "notifications": []
-          },
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "gdev-testdata",
-          "editable": true,
-          "error": false,
-          "fill": 1,
-          "id": 3,
-          "isNew": true,
-          "legend": {
-            "avg": false,
-            "current": false,
-            "max": false,
-            "min": false,
-            "show": true,
-            "total": false,
-            "values": false
-          },
-          "lines": true,
-          "linewidth": 2,
-          "links": [],
-          "nullPointMode": "connected",
-          "percentage": false,
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [],
-          "span": 6,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "refId": "A",
-              "scenario": "random_walk",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "1,20,90,30,5,0",
-              "target": ""
-            }
-          ],
-          "thresholds": [
-            {
-              "value": 60,
-              "op": "gt",
-              "fill": true,
-              "line": true,
-              "colorMode": "critical"
-            }
-          ],
-          "timeFrom": null,
-          "timeShift": null,
-          "title": "Always OK",
-          "tooltip": {
-            "msResolution": false,
-            "shared": true,
-            "sort": 0,
-            "value_type": "cumulative"
-          },
-          "type": "graph",
-          "xaxis": {
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "format": "short",
-              "label": "",
-              "logBase": 1,
-              "max": "125",
-              "min": "0",
-              "show": true
+          "format": "short",
+          "label": "",
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "alert": {
+        "conditions": [
+          {
+            "evaluator": {
+              "params": [
+                177
+              ],
+              "type": "gt"
             },
             },
-            {
-              "format": "short",
-              "label": null,
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            }
-          ]
+            "operator": {
+              "type": "and"
+            },
+            "query": {
+              "params": [
+                "A",
+                "15m",
+                "now"
+              ]
+            },
+            "reducer": {
+              "params": [],
+              "type": "avg"
+            },
+            "type": "query"
+          }
+        ],
+        "executionErrorState": "alerting",
+        "for": "1m",
+        "frequency": "1m",
+        "handler": 1,
+        "name": "TestData - Always Pending",
+        "noDataState": "no_data",
+        "notifications": []
+      },
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 12,
+        "x": 12,
+        "y": 7
+      },
+      "id": 6,
+      "isNew": true,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "connected",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenario": "random_walk",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "200,445,100,150,200,220,190",
+          "target": ""
+        }
+      ],
+      "thresholds": [
+        {
+          "colorMode": "critical",
+          "fill": true,
+          "line": true,
+          "op": "gt",
+          "value": 177
+        }
+      ],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "Always Alerting with For",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": "",
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
         },
         },
         {
         {
-          "alert": {
-            "conditions": [
-              {
-                "evaluator": {
-                  "params": [
-                    177
-                  ],
-                  "type": "gt"
-                },
-                "query": {
-                  "params": [
-                    "A",
-                    "5m",
-                    "now"
-                  ]
-                },
-                "reducer": {
-                  "params": [],
-                  "type": "avg"
-                },
-                "type": "query"
-              }
-            ],
-            "enabled": true,
-            "frequency": "60s",
-            "handler": 1,
-            "name": "TestData - Always Alerting",
-            "noDataState": "no_data",
-            "notifications": []
-          },
-          "aliasColors": {},
-          "bars": false,
-          "datasource": "gdev-testdata",
-          "editable": true,
-          "error": false,
-          "fill": 1,
-          "id": 4,
-          "isNew": true,
-          "legend": {
-            "avg": false,
-            "current": false,
-            "max": false,
-            "min": false,
-            "show": true,
-            "total": false,
-            "values": false
-          },
-          "lines": true,
-          "linewidth": 2,
-          "links": [],
-          "nullPointMode": "connected",
-          "percentage": false,
-          "pointradius": 5,
-          "points": false,
-          "renderer": "flot",
-          "seriesOverrides": [],
-          "span": 6,
-          "stack": false,
-          "steppedLine": false,
-          "targets": [
-            {
-              "refId": "A",
-              "scenario": "random_walk",
-              "scenarioId": "csv_metric_values",
-              "stringInput": "200,445,100,150,200,220,190",
-              "target": ""
-            }
-          ],
-          "thresholds": [
-            {
-              "colorMode": "critical",
-              "fill": true,
-              "line": true,
-              "op": "gt",
-              "value": 177
-            }
-          ],
-          "timeFrom": null,
-          "timeShift": null,
-          "title": "Always Alerting",
-          "tooltip": {
-            "msResolution": false,
-            "shared": true,
-            "sort": 0,
-            "value_type": "cumulative"
-          },
-          "type": "graph",
-          "xaxis": {
-            "mode": "time",
-            "name": null,
-            "show": true,
-            "values": []
-          },
-          "yaxes": [
-            {
-              "format": "short",
-              "label": "",
-              "logBase": 1,
-              "max": null,
-              "min": "0",
-              "show": true
+          "format": "short",
+          "label": "",
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
+    },
+    {
+      "alert": {
+        "conditions": [
+          {
+            "evaluator": {
+              "params": [
+                100
+              ],
+              "type": "gt"
+            },
+            "operator": {
+              "type": "and"
+            },
+            "query": {
+              "params": [
+                "A",
+                "5m",
+                "now"
+              ]
             },
             },
-            {
-              "format": "short",
-              "label": "",
-              "logBase": 1,
-              "max": null,
-              "min": null,
-              "show": true
-            }
-          ]
+            "reducer": {
+              "params": [],
+              "type": "avg"
+            },
+            "type": "query"
+          }
+        ],
+        "executionErrorState": "alerting",
+        "for": "900000h",
+        "frequency": "1m",
+        "handler": 1,
+        "name": "Always Pending",
+        "noDataState": "no_data",
+        "notifications": []
+      },
+      "aliasColors": {},
+      "bars": false,
+      "dashLength": 10,
+      "dashes": false,
+      "datasource": "gdev-testdata",
+      "editable": true,
+      "error": false,
+      "fill": 1,
+      "gridPos": {
+        "h": 7,
+        "w": 12,
+        "x": 12,
+        "y": 14
+      },
+      "id": 7,
+      "isNew": true,
+      "legend": {
+        "avg": false,
+        "current": false,
+        "max": false,
+        "min": false,
+        "show": true,
+        "total": false,
+        "values": false
+      },
+      "lines": true,
+      "linewidth": 2,
+      "links": [],
+      "nullPointMode": "connected",
+      "percentage": false,
+      "pointradius": 5,
+      "points": false,
+      "renderer": "flot",
+      "seriesOverrides": [],
+      "spaceLength": 10,
+      "stack": false,
+      "steppedLine": false,
+      "targets": [
+        {
+          "refId": "A",
+          "scenario": "random_walk",
+          "scenarioId": "csv_metric_values",
+          "stringInput": "200,445,100,150,200,220,190",
+          "target": ""
         }
         }
       ],
       ],
-      "title": "New row"
+      "thresholds": [
+        {
+          "colorMode": "critical",
+          "fill": true,
+          "line": true,
+          "op": "gt",
+          "value": 100
+        }
+      ],
+      "timeFrom": null,
+      "timeRegions": [],
+      "timeShift": null,
+      "title": "Always Alerting with For",
+      "tooltip": {
+        "msResolution": false,
+        "shared": true,
+        "sort": 0,
+        "value_type": "cumulative"
+      },
+      "type": "graph",
+      "xaxis": {
+        "buckets": null,
+        "mode": "time",
+        "name": null,
+        "show": true,
+        "values": []
+      },
+      "yaxes": [
+        {
+          "format": "short",
+          "label": "",
+          "logBase": 1,
+          "max": null,
+          "min": "0",
+          "show": true
+        },
+        {
+          "format": "short",
+          "label": "",
+          "logBase": 1,
+          "max": null,
+          "min": null,
+          "show": true
+        }
+      ],
+      "yaxis": {
+        "align": false,
+        "alignLevel": null
+      }
     }
     }
   ],
   ],
+  "revision": 2,
+  "schemaVersion": 16,
+  "style": "dark",
+  "tags": [
+    "grafana-test"
+  ],
+  "templating": {
+    "list": []
+  },
   "time": {
   "time": {
     "from": "now-6h",
     "from": "now-6h",
     "to": "now"
     "to": "now"
@@ -274,14 +705,8 @@
       "30d"
       "30d"
     ]
     ]
   },
   },
-  "templating": {
-    "list": []
-  },
-  "annotations": {
-    "list": []
-  },
-  "schemaVersion": 13,
-  "version": 4,
-  "links": [],
-  "gnetId": null
-}
+  "timezone": "browser",
+  "title": "Alerting with TestData",
+  "uid": "7MeksYbmk",
+  "version": 7
+}

+ 1 - 1
devenv/docker/ha_test/docker-compose.yaml

@@ -9,7 +9,7 @@ services:
       - /var/run/docker.sock:/tmp/docker.sock:ro
       - /var/run/docker.sock:/tmp/docker.sock:ro
 
 
   db:
   db:
-    image: mysql
+    image: mysql:5.6
     environment:
     environment:
       MYSQL_ROOT_PASSWORD: rootpass
       MYSQL_ROOT_PASSWORD: rootpass
       MYSQL_DATABASE: grafana
       MYSQL_DATABASE: grafana

+ 1 - 0
devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet

@@ -39,6 +39,7 @@ local alertDashboardTemplate = {
         "executionErrorState": "alerting",
         "executionErrorState": "alerting",
         "frequency": "10s",
         "frequency": "10s",
         "handler": 1,
         "handler": 1,
+        "for": "1m",
         "name": "bulk alerting",
         "name": "bulk alerting",
         "noDataState": "no_data",
         "noDataState": "no_data",
         "notifications": [
         "notifications": [

+ 1 - 1
docs/sources/administration/provisioning.md

@@ -230,4 +230,4 @@ By default Grafana will delete dashboards in the database if the file is removed
 > **Note.** Provisioning allows you to overwrite existing dashboards
 > **Note.** Provisioning allows you to overwrite existing dashboards
 > which leads to problems if you re-use settings that are supposed to be unique.
 > which leads to problems if you re-use settings that are supposed to be unique.
 > Be careful not to re-use the same `title` multiple times within a folder
 > Be careful not to re-use the same `title` multiple times within a folder
-> or `uid` within the same installation as this will cause weird behaviours.
+> or `uid` within the same installation as this will cause weird behaviors.

+ 5 - 1
docs/sources/alerting/notifications.md

@@ -55,6 +55,10 @@ Alert rule evaluation interval | Send reminders every | Reminder sent every (aft
 
 
 <div class="clearfix"></div>
 <div class="clearfix"></div>
 
 
+### Disable resolve message
+
+When checked, this option will disable resolve message [OK] that is sent when alerting state returns to false.
+
 ## Supported Notification Types
 ## Supported Notification Types
 
 
 Grafana ships with the following set of notification types:
 Grafana ships with the following set of notification types:
@@ -132,7 +136,7 @@ In DingTalk PC Client:
 
 
 2. Click "Robot Manage" item in the pop menu, there will be a new panel call "Robot Manage".
 2. Click "Robot Manage" item in the pop menu, there will be a new panel call "Robot Manage".
 
 
-3. In the  "Robot Manage" panel, select "customised: customised robot with Webhook".
+3. In the  "Robot Manage" panel, select "customized: customized robot with Webhook".
 
 
 4. In the next new panel named "robot detail", click "Add" button.
 4. In the next new panel named "robot detail", click "Add" button.
 
 

+ 16 - 3
docs/sources/alerting/rules.md

@@ -39,7 +39,7 @@ Currently alerting supports a limited form of high availability. Since v4.2.0 of
 
 
 ## Rule Config
 ## Rule Config
 
 
-{{< imgbox max-width="40%" img="/img/docs/v4/alerting_conditions.png" caption="Alerting Conditions" >}}
+
 
 
 Currently only the graph panel supports alert rules but this will be added to the **Singlestat** and **Table**
 Currently only the graph panel supports alert rules but this will be added to the **Singlestat** and **Table**
 panels as well in a future release.
 panels as well in a future release.
@@ -48,6 +48,19 @@ panels as well in a future release.
 
 
 Here you can specify the name of the alert rule and how often the scheduler should evaluate the alert rule.
 Here you can specify the name of the alert rule and how often the scheduler should evaluate the alert rule.
 
 
+### For
+
+> This setting is available in Grafana 5.4 and above.
+
+If an alert rule has a configured `For` and the query violates the configured threshold it will first go from `OK` to `Pending`. Going from `OK` to `Pending` Grafana will not send any notifications. Once the alert rule has been firing for more than `For` duration, it will change to `Alerting` and send alert notifications. 
+
+Typically, it's always a good idea to use this setting since it's often worse to get false positive than wait a few minutes before the alert notification triggers. Looking at the `Alert list` or `Alert list panels` you will be able to see alerts in pending state. 
+
+Below you can see an example timeline of an alert using the `For` setting. At ~16:04 the alert state changes to `Pending` and after 4 minutes it changes to `Alerting` which is when alert notifications are sent. Once the series falls back to normal the alert rule goes back to `OK`.
+{{< imgbox img="/img/docs/v54/alerting-for-dark-theme.png" caption="Alerting For" >}}
+
+{{< imgbox max-width="40%" img="/img/docs/v4/alerting_conditions.png" caption="Alerting Conditions" >}}
+
 ### Conditions
 ### Conditions
 
 
 Currently the only condition type that exists is a `Query` condition that allows you to
 Currently the only condition type that exists is a `Query` condition that allows you to
@@ -57,11 +70,11 @@ specify a query letter, time range and an aggregation function.
 ### Query condition example
 ### Query condition example
 
 
 ```sql
 ```sql
-avg() OF query(A, 5m, now) IS BELOW 14
+avg() OF query(A, 15m, now) IS BELOW 14
 ```
 ```
 
 
 - `avg()` Controls how the values for **each** series should be reduced to a value that can be compared against the threshold. Click on the function to change it to another aggregation function.
 - `avg()` Controls how the values for **each** series should be reduced to a value that can be compared against the threshold. Click on the function to change it to another aggregation function.
-- `query(A, 5m, now)`  The letter defines what query to execute from the **Metrics** tab. The second two parameters define the time range, `5m, now` means 5 minutes ago to now. You can also do `10m, now-2m` to define a time range that will be 10 minutes ago to 2 minutes ago. This is useful if you want to ignore the last 2 minutes of data.
+- `query(A, 15m, now)`  The letter defines what query to execute from the **Metrics** tab. The second two parameters define the time range, `15m, now` means 15 minutes ago to now. You can also do `10m, now-2m` to define a time range that will be 10 minutes ago to 2 minutes ago. This is useful if you want to ignore the last 2 minutes of data.
 - `IS BELOW 14`  Defines the type of threshold and the threshold value.  You can click on `IS BELOW` to change the type of threshold.
 - `IS BELOW 14`  Defines the type of threshold and the threshold value.  You can click on `IS BELOW` to change the type of threshold.
 
 
 The query used in an alert rule cannot contain any template variables. Currently we only support `AND` and `OR` operators between conditions and they are executed serially.
 The query used in an alert rule cannot contain any template variables. Currently we only support `AND` and `OR` operators between conditions and they are executed serially.

+ 3 - 2
docs/sources/auth/ldap.md

@@ -3,6 +3,7 @@ title = "LDAP Authentication"
 description = "Grafana LDAP Authentication Guide "
 description = "Grafana LDAP Authentication Guide "
 keywords = ["grafana", "configuration", "documentation", "ldap", "active directory"]
 keywords = ["grafana", "configuration", "documentation", "ldap", "active directory"]
 type = "docs"
 type = "docs"
+aliases = ["/installation/ldap/"]
 [menu.docs]
 [menu.docs]
 name = "LDAP"
 name = "LDAP"
 identifier = "ldap"
 identifier = "ldap"
@@ -162,9 +163,9 @@ org_role = "Viewer"
 Setting | Required | Description | Default
 Setting | Required | Description | Default
 ------------ | ------------ | ------------- | -------------
 ------------ | ------------ | ------------- | -------------
 `group_dn` | Yes | LDAP distinguished name (DN) of LDAP group. If you want to match all (or no LDAP groups) then you can use wildcard (`"*"`) |
 `group_dn` | Yes | LDAP distinguished name (DN) of LDAP group. If you want to match all (or no LDAP groups) then you can use wildcard (`"*"`) |
-`org_role` | Yes | Assign users of `group_dn` the organisation role `"Admin"`, `"Editor"` or `"Viewer"` |
+`org_role` | Yes | Assign users of `group_dn` the organization role `"Admin"`, `"Editor"` or `"Viewer"` |
 `org_id` | No | The Grafana organization database id. Setting this allows for multiple group_dn's to be assigned to the same `org_role` provided the `org_id` differs | `1` (default org id)
 `org_id` | No | The Grafana organization database id. Setting this allows for multiple group_dn's to be assigned to the same `org_role` provided the `org_id` differs | `1` (default org id)
-`grafana_admin` | No | When `true` makes user of `group_dn` Grafana server admin. A Grafana server admin has admin access over all organisations and users. Available in Grafana v5.3 and above | `false`
+`grafana_admin` | No | When `true` makes user of `group_dn` Grafana server admin. A Grafana server admin has admin access over all organizations and users. Available in Grafana v5.3 and above | `false`
 
 
 ### Nested/recursive group membership
 ### Nested/recursive group membership
 
 

+ 21 - 1
docs/sources/auth/overview.md

@@ -73,7 +73,18 @@ You can hide the Grafana login form using the below configuration settings.
 
 
 ```bash
 ```bash
 [auth]
 [auth]
-disable_login_form ⁼ true
+disable_login_form = true
+```
+
+### Automatic OAuth login
+
+Set to true to attempt login with OAuth automatically, skipping the login screen.
+This setting is ignored if multiple OAuth providers are configured.
+Defaults to `false`.
+
+```bash
+[auth]
+oauth_auto_login = true
 ```
 ```
 
 
 ### Hide sign-out menu
 ### Hide sign-out menu
@@ -84,3 +95,12 @@ Set to the option detailed below to true to hide sign-out menu link. Useful if y
 [auth]
 [auth]
 disable_signout_menu = true
 disable_signout_menu = true
 ```
 ```
+
+### URL redirect after signing out
+
+URL to redirect the user to after signing out from Grafana. This can for example be used to enable signout from oauth provider.
+
+```bash
+[auth]
+signout_redirect_url =
+```

+ 2 - 2
docs/sources/contribute/cla.md

@@ -1,6 +1,6 @@
 +++
 +++
-title = "Contributor Licence Agreement (CLA)"
-description = "Contributor Licence Agreement (CLA)"
+title = "Contributor License Agreement (CLA)"
+description = "Contributor License Agreement (CLA)"
 type = "docs"
 type = "docs"
 aliases = ["/project/cla", "docs/contributing/cla.html"]
 aliases = ["/project/cla", "docs/contributing/cla.html"]
 [menu.docs]
 [menu.docs]

+ 1 - 1
docs/sources/enterprise/index.md

@@ -31,7 +31,7 @@ Datasource permissions allow you to restrict query access to only specific Teams
 
 
 ### Premium Plugins
 ### Premium Plugins
 
 
-With a Grafana Enterprise licence you will get access to premium plugins, including:
+With a Grafana Enterprise license you will get access to premium plugins, including:
 
 
 * [Splunk](https://grafana.com/plugins/grafana-splunk-datasource)
 * [Splunk](https://grafana.com/plugins/grafana-splunk-datasource)
 * [AppDynamics](https://grafana.com/plugins/dlopes7-appdynamics-datasource)
 * [AppDynamics](https://grafana.com/plugins/dlopes7-appdynamics-datasource)

+ 3 - 3
docs/sources/features/datasources/mysql.md

@@ -133,9 +133,9 @@ Macro example | Description
 ------------ | -------------
 ------------ | -------------
 *$__time(dateColumn)* | Will be replaced by an expression to convert to a UNIX timestamp and rename the column to `time_sec`. For example, *UNIX_TIMESTAMP(dateColumn) as time_sec*
 *$__time(dateColumn)* | Will be replaced by an expression to convert to a UNIX timestamp and rename the column to `time_sec`. For example, *UNIX_TIMESTAMP(dateColumn) as time_sec*
 *$__timeEpoch(dateColumn)* | Will be replaced by an expression to convert to a UNIX timestamp and rename the column to `time_sec`. For example, *UNIX_TIMESTAMP(dateColumn) as time_sec*
 *$__timeEpoch(dateColumn)* | Will be replaced by an expression to convert to a UNIX timestamp and rename the column to `time_sec`. For example, *UNIX_TIMESTAMP(dateColumn) as time_sec*
-*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:06:17Z'*
-*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'*
-*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'*
+*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn BETWEEN FROM_UNIXTIME(1494410783) AND FROM_UNIXTIME(1494410983)*
+*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *FROM_UNIXTIME(1494410783)*
+*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *FROM_UNIXTIME(1494410983)*
 *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed),*
 *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed),*
 *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value.
 *$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value.
 *$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points.
 *$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points.

+ 14 - 4
docs/sources/features/datasources/stackdriver.md

@@ -158,9 +158,9 @@ Example Result: `compute.googleapis.com/instance/cpu/usage_time - server1-prod`
 
 
 It is also possible to resolve the name of the Monitored Resource Type. 
 It is also possible to resolve the name of the Monitored Resource Type. 
 
 
-| Alias Pattern Format     | Description                                     | Example Result   |
-| ------------------------ | ------------------------------------------------| ---------------- |
-| `{{resource.type}}`      | returns the name of the monitored resource type | `gce_instance`     |
+| Alias Pattern Format | Description                                     | Example Result |
+| -------------------- | ----------------------------------------------- | -------------- |
+| `{{resource.type}}`  | returns the name of the monitored resource type | `gce_instance` |
 
 
 Example Alias By: `{{resource.type}} - {{metric.type}}`
 Example Alias By: `{{resource.type}} - {{metric.type}}`
 
 
@@ -177,7 +177,17 @@ types of template variables.
 
 
 ### Query Variable
 ### Query Variable
 
 
-Writing variable queries is not supported yet.
+Variable of the type *Query* allows you to query Stackdriver for various types of data. The Stackdriver data source plugin provides the following `Query Types`.
+
+| Name                | Description                                                                                       |
+| ------------------- | ------------------------------------------------------------------------------------------------- |
+| *Metric Types*      | Returns a list of metric type names that are available for the specified service.                 |
+| *Labels Keys*       | Returns a list of keys for `metric label` and `resource label` in the specified metric.           |
+| *Labels Values*     | Returns a list of values for the label in the specified metric.                                   |
+| *Resource Types*    | Returns a list of resource types for the the specified metric.                                    |
+| *Aggregations*      | Returns a list of aggregations (cross series reducers) for the the specified metric.              |
+| *Aligners*          | Returns a list of aligners (per series aligners) for the the specified metric.                    |
+| *Alignment periods* | Returns a list of all alignment periods that are available in Stackdriver query editor in Grafana |
 
 
 ### Using variables in queries
 ### Using variables in queries
 
 

+ 1 - 1
docs/sources/guides/whats-new-in-v4.md

@@ -134,7 +134,7 @@ continue work on a `build mode` for a future release.
 The new row menu automatically slides out when you mouse over the edge of the row. You no longer need
 The new row menu automatically slides out when you mouse over the edge of the row. You no longer need
 to hover over the small green icon and then click it to expand the row menu.
 to hover over the small green icon and then click it to expand the row menu.
 
 
-There are some minor improvements to drag and drop behaviour. Now when dragging a panel from one row
+There are some minor improvements to drag and drop behavior. Now when dragging a panel from one row
 to another you will insert the panel and Grafana will automatically make room for it.
 to another you will insert the panel and Grafana will automatically make room for it.
 When you drag a panel within a row you will simply reorder the panels.
 When you drag a panel within a row you will simply reorder the panels.
 
 

+ 83 - 0
docs/sources/guides/whats-new-in-v5-4.md

@@ -0,0 +1,83 @@
++++
+title = "What's New in Grafana v5.4"
+description = "Feature & improvement highlights for Grafana v5.4"
+keywords = ["grafana", "new", "documentation", "5.4"]
+type = "docs"
+[menu.docs]
+name = "Version 5.4"
+identifier = "v5.4"
+parent = "whatsnew"
+weight = -10
++++
+
+# What's New in Grafana v5.4
+
+Grafana v5.4 brings new features, many enhancements and bug fixes. This article will detail the major new features and enhancements.
+
+- [Alerting]({{< relref "#alerting" >}}) Limit false positives with the new `For` setting
+- [Google Stackdriver]({{< relref "#google-stackdriver" >}}) Now with support for templating queries
+- [MySQL]({{< relref "#mysql-query-builder" >}}) gets a new query builder!
+- [Graph Panel]({{< relref "#graph-panel-enhancements" >}}) Highlight time regions and more
+- [Team Preferences]({{< relref "#team-preferences" >}}) Give your teams their own home dashboard
+
+## Alerting
+
+{{< docs-imagebox img="/img/docs/v54/alerting-for-dark-theme.png" max-width="600px" class="docs-image--right" >}}
+
+Grafana v5.4 ships with a new alert rule setting named `For` which is great for removing false positives. If an alert rule has a configured `For` and the query violates the configured threshold it will first go from `OK` to `Pending`. Going from `OK` to `Pending` Grafana will not send any notifications. Once the alert rule has been firing for more than `For` duration, it will change to `Alerting` and send alert notifications. Typically, it's always a good idea to use this setting since it's often worse to get false positive than wait a few minutes before the alert notification triggers.
+
+In the screenshot you can see an example timeline of an alert using the `For` setting. At ~16:04 the alert state changes to `Pending` and after 4 minutes it changes to `Alerting` which is when alert notifications are sent. Once the series falls back to normal the alert rule goes back to `OK`. [Learn more](/alerting/rules/#for).
+
+Additionally, there's now support for disable the sending of `OK` alert notifications. [Learn more](/alerting/notifications/#disable-resolve-message).
+
+<div class="clearfix"></div>
+
+## Google Stackdriver
+
+{{< docs-imagebox img="/img/docs/v54/stackdriver_template_query.png" max-width="600px" class="docs-image--right" >}}
+
+Grafana v5.3 included built-in support for [Google Stackdriver](https://cloud.google.com/stackdriver/) which enables you to visualize your Stackdriver metrics in Grafana.
+One important feature missing was support for templating queries. This is now included together with a brand new templating query editor for Stackdriver.
+
+The Stackdriver templating query editor lets you choose from a set of different Query Types. This will in turn reveal additional drop downs to help you
+find, filter and select the templating values you're interested in, see screenshot for details. The templating query editor also supports chaining multiple variables
+making it easy to define variables that's dependent on other variables.
+
+Stackdriver is the first datasource which has support for a custom templating query editor. But starting from Grafana v5.4 it's now possible for all datasources, including plugin datasources, to
+create their very own templating query editor.
+
+Additionally, if Grafana is running on a Google Compute Engine (GCE) virtual machine, it is now possible for Grafana to automatically retrieve default credentials from the metadata server.
+This has the advantage of not needing to generate a private key file for the service account and also not having to upload the file to Grafana. [Learn more](/features/datasources/stackdriver/#using-gce-default-service-account).
+
+Please read [Using Google Stackdriver in Grafana](/features/datasources/stackdriver/) for more detailed information on how to get started and use it.
+
+<div class="clearfix"></div>
+
+## MySQL Query Builder
+
+Grafana v5.4 comes with a new graphical query builder for MySQL. This brings MySQL integration more in line with some of the other datasources and makes it easier for both advanced users and beginners to work with timeseries in MySQL. Learn more about it in the [documentation](/features/datasources/mysql/#query-editor).
+
+{{< docs-imagebox img="/img/docs/v54/mysql_query_still.png" animated-gif="/img/docs/v54/mysql_query.gif" >}}
+
+## Graph Panel Enhancements
+
+Grafana v5.4 adds support for highlighting weekdays and/or certain timespans in the graph panel. This should make it easier to compare for example weekends, business hours and/or off work hours.
+
+{{< docs-imagebox img="/img/docs/v54/graph_time_regions.png" max-width= "800px" >}}
+
+Additionally, when rendering series as lines in the graph panel, should there be only one data point available for one series so that a connecting line cannot be established, a point will
+automatically be rendered for that data point. This should make it easier to understand what's going on when only receiving a single data point.
+
+{{< docs-imagebox img="/img/docs/v54/graph_dot_single_point.png" max-width= "800px" >}}
+
+## Team Preferences
+
+Grafana v5.4 adds support for customizing home dashboard, timezone and theme for teams, in addition to the existing customization on Organization and user Profile level.
+
+1. Specifying a preference on User Profile level will override preference on Team and/or Organization level
+2. Specifying a preference on Team level will override preference on Organization level.
+
+## Changelog
+
+Checkout the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file for a complete list
+of new features, changes, and bug fixes.

+ 4 - 1
docs/sources/http_api/alerting.md

@@ -160,11 +160,14 @@ Content-Type: application/json
 
 
 `POST /api/admin/pause-all-alerts`
 `POST /api/admin/pause-all-alerts`
 
 
+Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
+
+**Example Request**:
+
 ```http
 ```http
 POST /api/admin/pause-all-alerts HTTP/1.1
 POST /api/admin/pause-all-alerts HTTP/1.1
 Accept: application/json
 Accept: application/json
 Content-Type: application/json
 Content-Type: application/json
-Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 
 
 {
 {
   "paused": true
   "paused": true

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

@@ -26,7 +26,7 @@ dashboards, creating users and updating data sources.
 * [Folder Permissions API]({{< relref "http_api/folder_permissions.md" >}})
 * [Folder Permissions API]({{< relref "http_api/folder_permissions.md" >}})
 * [Folder/dashboard search API]({{< relref "/http_api/folder_dashboard_search.md" >}})
 * [Folder/dashboard search API]({{< relref "/http_api/folder_dashboard_search.md" >}})
 * [Data Source API]({{< relref "http_api/data_source.md" >}})
 * [Data Source API]({{< relref "http_api/data_source.md" >}})
-* [Organisation API]({{< relref "http_api/org.md" >}})
+* [Organization API]({{< relref "http_api/org.md" >}})
 * [Snapshot API]({{< relref "http_api/snapshot.md" >}})
 * [Snapshot API]({{< relref "http_api/snapshot.md" >}})
 * [Annotations API]({{< relref "http_api/annotations.md" >}})
 * [Annotations API]({{< relref "http_api/annotations.md" >}})
 * [Alerting API]({{< relref "http_api/alerting.md" >}})
 * [Alerting API]({{< relref "http_api/alerting.md" >}})

+ 39 - 39
docs/sources/http_api/org.md

@@ -1,24 +1,24 @@
 +++
 +++
-title = "Organisation HTTP API "
-description = "Grafana Organisation HTTP API"
-keywords = ["grafana", "http", "documentation", "api", "organisation"]
-aliases = ["/http_api/organisation/"]
+title = "Organization HTTP API "
+description = "Grafana Organization HTTP API"
+keywords = ["grafana", "http", "documentation", "api", "organization"]
+aliases = ["/http_api/organization/"]
 type = "docs"
 type = "docs"
 [menu.docs]
 [menu.docs]
-name = "Organisation"
+name = "Organization"
 parent = "http_api"
 parent = "http_api"
 +++
 +++
 
 
 
 
-# Organisation API
+# Organization API
 
 
-The Organisation HTTP API is divided in two resources, `/api/org` (current organisation)
-and `/api/orgs` (admin organisations). One big difference between these are that
-the admin of all organisations API only works with basic authentication, see [Admin Organisations API](#admin-organisations-api) for more information.
+The Organization HTTP API is divided in two resources, `/api/org` (current organization)
+and `/api/orgs` (admin organizations). One big difference between these are that
+the admin of all organizations API only works with basic authentication, see [Admin Organizations API](#admin-organizations-api) for more information.
 
 
-## Current Organisation API
+## Current Organization API
 
 
-### Get current Organisation
+### Get current Organization
 
 
 `GET /api/org/`
 `GET /api/org/`
 
 
@@ -43,7 +43,7 @@ Content-Type: application/json
 }
 }
 ```
 ```
 
 
-### Get all users within the current organisation
+### Get all users within the current organization
 
 
 `GET /api/org/users`
 `GET /api/org/users`
 
 
@@ -99,7 +99,7 @@ Content-Type: application/json
 {"message":"Organization user updated"}
 {"message":"Organization user updated"}
 ```
 ```
 
 
-### Delete user in current organisation
+### Delete user in current organization
 
 
 `DELETE /api/org/users/:userId`
 `DELETE /api/org/users/:userId`
 
 
@@ -121,7 +121,7 @@ Content-Type: application/json
 {"message":"User removed from organization"}
 {"message":"User removed from organization"}
 ```
 ```
 
 
-### Update current Organisation
+### Update current Organization
 
 
 `PUT /api/org`
 `PUT /api/org`
 
 
@@ -147,11 +147,11 @@ Content-Type: application/json
 {"message":"Organization updated"}
 {"message":"Organization updated"}
 ```
 ```
 
 
-### Add a new user to the current organisation
+### Add a new user to the current organization
 
 
 `POST /api/org/users`
 `POST /api/org/users`
 
 
-Adds a global user to the current organisation.
+Adds a global user to the current organization.
 
 
 **Example Request**:
 **Example Request**:
 
 
@@ -176,19 +176,19 @@ Content-Type: application/json
 {"message":"User added to organization"}
 {"message":"User added to organization"}
 ```
 ```
 
 
-## Admin Organisations API
+## Admin Organizations API
 
 
-The Admin Organisations HTTP API does not currently work with an API Token. API Tokens are currently
+The Admin Organizations HTTP API does not currently work with an API Token. API Tokens are currently
 only linked to an organization and an organization role. They cannot be given the permission of server
 only linked to an organization and an organization role. They cannot be given the permission of server
 admin, only users can be given that permission. So in order to use these API calls you will have to
 admin, only users can be given that permission. So in order to use these API calls you will have to
 use Basic Auth and the Grafana user must have the Grafana Admin permission (The default admin user
 use Basic Auth and the Grafana user must have the Grafana Admin permission (The default admin user
 is called `admin` and has permission to use this API).
 is called `admin` and has permission to use this API).
 
 
-### Get Organisation by Id
+### Get Organization by Id
 
 
 `GET /api/orgs/:orgId`
 `GET /api/orgs/:orgId`
 
 
-Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
+Only works with Basic Authentication (username and password), see [introduction](#admin-organizations-api).
 
 
 **Example Request**:
 **Example Request**:
 
 
@@ -217,11 +217,11 @@ Content-Type: application/json
   }
   }
 }
 }
 ```
 ```
-### Get Organisation by Name
+### Get Organization by Name
 
 
 `GET /api/orgs/name/:orgName`
 `GET /api/orgs/name/:orgName`
 
 
-Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
+Only works with Basic Authentication (username and password), see [introduction](#admin-organizations-api).
 
 
 **Example Request**:
 **Example Request**:
 
 
@@ -251,11 +251,11 @@ Content-Type: application/json
 }
 }
 ```
 ```
 
 
-### Create Organisation
+### Create Organization
 
 
 `POST /api/orgs`
 `POST /api/orgs`
 
 
-Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
+Only works with Basic Authentication (username and password), see [introduction](#admin-organizations-api).
 
 
 **Example Request**:
 **Example Request**:
 
 
@@ -284,11 +284,11 @@ Content-Type: application/json
 }
 }
 ```
 ```
 
 
-### Search all Organisations
+### Search all Organizations
 
 
 `GET /api/orgs`
 `GET /api/orgs`
 
 
-Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
+Only works with Basic Authentication (username and password), see [introduction](#admin-organizations-api).
 
 
 **Example Request**:
 **Example Request**:
 
 
@@ -314,12 +314,12 @@ Content-Type: application/json
 ]
 ]
 ```
 ```
 
 
-### Update Organisation
+### Update Organization
 
 
 `PUT /api/orgs/:orgId`
 `PUT /api/orgs/:orgId`
 
 
-Update Organisation, fields *Address 1*, *Address 2*, *City* are not implemented yet.
-Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
+Update Organization, fields *Address 1*, *Address 2*, *City* are not implemented yet.
+Only works with Basic Authentication (username and password), see [introduction](#admin-organizations-api).
 
 
 **Example Request**:
 **Example Request**:
 
 
@@ -342,11 +342,11 @@ Content-Type: application/json
 {"message":"Organization updated"}
 {"message":"Organization updated"}
 ```
 ```
 
 
-### Delete Organisation
+### Delete Organization
 
 
 `DELETE /api/orgs/:orgId`
 `DELETE /api/orgs/:orgId`
 
 
-Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
+Only works with Basic Authentication (username and password), see [introduction](#admin-organizations-api).
 
 
 **Example Request**:
 **Example Request**:
 
 
@@ -364,11 +364,11 @@ Content-Type: application/json
 {"message":"Organization deleted"}
 {"message":"Organization deleted"}
 ```
 ```
 
 
-### Get Users in Organisation
+### Get Users in Organization
 
 
 `GET /api/orgs/:orgId/users`
 `GET /api/orgs/:orgId/users`
 
 
-Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
+Only works with Basic Authentication (username and password), see [introduction](#admin-organizations-api).
 
 
 **Example Request**:
 **Example Request**:
 
 
@@ -397,11 +397,11 @@ Content-Type: application/json
 ]
 ]
 ```
 ```
 
 
-### Add User in Organisation
+### Add User in Organization
 
 
 `POST /api/orgs/:orgId/users`
 `POST /api/orgs/:orgId/users`
 
 
-Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
+Only works with Basic Authentication (username and password), see [introduction](#admin-organizations-api).
 
 
 **Example Request**:
 **Example Request**:
 
 
@@ -425,11 +425,11 @@ Content-Type: application/json
 {"message":"User added to organization"}
 {"message":"User added to organization"}
 ```
 ```
 
 
-### Update Users in Organisation
+### Update Users in Organization
 
 
 `PATCH /api/orgs/:orgId/users/:userId`
 `PATCH /api/orgs/:orgId/users/:userId`
 
 
-Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
+Only works with Basic Authentication (username and password), see [introduction](#admin-organizations-api).
 
 
 **Example Request**:
 **Example Request**:
 
 
@@ -452,11 +452,11 @@ Content-Type: application/json
 {"message":"Organization user updated"}
 {"message":"Organization user updated"}
 ```
 ```
 
 
-### Delete User in Organisation
+### Delete User in Organization
 
 
 `DELETE /api/orgs/:orgId/users/:userId`
 `DELETE /api/orgs/:orgId/users/:userId`
 
 
-Only works with Basic Authentication (username and password), see [introduction](#admin-organisations-api).
+Only works with Basic Authentication (username and password), see [introduction](#admin-organizations-api).
 
 
 **Example Request**:
 **Example Request**:
 
 

+ 37 - 3
docs/sources/http_api/user.md

@@ -196,7 +196,7 @@ Content-Type: application/json
 {"message":"User updated"}
 {"message":"User updated"}
 ```
 ```
 
 
-## Get Organisations for user
+## Get Organizations for user
 
 
 `GET /api/users/:id/orgs`
 `GET /api/users/:id/orgs`
 
 
@@ -226,6 +226,40 @@ Content-Type: application/json
 ]
 ]
 ```
 ```
 
 
+## Get Teams for user
+
+`GET /api/users/:id/teams`
+
+**Example Request**:
+
+```http
+GET /api/users/1/teams HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Basic YWRtaW46YWRtaW4=
+```
+
+Requires basic authentication and that the authenticated user is a Grafana Admin.
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+[
+  {
+    "id":1,
+    "orgId":1,
+    "name":"team1",
+    "email":"",
+    "avatarUrl":"/avatar/3fcfe295eae3bcb67a49349377428a66",
+    "memberCount":1
+  }
+]
+```
+
+
 ## User
 ## User
 
 
 ## Actual User
 ## Actual User
@@ -333,11 +367,11 @@ Content-Type: application/json
 {"message":"Active organization changed"}
 {"message":"Active organization changed"}
 ```
 ```
 
 
-## Organisations of the actual User
+## Organizations of the actual User
 
 
 `GET /api/user/orgs`
 `GET /api/user/orgs`
 
 
-Return a list of all organisations of the current user.
+Return a list of all organizations of the current user.
 
 
 **Example Request**:
 **Example Request**:
 
 

+ 3 - 3
docs/sources/index.md

@@ -60,9 +60,9 @@ aliases = ["v1.1", "guides/reference/admin"]
         <h4>Provisioning</h4>
         <h4>Provisioning</h4>
         <p>A guide to help you automate your Grafana setup & configuration.</p>
         <p>A guide to help you automate your Grafana setup & configuration.</p>
     </a>
     </a>
-    <a href="{{< relref "guides/whats-new-in-v5-3.md" >}}" class="nav-cards__item nav-cards__item--guide">
-        <h4>What's new in v5.3</h4>
-        <p>Article on all the new cool features and enhancements in v5.3</p>
+    <a href="{{< relref "guides/whats-new-in-v5-4.md" >}}" class="nav-cards__item nav-cards__item--guide">
+        <h4>What's new in v5.4</h4>
+        <p>Article on all the new cool features and enhancements in v5.4</p>
     </a>
     </a>
     <a href="{{< relref "tutorials/screencasts.md" >}}" class="nav-cards__item nav-cards__item--guide">
     <a href="{{< relref "tutorials/screencasts.md" >}}" class="nav-cards__item nav-cards__item--guide">
         <h4>Screencasts</h4>
         <h4>Screencasts</h4>

+ 6 - 0
docs/sources/installation/configuration.md

@@ -454,6 +454,12 @@ Ex `filters = sqlstore:debug`
 ### enabled
 ### enabled
 Enable metrics reporting. defaults true. Available via HTTP API `/metrics`.
 Enable metrics reporting. defaults true. Available via HTTP API `/metrics`.
 
 
+### basic_auth_username
+If set configures the username to use for basic authentication on the metrics endpoint.
+
+### basic_auth_password
+If set configures the password to use for basic authentication on the metrics endpoint.
+
 ### interval_seconds
 ### interval_seconds
 
 
 Flush/Write interval when sending metrics to external TSDB. Defaults to 10s.
 Flush/Write interval when sending metrics to external TSDB. Defaults to 10s.

+ 2 - 0
docs/sources/reference/export_import.md

@@ -107,3 +107,5 @@ it as usual and then update the data source option in the metrics tab so that th
 data source. Another alternative is to open the json file in a a text editor and update the data source properties
 data source. Another alternative is to open the json file in a a text editor and update the data source properties
 to value that matches a name of your data source.
 to value that matches a name of your data source.
 
 
+## Note
+In Grafana v5.3.4+ the export modal has new checkbox for sharing for external use (other instances). If the checkbox is not checked then the `__inputs` section will not be included in the exported JSON file.

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

@@ -25,7 +25,7 @@ the value, using the dropdown at the top of the dashboard, your panel's metric q
 
 
 Panel titles and metric queries can refer to variables using two different syntaxes:
 Panel titles and metric queries can refer to variables using two different syntaxes:
 
 
-- `$<varname>`  Example: apps.frontend.$server.requests.count
+- `$varname`  Example: apps.frontend.$server.requests.count
 - `[[varname]]` Example: apps.frontend.[[server]].requests.count
 - `[[varname]]` Example: apps.frontend.[[server]].requests.count
 
 
 Why two ways? The first syntax is easier to read and write but does not allow you to use a variable in the middle of word. Use
 Why two ways? The first syntax is easier to read and write but does not allow you to use a variable in the middle of word. Use

+ 2 - 1
docs/versions.json

@@ -1,5 +1,6 @@
 [
 [
-  { "version": "v5.3", "path": "/", "archived": false, "current": true },
+  { "version": "v5.4", "path": "/", "archived": false, "current": 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 },
   { "version": "v5.0", "path": "/v5.0", "archived": true },
   { "version": "v5.0", "path": "/v5.0", "archived": true },

+ 2 - 2
latest.json

@@ -1,4 +1,4 @@
 {
 {
-  "stable": "5.3.4",
-  "testing": "5.3.4"
+  "stable": "5.4.0",
+  "testing": "5.4.0"
 }
 }

+ 16 - 20
package.json

@@ -4,12 +4,18 @@
     "company": "Grafana Labs"
     "company": "Grafana Labs"
   },
   },
   "name": "grafana",
   "name": "grafana",
-  "version": "5.4.0-pre1",
+  "version": "5.5.0-pre1",
   "repository": {
   "repository": {
     "type": "git",
     "type": "git",
     "url": "http://github.com/grafana/grafana.git"
     "url": "http://github.com/grafana/grafana.git"
   },
   },
   "devDependencies": {
   "devDependencies": {
+    "@babel/core": "^7.1.2",
+    "@rtsao/plugin-proposal-class-properties": "^7.0.1-patch.1",
+    "@babel/plugin-syntax-dynamic-import": "^7.0.0",
+    "@babel/preset-env": "^7.1.0",
+    "@babel/preset-react": "^7.0.0",
+    "@babel/preset-typescript": "^7.1.0",
     "@types/d3": "^4.10.1",
     "@types/d3": "^4.10.1",
     "@types/enzyme": "^3.1.13",
     "@types/enzyme": "^3.1.13",
     "@types/jest": "^23.3.2",
     "@types/jest": "^23.3.2",
@@ -21,10 +27,10 @@
     "angular-mocks": "1.6.6",
     "angular-mocks": "1.6.6",
     "autoprefixer": "^6.4.0",
     "autoprefixer": "^6.4.0",
     "axios": "^0.17.1",
     "axios": "^0.17.1",
-    "babel-core": "^6.26.0",
-    "babel-loader": "^7.1.4",
-    "babel-plugin-syntax-dynamic-import": "^6.18.0",
-    "babel-preset-es2015": "^6.24.1",
+    "babel-core": "^7.0.0-bridge",
+    "babel-jest": "^23.6.0",
+    "babel-loader": "^8.0.4",
+    "babel-plugin-angularjs-annotate": "^0.9.0",
     "clean-webpack-plugin": "^0.1.19",
     "clean-webpack-plugin": "^0.1.19",
     "css-loader": "^0.28.7",
     "css-loader": "^0.28.7",
     "enzyme": "^3.6.0",
     "enzyme": "^3.6.0",
@@ -108,18 +114,9 @@
     "precommit": "lint-staged && grunt precommit"
     "precommit": "lint-staged && grunt precommit"
   },
   },
   "lint-staged": {
   "lint-staged": {
-    "*.{ts,tsx}": [
-      "prettier --write",
-      "git add"
-    ],
-    "*.scss": [
-      "prettier --write",
-      "git add"
-    ],
-    "*pkg/**/*.go": [
-      "gofmt -w -s",
-      "git add"
-    ]
+    "*.{ts,tsx}": ["prettier --write", "git add"],
+    "*.scss": ["prettier --write", "git add"],
+    "*pkg/**/*.go": ["gofmt -w -s", "git add"]
   },
   },
   "prettier": {
   "prettier": {
     "trailingComma": "es5",
     "trailingComma": "es5",
@@ -128,13 +125,12 @@
   },
   },
   "license": "Apache-2.0",
   "license": "Apache-2.0",
   "dependencies": {
   "dependencies": {
+    "@babel/polyfill": "^7.0.0",
     "angular": "1.6.6",
     "angular": "1.6.6",
     "angular-bindonce": "0.3.1",
     "angular-bindonce": "0.3.1",
     "angular-native-dragdrop": "1.2.2",
     "angular-native-dragdrop": "1.2.2",
     "angular-route": "1.6.6",
     "angular-route": "1.6.6",
     "angular-sanitize": "1.6.6",
     "angular-sanitize": "1.6.6",
-    "babel-jest": "^23.6.0",
-    "babel-polyfill": "^6.26.0",
     "baron": "^3.0.3",
     "baron": "^3.0.3",
     "brace": "^0.10.0",
     "brace": "^0.10.0",
     "classnames": "^2.2.5",
     "classnames": "^2.2.5",
@@ -156,7 +152,7 @@
     "react-custom-scrollbars": "^4.2.1",
     "react-custom-scrollbars": "^4.2.1",
     "react-dom": "^16.5.0",
     "react-dom": "^16.5.0",
     "react-grid-layout": "0.16.6",
     "react-grid-layout": "0.16.6",
-    "react-highlight-words": "^0.10.0",
+    "react-highlight-words": "0.11.0",
     "react-popper": "^0.7.5",
     "react-popper": "^0.7.5",
     "react-redux": "^5.0.7",
     "react-redux": "^5.0.7",
     "react-select": "2.1.0",
     "react-select": "2.1.0",

+ 1 - 1
packaging/deb/init.d/grafana-server

@@ -56,7 +56,7 @@ if [ -f "$DEFAULT" ]; then
 	. "$DEFAULT"
 	. "$DEFAULT"
 fi
 fi
 
 
-DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
+DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} --packaging=deb cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
 
 
 function checkUser() {
 function checkUser() {
   if [ `id -u` -ne 0 ]; then
   if [ `id -u` -ne 0 ]; then

+ 1 - 0
packaging/deb/systemd/grafana-server.service

@@ -17,6 +17,7 @@ RuntimeDirectoryMode=0750
 ExecStart=/usr/sbin/grafana-server                                                  \
 ExecStart=/usr/sbin/grafana-server                                                  \
                             --config=${CONF_FILE}                                   \
                             --config=${CONF_FILE}                                   \
                             --pidfile=${PID_FILE_DIR}/grafana-server.pid            \
                             --pidfile=${PID_FILE_DIR}/grafana-server.pid            \
+                            --packaging=deb                                         \
                             cfg:default.paths.logs=${LOG_DIR}                       \
                             cfg:default.paths.logs=${LOG_DIR}                       \
                             cfg:default.paths.data=${DATA_DIR}                      \
                             cfg:default.paths.data=${DATA_DIR}                      \
                             cfg:default.paths.plugins=${PLUGINS_DIR}                \
                             cfg:default.paths.plugins=${PLUGINS_DIR}                \

+ 2 - 1
packaging/docker/Dockerfile

@@ -25,7 +25,8 @@ ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bi
 
 
 WORKDIR $GF_PATHS_HOME
 WORKDIR $GF_PATHS_HOME
 
 
-RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates curl && \
+RUN apt-get update && apt-get -y upgrade && \
+    apt-get install -qq -y libfontconfig ca-certificates curl && \
     apt-get autoremove -y && \
     apt-get autoremove -y && \
     rm -rf /var/lib/apt/lists/*
     rm -rf /var/lib/apt/lists/*
 
 

+ 14 - 1
packaging/docker/build-enterprise.sh

@@ -1,12 +1,25 @@
 #!/bin/sh
 #!/bin/sh
 set -e
 set -e
 
 
-_grafana_tag=$1
+_raw_grafana_tag=$1
 _docker_repo=${2:-grafana/grafana-enterprise}
 _docker_repo=${2:-grafana/grafana-enterprise}
 
 
+if echo "$_raw_grafana_tag" | grep -q "^v"; then
+  _grafana_tag=$(echo "${_raw_grafana_tag}" | cut -d "v" -f 2)
+else
+  _grafana_tag="${_raw_grafana_tag}"
+fi
+
+echo "Building and deploying ${_docker_repo}:${_grafana_tag}"
+
 docker build \
 docker build \
   --tag "${_docker_repo}:${_grafana_tag}"\
   --tag "${_docker_repo}:${_grafana_tag}"\
   --no-cache=true \
   --no-cache=true \
   .
   .
 
 
 docker push "${_docker_repo}:${_grafana_tag}"
 docker push "${_docker_repo}:${_grafana_tag}"
+
+if echo "$_raw_grafana_tag" | grep -q "^v" && echo "$_raw_grafana_tag" | grep -qv "beta"; then
+  docker tag "${_docker_repo}:${_grafana_tag}" "${_docker_repo}:latest"
+  docker push "${_docker_repo}:latest"
+fi

+ 1 - 0
packaging/docker/run.sh

@@ -80,6 +80,7 @@ fi
 exec grafana-server                                         \
 exec grafana-server                                         \
   --homepath="$GF_PATHS_HOME"                               \
   --homepath="$GF_PATHS_HOME"                               \
   --config="$GF_PATHS_CONFIG"                               \
   --config="$GF_PATHS_CONFIG"                               \
+  --packaging=docker                                        \
   "$@"                                                      \
   "$@"                                                      \
   cfg:default.log.mode="console"                            \
   cfg:default.log.mode="console"                            \
   cfg:default.paths.data="$GF_PATHS_DATA"                   \
   cfg:default.paths.data="$GF_PATHS_DATA"                   \

+ 1 - 1
packaging/rpm/init.d/grafana-server

@@ -60,7 +60,7 @@ fi
 # overwrite settings from default file
 # overwrite settings from default file
 [ -e /etc/sysconfig/$NAME ] && . /etc/sysconfig/$NAME
 [ -e /etc/sysconfig/$NAME ] && . /etc/sysconfig/$NAME
 
 
-DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
+DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} --packaging=rpm cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
 
 
 function isRunning() {
 function isRunning() {
   status -p $PID_FILE $NAME > /dev/null 2>&1
   status -p $PID_FILE $NAME > /dev/null 2>&1

+ 1 - 0
packaging/rpm/systemd/grafana-server.service

@@ -17,6 +17,7 @@ RuntimeDirectoryMode=0750
 ExecStart=/usr/sbin/grafana-server                                                  \
 ExecStart=/usr/sbin/grafana-server                                                  \
                             --config=${CONF_FILE}                                   \
                             --config=${CONF_FILE}                                   \
                             --pidfile=${PID_FILE_DIR}/grafana-server.pid            \
                             --pidfile=${PID_FILE_DIR}/grafana-server.pid            \
+                            --packaging=rpm                                         \
                             cfg:default.paths.logs=${LOG_DIR}                       \
                             cfg:default.paths.logs=${LOG_DIR}                       \
                             cfg:default.paths.data=${DATA_DIR}                      \
                             cfg:default.paths.data=${DATA_DIR}                      \
                             cfg:default.paths.plugins=${PLUGINS_DIR}                \
                             cfg:default.paths.plugins=${PLUGINS_DIR}                \

+ 6 - 0
pkg/api/admin_users.go

@@ -76,6 +76,7 @@ func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordF
 	c.JsonOK("User password updated")
 	c.JsonOK("User password updated")
 }
 }
 
 
+// PUT /api/admin/users/:id/permissions
 func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermissionsForm) {
 func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermissionsForm) {
 	userID := c.ParamsInt64(":id")
 	userID := c.ParamsInt64(":id")
 
 
@@ -85,6 +86,11 @@ func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermis
 	}
 	}
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
+		if err == m.ErrLastGrafanaAdmin {
+			c.JsonApiErr(400, m.ErrLastGrafanaAdmin.Error(), nil)
+			return
+		}
+
 		c.JsonApiErr(500, "Failed to update user permissions", err)
 		c.JsonApiErr(500, "Failed to update user permissions", err)
 		return
 		return
 	}
 	}

+ 50 - 0
pkg/api/admin_users_test.go

@@ -0,0 +1,50 @@
+package api
+
+import (
+	"testing"
+
+	"github.com/grafana/grafana/pkg/api/dtos"
+	"github.com/grafana/grafana/pkg/bus"
+	m "github.com/grafana/grafana/pkg/models"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestAdminApiEndpoint(t *testing.T) {
+	role := m.ROLE_ADMIN
+	Convey("Given a server admin attempts to remove themself as an admin", t, func() {
+
+		updateCmd := dtos.AdminUpdateUserPermissionsForm{
+			IsGrafanaAdmin: false,
+		}
+
+		bus.AddHandler("test", func(cmd *m.UpdateUserPermissionsCommand) error {
+			return m.ErrLastGrafanaAdmin
+		})
+
+		putAdminScenario("When calling PUT on", "/api/admin/users/1/permissions", "/api/admin/users/:id/permissions", role, updateCmd, func(sc *scenarioContext) {
+			sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
+			So(sc.resp.Code, ShouldEqual, 400)
+		})
+	})
+}
+
+func putAdminScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.AdminUpdateUserPermissionsForm, fn scenarioFunc) {
+	Convey(desc+" "+url, func() {
+		defer bus.ClearBusHandlers()
+
+		sc := setupScenarioContext(url)
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) {
+			sc.context = c
+			sc.context.UserId = TestUserID
+			sc.context.OrgId = TestOrgID
+			sc.context.OrgRole = role
+
+			AdminUpdateUserPermissions(c, cmd)
+		})
+
+		sc.m.Put(routePattern, sc.defaultHandler)
+
+		fn(sc)
+	})
+}

+ 1 - 1
pkg/api/alerting.go

@@ -295,7 +295,7 @@ func PauseAlert(c *m.ReqContext, dto dtos.PauseAlertCommand) Response {
 		return Error(500, "", err)
 		return Error(500, "", err)
 	}
 	}
 
 
-	var response m.AlertStateType = m.AlertStatePending
+	var response m.AlertStateType = m.AlertStateUnknown
 	pausedState := "un-paused"
 	pausedState := "un-paused"
 	if cmd.Paused {
 	if cmd.Paused {
 		response = m.AlertStatePaused
 		response = m.AlertStatePaused

+ 1 - 0
pkg/api/api.go

@@ -140,6 +140,7 @@ func (hs *HTTPServer) registerRoutes() {
 			usersRoute.Get("/", Wrap(SearchUsers))
 			usersRoute.Get("/", Wrap(SearchUsers))
 			usersRoute.Get("/search", Wrap(SearchUsersWithPaging))
 			usersRoute.Get("/search", Wrap(SearchUsersWithPaging))
 			usersRoute.Get("/:id", Wrap(GetUserByID))
 			usersRoute.Get("/:id", Wrap(GetUserByID))
+			usersRoute.Get("/:id/teams", Wrap(GetUserTeams))
 			usersRoute.Get("/:id/orgs", Wrap(GetUserOrgList))
 			usersRoute.Get("/:id/orgs", Wrap(GetUserOrgList))
 			// query parameters /users/lookup?loginOrEmail=admin@example.com
 			// query parameters /users/lookup?loginOrEmail=admin@example.com
 			usersRoute.Get("/lookup", Wrap(GetUserByLoginOrEmail))
 			usersRoute.Get("/lookup", Wrap(GetUserByLoginOrEmail))

+ 19 - 0
pkg/api/basic_auth.go

@@ -0,0 +1,19 @@
+package api
+
+import (
+	"crypto/subtle"
+	macaron "gopkg.in/macaron.v1"
+)
+
+// BasicAuthenticatedRequest parses the provided HTTP request for basic authentication credentials
+// and returns true if the provided credentials match the expected username and password.
+// Returns false if the request is unauthenticated.
+// Uses constant-time comparison in order to mitigate timing attacks.
+func BasicAuthenticatedRequest(req macaron.Request, expectedUser, expectedPass string) bool {
+	user, pass, ok := req.BasicAuth()
+	if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(expectedUser)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(expectedPass)) != 1 {
+		return false
+	}
+
+	return true
+}

+ 45 - 0
pkg/api/basic_auth_test.go

@@ -0,0 +1,45 @@
+package api
+
+import (
+	"encoding/base64"
+	"fmt"
+	"net/http"
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+	"gopkg.in/macaron.v1"
+)
+
+func TestBasicAuthenticatedRequest(t *testing.T) {
+	expectedUser := "prometheus"
+	expectedPass := "password"
+
+	Convey("Given a valid set of basic auth credentials", t, func() {
+		httpReq, err := http.NewRequest("GET", "http://localhost:3000/metrics", nil)
+		So(err, ShouldBeNil)
+		req := macaron.Request{
+			Request: httpReq,
+		}
+		encodedCreds := encodeBasicAuthCredentials(expectedUser, expectedPass)
+		req.Header.Add("Authorization", fmt.Sprintf("Basic %s", encodedCreds))
+		authenticated := BasicAuthenticatedRequest(req, expectedUser, expectedPass)
+		So(authenticated, ShouldBeTrue)
+	})
+
+	Convey("Given an invalid set of basic auth credentials", t, func() {
+		httpReq, err := http.NewRequest("GET", "http://localhost:3000/metrics", nil)
+		So(err, ShouldBeNil)
+		req := macaron.Request{
+			Request: httpReq,
+		}
+		encodedCreds := encodeBasicAuthCredentials("invaliduser", "invalidpass")
+		req.Header.Add("Authorization", fmt.Sprintf("Basic %s", encodedCreds))
+		authenticated := BasicAuthenticatedRequest(req, expectedUser, expectedPass)
+		So(authenticated, ShouldBeFalse)
+	})
+}
+
+func encodeBasicAuthCredentials(user, pass string) string {
+	creds := fmt.Sprintf("%s:%s", user, pass)
+	return base64.StdEncoding.EncodeToString([]byte(creds))
+}

+ 0 - 4
pkg/api/dashboard.go

@@ -277,10 +277,6 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
 		return Error(500, "Failed to save dashboard", err)
 		return Error(500, "Failed to save dashboard", err)
 	}
 	}
 
 
-	if err == m.ErrDashboardFailedToUpdateAlertData {
-		return Error(500, "Invalid alert data. Cannot save dashboard", err)
-	}
-
 	c.TimeRequest(metrics.M_Api_Dashboard_Save)
 	c.TimeRequest(metrics.M_Api_Dashboard_Save)
 	return JSON(200, util.DynMap{
 	return JSON(200, util.DynMap{
 		"status":  "success",
 		"status":  "success",

+ 0 - 1
pkg/api/dashboard_test.go

@@ -727,7 +727,6 @@ func TestDashboardApiEndpoint(t *testing.T) {
 				{SaveError: m.ErrDashboardTitleEmpty, ExpectedStatusCode: 400},
 				{SaveError: m.ErrDashboardTitleEmpty, ExpectedStatusCode: 400},
 				{SaveError: m.ErrDashboardFolderCannotHaveParent, ExpectedStatusCode: 400},
 				{SaveError: m.ErrDashboardFolderCannotHaveParent, ExpectedStatusCode: 400},
 				{SaveError: alerting.ValidationError{Reason: "Mu"}, ExpectedStatusCode: 422},
 				{SaveError: alerting.ValidationError{Reason: "Mu"}, ExpectedStatusCode: 422},
-				{SaveError: m.ErrDashboardFailedToUpdateAlertData, ExpectedStatusCode: 500},
 				{SaveError: m.ErrDashboardFailedGenerateUniqueUid, ExpectedStatusCode: 500},
 				{SaveError: m.ErrDashboardFailedGenerateUniqueUid, ExpectedStatusCode: 500},
 				{SaveError: m.ErrDashboardTypeMismatch, ExpectedStatusCode: 400},
 				{SaveError: m.ErrDashboardTypeMismatch, ExpectedStatusCode: 400},
 				{SaveError: m.ErrDashboardFolderWithSameNameAsDashboard, ExpectedStatusCode: 400},
 				{SaveError: m.ErrDashboardFolderWithSameNameAsDashboard, ExpectedStatusCode: 400},

+ 9 - 0
pkg/api/http_server.go

@@ -245,6 +245,11 @@ func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) {
 		return
 		return
 	}
 	}
 
 
+	if hs.metricsEndpointBasicAuthEnabled() && !BasicAuthenticatedRequest(ctx.Req, hs.Cfg.MetricsEndpointBasicAuthUsername, hs.Cfg.MetricsEndpointBasicAuthPassword) {
+		ctx.Resp.WriteHeader(http.StatusUnauthorized)
+		return
+	}
+
 	promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}).
 	promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}).
 		ServeHTTP(ctx.Resp, ctx.Req.Request)
 		ServeHTTP(ctx.Resp, ctx.Req.Request)
 }
 }
@@ -299,3 +304,7 @@ func (hs *HTTPServer) mapStatic(m *macaron.Macaron, rootDir string, dir string,
 		},
 		},
 	))
 	))
 }
 }
+
+func (hs *HTTPServer) metricsEndpointBasicAuthEnabled() bool {
+	return hs.Cfg.MetricsEndpointBasicAuthUsername != "" && hs.Cfg.MetricsEndpointBasicAuthPassword != ""
+}

+ 30 - 0
pkg/api/http_server_test.go

@@ -0,0 +1,30 @@
+package api
+
+import (
+	"testing"
+
+	"github.com/grafana/grafana/pkg/setting"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestHTTPServer(t *testing.T) {
+	Convey("Given a HTTPServer", t, func() {
+		ts := &HTTPServer{
+			Cfg: setting.NewCfg(),
+		}
+
+		Convey("Given that basic auth on the metrics endpoint is enabled", func() {
+			ts.Cfg.MetricsEndpointBasicAuthUsername = "foo"
+			ts.Cfg.MetricsEndpointBasicAuthPassword = "bar"
+
+			So(ts.metricsEndpointBasicAuthEnabled(), ShouldBeTrue)
+		})
+
+		Convey("Given that basic auth on the metrics endpoint is disabled", func() {
+			ts.Cfg.MetricsEndpointBasicAuthUsername = ""
+			ts.Cfg.MetricsEndpointBasicAuthPassword = ""
+
+			So(ts.metricsEndpointBasicAuthEnabled(), ShouldBeFalse)
+		})
+	})
+}

+ 22 - 0
pkg/api/login.go

@@ -39,6 +39,10 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) {
 		viewData.Settings["loginError"] = loginError
 		viewData.Settings["loginError"] = loginError
 	}
 	}
 
 
+	if tryOAuthAutoLogin(c) {
+		return
+	}
+
 	if !tryLoginUsingRememberCookie(c) {
 	if !tryLoginUsingRememberCookie(c) {
 		c.HTML(200, ViewIndex, viewData)
 		c.HTML(200, ViewIndex, viewData)
 		return
 		return
@@ -53,6 +57,24 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) {
 	c.Redirect(setting.AppSubUrl + "/")
 	c.Redirect(setting.AppSubUrl + "/")
 }
 }
 
 
+func tryOAuthAutoLogin(c *m.ReqContext) bool {
+	if !setting.OAuthAutoLogin {
+		return false
+	}
+	oauthInfos := setting.OAuthService.OAuthInfos
+	if len(oauthInfos) != 1 {
+		log.Warn("Skipping OAuth auto login because multiple OAuth providers are configured.")
+		return false
+	}
+	for key := range setting.OAuthService.OAuthInfos {
+		redirectUrl := setting.AppSubUrl + "/login/" + key
+		log.Info("OAuth auto login enabled. Redirecting to " + redirectUrl)
+		c.Redirect(redirectUrl, 307)
+		return true
+	}
+	return false
+}
+
 func tryLoginUsingRememberCookie(c *m.ReqContext) bool {
 func tryLoginUsingRememberCookie(c *m.ReqContext) bool {
 	// Check auto-login.
 	// Check auto-login.
 	uname := c.GetCookie(setting.CookieUserName)
 	uname := c.GetCookie(setting.CookieUserName)

+ 8 - 0
pkg/api/password.go

@@ -4,10 +4,18 @@ import (
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	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/util"
 	"github.com/grafana/grafana/pkg/util"
 )
 )
 
 
 func SendResetPasswordEmail(c *m.ReqContext, form dtos.SendResetPasswordEmailForm) Response {
 func SendResetPasswordEmail(c *m.ReqContext, form dtos.SendResetPasswordEmailForm) Response {
+	if setting.LdapEnabled || setting.AuthProxyEnabled {
+		return Error(401, "Not allowed to reset password when LDAP or Auth Proxy is enabled", nil)
+	}
+	if setting.DisableLoginForm {
+		return Error(401, "Not allowed to reset password when login form is disabled", nil)
+	}
+
 	userQuery := m.GetUserByLoginQuery{LoginOrEmail: form.UserOrEmail}
 	userQuery := m.GetUserByLoginQuery{LoginOrEmail: form.UserOrEmail}
 
 
 	if err := bus.Dispatch(&userQuery); err != nil {
 	if err := bus.Dispatch(&userQuery); err != nil {

+ 3 - 3
pkg/api/pluginproxy/ds_auth_provider.go

@@ -51,7 +51,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
 		if token, err := tokenProvider.getAccessToken(data); err != nil {
 		if token, err := tokenProvider.getAccessToken(data); err != nil {
 			logger.Error("Failed to get access token", "error", err)
 			logger.Error("Failed to get access token", "error", err)
 		} else {
 		} else {
-			req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
+			req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
 		}
 		}
 	}
 	}
 
 
@@ -60,7 +60,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
 		if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil {
 		if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil {
 			logger.Error("Failed to get access token", "error", err)
 			logger.Error("Failed to get access token", "error", err)
 		} else {
 		} else {
-			req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
+			req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
 		}
 		}
 	}
 	}
 
 
@@ -73,7 +73,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
 			if err != nil {
 			if err != nil {
 				logger.Error("Failed to get default access token from meta data server", "error", err)
 				logger.Error("Failed to get default access token from meta data server", "error", err)
 			} else {
 			} else {
-				req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
+				req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
 			}
 			}
 		}
 		}
 	}
 	}

+ 1 - 1
pkg/api/pluginproxy/pluginproxy.go

@@ -87,7 +87,7 @@ func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPl
 			}
 			}
 
 
 			for key, value := range headers {
 			for key, value := range headers {
-				log.Trace("setting key %v value %v", key, value[0])
+				log.Trace("setting key %v value <redacted>", key)
 				req.Header.Set(key, value[0])
 				req.Header.Set(key, value[0])
 			}
 			}
 		}
 		}

+ 11 - 3
pkg/api/user.go

@@ -113,7 +113,16 @@ func GetSignedInUserOrgList(c *m.ReqContext) Response {
 
 
 // GET /api/user/teams
 // GET /api/user/teams
 func GetSignedInUserTeamList(c *m.ReqContext) Response {
 func GetSignedInUserTeamList(c *m.ReqContext) Response {
-	query := m.GetTeamsByUserQuery{OrgId: c.OrgId, UserId: c.UserId}
+	return getUserTeamList(c.OrgId, c.UserId)
+}
+
+// GET /api/users/:id/teams
+func GetUserTeams(c *m.ReqContext) Response {
+	return getUserTeamList(c.OrgId, c.ParamsInt64(":id"))
+}
+
+func getUserTeamList(userID int64, orgID int64) Response {
+	query := m.GetTeamsByUserQuery{OrgId: orgID, UserId: userID}
 
 
 	if err := bus.Dispatch(&query); err != nil {
 	if err := bus.Dispatch(&query); err != nil {
 		return Error(500, "Failed to get user teams", err)
 		return Error(500, "Failed to get user teams", err)
@@ -122,11 +131,10 @@ func GetSignedInUserTeamList(c *m.ReqContext) Response {
 	for _, team := range query.Result {
 	for _, team := range query.Result {
 		team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name)
 		team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name)
 	}
 	}
-
 	return JSON(200, query.Result)
 	return JSON(200, query.Result)
 }
 }
 
 
-// GET /api/user/:id/orgs
+// GET /api/users/:id/orgs
 func GetUserOrgList(c *m.ReqContext) Response {
 func GetUserOrgList(c *m.ReqContext) Response {
 	return getUserOrgList(c.ParamsInt64(":id"))
 	return getUserOrgList(c.ParamsInt64(":id"))
 }
 }

+ 17 - 2
pkg/cmd/grafana-server/main.go

@@ -13,7 +13,7 @@ import (
 	"syscall"
 	"syscall"
 	"time"
 	"time"
 
 
-	extensions "github.com/grafana/grafana/pkg/extensions"
+	"github.com/grafana/grafana/pkg/extensions"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/metrics"
 	_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
 	_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
@@ -39,6 +39,7 @@ var buildstamp string
 var configFile = flag.String("config", "", "path to config file")
 var configFile = flag.String("config", "", "path to config file")
 var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory")
 var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory")
 var pidFile = flag.String("pidfile", "", "path to pid file")
 var pidFile = flag.String("pidfile", "", "path to pid file")
+var packaging = flag.String("packaging", "unknown", "describes the way Grafana was installed")
 
 
 func main() {
 func main() {
 	v := flag.Bool("v", false, "prints current version and exits")
 	v := flag.Bool("v", false, "prints current version and exits")
@@ -53,7 +54,10 @@ func main() {
 	if *profile {
 	if *profile {
 		runtime.SetBlockProfileRate(1)
 		runtime.SetBlockProfileRate(1)
 		go func() {
 		go func() {
-			http.ListenAndServe(fmt.Sprintf("localhost:%d", *profilePort), nil)
+			err := http.ListenAndServe(fmt.Sprintf("localhost:%d", *profilePort), nil)
+			if err != nil {
+				panic(err)
+			}
 		}()
 		}()
 
 
 		f, err := os.Create("trace.out")
 		f, err := os.Create("trace.out")
@@ -79,6 +83,7 @@ func main() {
 	setting.BuildStamp = buildstampInt64
 	setting.BuildStamp = buildstampInt64
 	setting.BuildBranch = buildBranch
 	setting.BuildBranch = buildBranch
 	setting.IsEnterprise = extensions.IsEnterprise
 	setting.IsEnterprise = extensions.IsEnterprise
+	setting.Packaging = validPackaging(*packaging)
 
 
 	metrics.SetBuildInformation(version, commit, buildBranch)
 	metrics.SetBuildInformation(version, commit, buildBranch)
 
 
@@ -95,6 +100,16 @@ func main() {
 	os.Exit(code)
 	os.Exit(code)
 }
 }
 
 
+func validPackaging(packaging string) string {
+	validTypes := []string{"dev", "deb", "rpm", "docker", "brew", "hosted", "unknown"}
+	for _, vt := range validTypes {
+		if packaging == vt {
+			return packaging
+		}
+	}
+	return "unknown"
+}
+
 func listenToSystemSignals(server *GrafanaServerImpl) {
 func listenToSystemSignals(server *GrafanaServerImpl) {
 	signalChan := make(chan os.Signal, 1)
 	signalChan := make(chan os.Signal, 1)
 	sighupChan := make(chan os.Signal, 1)
 	sighupChan := make(chan os.Signal, 1)

+ 26 - 6
pkg/cmd/grafana-server/server.go

@@ -67,6 +67,7 @@ type GrafanaServerImpl struct {
 }
 }
 
 
 func (g *GrafanaServerImpl) Run() error {
 func (g *GrafanaServerImpl) Run() error {
+	var err error
 	g.loadConfiguration()
 	g.loadConfiguration()
 	g.writePIDFile()
 	g.writePIDFile()
 
 
@@ -74,20 +75,38 @@ func (g *GrafanaServerImpl) Run() error {
 	social.NewOAuthService()
 	social.NewOAuthService()
 
 
 	serviceGraph := inject.Graph{}
 	serviceGraph := inject.Graph{}
-	serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
-	serviceGraph.Provide(&inject.Object{Value: g.cfg})
-	serviceGraph.Provide(&inject.Object{Value: routing.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
-	serviceGraph.Provide(&inject.Object{Value: cache.New(5*time.Minute, 10*time.Minute)})
+	err = serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
+	if err != nil {
+		return fmt.Errorf("Failed to provide object to the graph: %v", err)
+	}
+	err = serviceGraph.Provide(&inject.Object{Value: g.cfg})
+	if err != nil {
+		return fmt.Errorf("Failed to provide object to the graph: %v", err)
+	}
+	err = serviceGraph.Provide(&inject.Object{Value: routing.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
+	if err != nil {
+		return fmt.Errorf("Failed to provide object to the graph: %v", err)
+	}
+	err = serviceGraph.Provide(&inject.Object{Value: cache.New(5*time.Minute, 10*time.Minute)})
+	if err != nil {
+		return fmt.Errorf("Failed to provide object to the graph: %v", err)
+	}
 
 
 	// self registered services
 	// self registered services
 	services := registry.GetServices()
 	services := registry.GetServices()
 
 
 	// Add all services to dependency graph
 	// Add all services to dependency graph
 	for _, service := range services {
 	for _, service := range services {
-		serviceGraph.Provide(&inject.Object{Value: service.Instance})
+		err = serviceGraph.Provide(&inject.Object{Value: service.Instance})
+		if err != nil {
+			return fmt.Errorf("Failed to provide object to the graph: %v", err)
+		}
 	}
 	}
 
 
-	serviceGraph.Provide(&inject.Object{Value: g})
+	err = serviceGraph.Provide(&inject.Object{Value: g})
+	if err != nil {
+		return fmt.Errorf("Failed to provide object to the graph: %v", err)
+	}
 
 
 	// Inject dependencies to services
 	// Inject dependencies to services
 	if err := serviceGraph.Populate(); err != nil {
 	if err := serviceGraph.Populate(); err != nil {
@@ -144,6 +163,7 @@ func (g *GrafanaServerImpl) Run() error {
 	}
 	}
 
 
 	sendSystemdNotification("READY=1")
 	sendSystemdNotification("READY=1")
+
 	return g.childRoutines.Wait()
 	return g.childRoutines.Wait()
 }
 }
 
 

+ 1 - 1
pkg/components/dynmap/dynmap.go

@@ -1,5 +1,5 @@
 // uses code from https://github.com/antonholmquist/jason/blob/master/jason.go
 // uses code from https://github.com/antonholmquist/jason/blob/master/jason.go
-// MIT Licence
+// MIT License
 
 
 package dynmap
 package dynmap
 
 

+ 1 - 1
pkg/components/dynmap/dynmap_test.go

@@ -1,5 +1,5 @@
 // uses code from https://github.com/antonholmquist/jason/blob/master/jason.go
 // uses code from https://github.com/antonholmquist/jason/blob/master/jason.go
-// MIT Licence
+// MIT License
 
 
 package dynmap
 package dynmap
 
 

+ 9 - 6
pkg/metrics/metrics.go

@@ -313,7 +313,7 @@ func init() {
 
 
 // SetBuildInformation sets the build information for this binary
 // SetBuildInformation sets the build information for this binary
 func SetBuildInformation(version, revision, branch string) {
 func SetBuildInformation(version, revision, branch string) {
-	// We export this info twice for backwards compability.
+	// We export this info twice for backwards compatibility.
 	// Once this have been released for some time we should be able to remote `M_Grafana_Version`
 	// Once this have been released for some time we should be able to remote `M_Grafana_Version`
 	// The reason we added a new one is that its common practice in the prometheus community
 	// The reason we added a new one is that its common practice in the prometheus community
 	// to name this metric `*_build_info` so its easy to do aggregation on all programs.
 	// to name this metric `*_build_info` so its easy to do aggregation on all programs.
@@ -397,11 +397,12 @@ func sendUsageStats(oauthProviders map[string]bool) {
 
 
 	metrics := map[string]interface{}{}
 	metrics := map[string]interface{}{}
 	report := map[string]interface{}{
 	report := map[string]interface{}{
-		"version": version,
-		"metrics": metrics,
-		"os":      runtime.GOOS,
-		"arch":    runtime.GOARCH,
-		"edition": getEdition(),
+		"version":   version,
+		"metrics":   metrics,
+		"os":        runtime.GOOS,
+		"arch":      runtime.GOARCH,
+		"edition":   getEdition(),
+		"packaging": setting.Packaging,
 	}
 	}
 
 
 	statsQuery := models.GetSystemStatsQuery{}
 	statsQuery := models.GetSystemStatsQuery{}
@@ -447,6 +448,8 @@ func sendUsageStats(oauthProviders map[string]bool) {
 	}
 	}
 	metrics["stats.ds.other.count"] = dsOtherCount
 	metrics["stats.ds.other.count"] = dsOtherCount
 
 
+	metrics["stats.packaging."+setting.Packaging+".count"] = 1
+
 	dsAccessStats := models.GetDataSourceAccessStatsQuery{}
 	dsAccessStats := models.GetDataSourceAccessStatsQuery{}
 	if err := bus.Dispatch(&dsAccessStats); err != nil {
 	if err := bus.Dispatch(&dsAccessStats); err != nil {
 		metricsLogger.Error("Failed to get datasource access stats", "error", err)
 		metricsLogger.Error("Failed to get datasource access stats", "error", err)

+ 3 - 0
pkg/metrics/metrics_test.go

@@ -176,6 +176,7 @@ func TestMetrics(t *testing.T) {
 			setting.BasicAuthEnabled = true
 			setting.BasicAuthEnabled = true
 			setting.LdapEnabled = true
 			setting.LdapEnabled = true
 			setting.AuthProxyEnabled = true
 			setting.AuthProxyEnabled = true
+			setting.Packaging = "deb"
 
 
 			wg.Add(1)
 			wg.Add(1)
 			sendUsageStats(oauthProviders)
 			sendUsageStats(oauthProviders)
@@ -243,6 +244,8 @@ func TestMetrics(t *testing.T) {
 				So(metrics.Get("stats.auth_enabled.oauth_google.count").MustInt(), ShouldEqual, 1)
 				So(metrics.Get("stats.auth_enabled.oauth_google.count").MustInt(), ShouldEqual, 1)
 				So(metrics.Get("stats.auth_enabled.oauth_generic_oauth.count").MustInt(), ShouldEqual, 1)
 				So(metrics.Get("stats.auth_enabled.oauth_generic_oauth.count").MustInt(), ShouldEqual, 1)
 				So(metrics.Get("stats.auth_enabled.oauth_grafana_com.count").MustInt(), ShouldEqual, 1)
 				So(metrics.Get("stats.auth_enabled.oauth_grafana_com.count").MustInt(), ShouldEqual, 1)
+
+				So(metrics.Get("stats.packaging.deb.count").MustInt(), ShouldEqual, 1)
 			})
 			})
 		})
 		})
 
 

+ 1 - 0
pkg/middleware/recovery.go

@@ -115,6 +115,7 @@ func Recovery() macaron.Handler {
 
 
 				c.Data["Title"] = "Server Error"
 				c.Data["Title"] = "Server Error"
 				c.Data["AppSubUrl"] = setting.AppSubUrl
 				c.Data["AppSubUrl"] = setting.AppSubUrl
+				c.Data["Theme"] = setting.DefaultTheme
 
 
 				if setting.Env == setting.DEV {
 				if setting.Env == setting.DEV {
 					if theErr, ok := err.(error); ok {
 					if theErr, ok := err.(error); ok {

+ 10 - 3
pkg/models/alert.go

@@ -19,6 +19,7 @@ const (
 	AlertStateAlerting AlertStateType = "alerting"
 	AlertStateAlerting AlertStateType = "alerting"
 	AlertStateOK       AlertStateType = "ok"
 	AlertStateOK       AlertStateType = "ok"
 	AlertStatePending  AlertStateType = "pending"
 	AlertStatePending  AlertStateType = "pending"
+	AlertStateUnknown  AlertStateType = "unknown"
 )
 )
 
 
 const (
 const (
@@ -39,7 +40,12 @@ var (
 )
 )
 
 
 func (s AlertStateType) IsValid() bool {
 func (s AlertStateType) IsValid() bool {
-	return s == AlertStateOK || s == AlertStateNoData || s == AlertStatePaused || s == AlertStatePending
+	return s == AlertStateOK ||
+		s == AlertStateNoData ||
+		s == AlertStatePaused ||
+		s == AlertStatePending ||
+		s == AlertStateAlerting ||
+		s == AlertStateUnknown
 }
 }
 
 
 func (s NoDataOption) IsValid() bool {
 func (s NoDataOption) IsValid() bool {
@@ -66,12 +72,13 @@ type Alert struct {
 	PanelId        int64
 	PanelId        int64
 	Name           string
 	Name           string
 	Message        string
 	Message        string
-	Severity       string
+	Severity       string //Unused
 	State          AlertStateType
 	State          AlertStateType
-	Handler        int64
+	Handler        int64 //Unused
 	Silenced       bool
 	Silenced       bool
 	ExecutionError string
 	ExecutionError string
 	Frequency      int64
 	Frequency      int64
+	For            time.Duration
 
 
 	EvalData     *simplejson.Json
 	EvalData     *simplejson.Json
 	NewStateDate time.Time
 	NewStateDate time.Time

+ 0 - 1
pkg/models/dashboards.go

@@ -21,7 +21,6 @@ var (
 	ErrDashboardVersionMismatch                = errors.New("The dashboard has been changed by someone else")
 	ErrDashboardVersionMismatch                = errors.New("The dashboard has been changed by someone else")
 	ErrDashboardTitleEmpty                     = errors.New("Dashboard title cannot be empty")
 	ErrDashboardTitleEmpty                     = errors.New("Dashboard title cannot be empty")
 	ErrDashboardFolderCannotHaveParent         = errors.New("A Dashboard Folder cannot be added to another folder")
 	ErrDashboardFolderCannotHaveParent         = errors.New("A Dashboard Folder cannot be added to another folder")
-	ErrDashboardFailedToUpdateAlertData        = errors.New("Failed to save alert data")
 	ErrDashboardsWithSameSlugExists            = errors.New("Multiple dashboards with the same slug exists")
 	ErrDashboardsWithSameSlugExists            = errors.New("Multiple dashboards with the same slug exists")
 	ErrDashboardFailedGenerateUniqueUid        = errors.New("Failed to generate unique dashboard id")
 	ErrDashboardFailedGenerateUniqueUid        = errors.New("Failed to generate unique dashboard id")
 	ErrDashboardTypeMismatch                   = errors.New("Dashboard cannot be changed to a folder")
 	ErrDashboardTypeMismatch                   = errors.New("Dashboard cannot be changed to a folder")

+ 2 - 1
pkg/models/user.go

@@ -7,7 +7,8 @@ import (
 
 
 // Typed errors
 // Typed errors
 var (
 var (
-	ErrUserNotFound = errors.New("User not found")
+	ErrUserNotFound     = errors.New("User not found")
+	ErrLastGrafanaAdmin = errors.New("Cannot remove last grafana admin")
 )
 )
 
 
 type Password string
 type Password string

+ 29 - 3
pkg/services/alerting/eval_context.go

@@ -68,8 +68,13 @@ func (c *EvalContext) GetStateModel() *StateDescription {
 			Color: "#D63232",
 			Color: "#D63232",
 			Text:  "Alerting",
 			Text:  "Alerting",
 		}
 		}
+	case m.AlertStateUnknown:
+		return &StateDescription{
+			Color: "#888888",
+			Text:  "Unknown",
+		}
 	default:
 	default:
-		panic("Unknown rule state " + c.Rule.State)
+		panic("Unknown rule state for alert " + c.Rule.State)
 	}
 	}
 }
 }
 
 
@@ -113,7 +118,26 @@ func (c *EvalContext) GetRuleUrl() (string, error) {
 	return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil
 	return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil
 }
 }
 
 
+// GetNewState returns the new state from the alert rule evaluation
 func (c *EvalContext) GetNewState() m.AlertStateType {
 func (c *EvalContext) GetNewState() m.AlertStateType {
+	ns := getNewStateInternal(c)
+	if ns != m.AlertStateAlerting || c.Rule.For == 0 {
+		return ns
+	}
+
+	since := time.Since(c.Rule.LastStateChange)
+	if c.PrevAlertState == m.AlertStatePending && since > c.Rule.For {
+		return m.AlertStateAlerting
+	}
+
+	if c.PrevAlertState == m.AlertStateAlerting {
+		return m.AlertStateAlerting
+	}
+
+	return m.AlertStatePending
+}
+
+func getNewStateInternal(c *EvalContext) m.AlertStateType {
 	if c.Error != nil {
 	if c.Error != nil {
 		c.log.Error("Alert Rule Result Error",
 		c.log.Error("Alert Rule Result Error",
 			"ruleId", c.Rule.Id,
 			"ruleId", c.Rule.Id,
@@ -125,11 +149,13 @@ func (c *EvalContext) GetNewState() m.AlertStateType {
 			return c.PrevAlertState
 			return c.PrevAlertState
 		}
 		}
 		return c.Rule.ExecutionErrorState.ToAlertState()
 		return c.Rule.ExecutionErrorState.ToAlertState()
+	}
 
 
-	} else if c.Firing {
+	if c.Firing {
 		return m.AlertStateAlerting
 		return m.AlertStateAlerting
+	}
 
 
-	} else if c.NoDataFound {
+	if c.NoDataFound {
 		c.log.Info("Alert Rule returned no data",
 		c.log.Info("Alert Rule returned no data",
 			"ruleId", c.Rule.Id,
 			"ruleId", c.Rule.Id,
 			"name", c.Rule.Name,
 			"name", c.Rule.Name,

+ 172 - 67
pkg/services/alerting/eval_context_test.go

@@ -2,11 +2,11 @@ package alerting
 
 
 import (
 import (
 	"context"
 	"context"
-	"fmt"
+	"errors"
 	"testing"
 	"testing"
+	"time"
 
 
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/models"
-	. "github.com/smartystreets/goconvey/convey"
 )
 )
 
 
 func TestStateIsUpdatedWhenNeeded(t *testing.T) {
 func TestStateIsUpdatedWhenNeeded(t *testing.T) {
@@ -31,71 +31,176 @@ func TestStateIsUpdatedWhenNeeded(t *testing.T) {
 	})
 	})
 }
 }
 
 
-func TestAlertingEvalContext(t *testing.T) {
-	Convey("Should compute and replace properly new rule state", t, func() {
+func TestGetStateFromEvalContext(t *testing.T) {
+	tcs := []struct {
+		name     string
+		expected models.AlertStateType
+		applyFn  func(ec *EvalContext)
+	}{
+		{
+			name:     "ok -> alerting",
+			expected: models.AlertStateAlerting,
+			applyFn: func(ec *EvalContext) {
+				ec.Firing = true
+				ec.PrevAlertState = models.AlertStateOK
+			},
+		},
+		{
+			name:     "ok -> error(alerting)",
+			expected: models.AlertStateAlerting,
+			applyFn: func(ec *EvalContext) {
+				ec.PrevAlertState = models.AlertStateOK
+				ec.Error = errors.New("test error")
+				ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
+			},
+		},
+		{
+			name:     "ok -> pending. since its been firing for less than FOR",
+			expected: models.AlertStatePending,
+			applyFn: func(ec *EvalContext) {
+				ec.PrevAlertState = models.AlertStateOK
+				ec.Firing = true
+				ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2)
+				ec.Rule.For = time.Minute * 5
+			},
+		},
+		{
+			name:     "ok -> pending. since it has to be pending longer than FOR and prev state is ok",
+			expected: models.AlertStatePending,
+			applyFn: func(ec *EvalContext) {
+				ec.PrevAlertState = models.AlertStateOK
+				ec.Firing = true
+				ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5))
+				ec.Rule.For = time.Minute * 2
+			},
+		},
+		{
+			name:     "pending -> alerting. since its been firing for more than FOR and prev state is pending",
+			expected: models.AlertStateAlerting,
+			applyFn: func(ec *EvalContext) {
+				ec.PrevAlertState = models.AlertStatePending
+				ec.Firing = true
+				ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5))
+				ec.Rule.For = time.Minute * 2
+			},
+		},
+		{
+			name:     "alerting -> alerting. should not update regardless of FOR",
+			expected: models.AlertStateAlerting,
+			applyFn: func(ec *EvalContext) {
+				ec.PrevAlertState = models.AlertStateAlerting
+				ec.Firing = true
+				ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
+				ec.Rule.For = time.Minute * 2
+			},
+		},
+		{
+			name:     "ok -> ok. should not update regardless of FOR",
+			expected: models.AlertStateOK,
+			applyFn: func(ec *EvalContext) {
+				ec.PrevAlertState = models.AlertStateOK
+				ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
+				ec.Rule.For = time.Minute * 2
+			},
+		},
+		{
+			name:     "ok -> error(keep_last)",
+			expected: models.AlertStateOK,
+			applyFn: func(ec *EvalContext) {
+				ec.PrevAlertState = models.AlertStateOK
+				ec.Error = errors.New("test error")
+				ec.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
+			},
+		},
+		{
+			name:     "pending -> error(keep_last)",
+			expected: models.AlertStatePending,
+			applyFn: func(ec *EvalContext) {
+				ec.PrevAlertState = models.AlertStatePending
+				ec.Error = errors.New("test error")
+				ec.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
+			},
+		},
+		{
+			name:     "ok -> no_data(alerting)",
+			expected: models.AlertStateAlerting,
+			applyFn: func(ec *EvalContext) {
+				ec.PrevAlertState = models.AlertStateOK
+				ec.Rule.NoDataState = models.NoDataSetAlerting
+				ec.NoDataFound = true
+			},
+		},
+		{
+			name:     "ok -> no_data(keep_last)",
+			expected: models.AlertStateOK,
+			applyFn: func(ec *EvalContext) {
+				ec.PrevAlertState = models.AlertStateOK
+				ec.Rule.NoDataState = models.NoDataKeepState
+				ec.NoDataFound = true
+			},
+		},
+		{
+			name:     "pending -> no_data(keep_last)",
+			expected: models.AlertStatePending,
+			applyFn: func(ec *EvalContext) {
+				ec.PrevAlertState = models.AlertStatePending
+				ec.Rule.NoDataState = models.NoDataKeepState
+				ec.NoDataFound = true
+			},
+		},
+		{
+			name:     "pending -> no_data(alerting) with for duration have not passed",
+			expected: models.AlertStatePending,
+			applyFn: func(ec *EvalContext) {
+				ec.PrevAlertState = models.AlertStatePending
+				ec.Rule.NoDataState = models.NoDataSetAlerting
+				ec.NoDataFound = true
+				ec.Rule.For = time.Minute * 5
+				ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2)
+			},
+		},
+		{
+			name:     "pending -> no_data(alerting) should set alerting since time passed FOR",
+			expected: models.AlertStateAlerting,
+			applyFn: func(ec *EvalContext) {
+				ec.PrevAlertState = models.AlertStatePending
+				ec.Rule.NoDataState = models.NoDataSetAlerting
+				ec.NoDataFound = true
+				ec.Rule.For = time.Minute * 2
+				ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
+			},
+		},
+		{
+			name:     "pending -> error(alerting) with for duration have not passed ",
+			expected: models.AlertStatePending,
+			applyFn: func(ec *EvalContext) {
+				ec.PrevAlertState = models.AlertStatePending
+				ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
+				ec.Error = errors.New("test error")
+				ec.Rule.For = time.Minute * 5
+				ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2)
+			},
+		},
+		{
+			name:     "pending -> error(alerting) should set alerting since time passed FOR",
+			expected: models.AlertStateAlerting,
+			applyFn: func(ec *EvalContext) {
+				ec.PrevAlertState = models.AlertStatePending
+				ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
+				ec.Error = errors.New("test error")
+				ec.Rule.For = time.Minute * 2
+				ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5)
+			},
+		},
+	}
+
+	for _, tc := range tcs {
 		ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
 		ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}})
-		dummieError := fmt.Errorf("dummie error")
 
 
-		Convey("ok -> alerting", func() {
-			ctx.PrevAlertState = models.AlertStateOK
-			ctx.Firing = true
-
-			ctx.Rule.State = ctx.GetNewState()
-			So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
-		})
-
-		Convey("ok -> error(alerting)", func() {
-			ctx.PrevAlertState = models.AlertStateOK
-			ctx.Error = dummieError
-			ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting
-
-			ctx.Rule.State = ctx.GetNewState()
-			So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
-		})
-
-		Convey("ok -> error(keep_last)", func() {
-			ctx.PrevAlertState = models.AlertStateOK
-			ctx.Error = dummieError
-			ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
-
-			ctx.Rule.State = ctx.GetNewState()
-			So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
-		})
-
-		Convey("pending -> error(keep_last)", func() {
-			ctx.PrevAlertState = models.AlertStatePending
-			ctx.Error = dummieError
-			ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState
-
-			ctx.Rule.State = ctx.GetNewState()
-			So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
-		})
-
-		Convey("ok -> no_data(alerting)", func() {
-			ctx.PrevAlertState = models.AlertStateOK
-			ctx.Rule.NoDataState = models.NoDataSetAlerting
-			ctx.NoDataFound = true
-
-			ctx.Rule.State = ctx.GetNewState()
-			So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting)
-		})
-
-		Convey("ok -> no_data(keep_last)", func() {
-			ctx.PrevAlertState = models.AlertStateOK
-			ctx.Rule.NoDataState = models.NoDataKeepState
-			ctx.NoDataFound = true
-
-			ctx.Rule.State = ctx.GetNewState()
-			So(ctx.Rule.State, ShouldEqual, models.AlertStateOK)
-		})
-
-		Convey("pending -> no_data(keep_last)", func() {
-			ctx.PrevAlertState = models.AlertStatePending
-			ctx.Rule.NoDataState = models.NoDataKeepState
-			ctx.NoDataFound = true
-
-			ctx.Rule.State = ctx.GetNewState()
-			So(ctx.Rule.State, ShouldEqual, models.AlertStatePending)
-		})
-	})
+		tc.applyFn(ctx)
+		have := ctx.GetNewState()
+		if have != tc.expected {
+			t.Errorf("failed: %s \n expected '%s' have '%s'\n", tc.name, tc.expected, string(have))
+		}
+	}
 }
 }

+ 11 - 1
pkg/services/alerting/extractor.go

@@ -2,8 +2,8 @@ package alerting
 
 
 import (
 import (
 	"errors"
 	"errors"
-
 	"fmt"
 	"fmt"
+	"time"
 
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
@@ -115,6 +115,15 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
 			return nil, ValidationError{Reason: "Could not parse frequency"}
 			return nil, ValidationError{Reason: "Could not parse frequency"}
 		}
 		}
 
 
+		rawFor := jsonAlert.Get("for").MustString()
+		var forValue time.Duration
+		if rawFor != "" {
+			forValue, err = time.ParseDuration(rawFor)
+			if err != nil {
+				return nil, ValidationError{Reason: "Could not parse for"}
+			}
+		}
+
 		alert := &m.Alert{
 		alert := &m.Alert{
 			DashboardId: e.Dash.Id,
 			DashboardId: e.Dash.Id,
 			OrgId:       e.OrgID,
 			OrgId:       e.OrgID,
@@ -124,6 +133,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
 			Handler:     jsonAlert.Get("handler").MustInt64(),
 			Handler:     jsonAlert.Get("handler").MustInt64(),
 			Message:     jsonAlert.Get("message").MustString(),
 			Message:     jsonAlert.Get("message").MustString(),
 			Frequency:   frequency,
 			Frequency:   frequency,
+			For:         forValue,
 		}
 		}
 
 
 		for _, condition := range jsonAlert.Get("conditions").MustArray() {
 		for _, condition := range jsonAlert.Get("conditions").MustArray() {

+ 13 - 7
pkg/services/alerting/extractor_test.go

@@ -3,6 +3,7 @@ package alerting
 import (
 import (
 	"io/ioutil"
 	"io/ioutil"
 	"testing"
 	"testing"
+	"time"
 
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
@@ -46,7 +47,7 @@ func TestAlertRuleExtraction(t *testing.T) {
 			return nil
 			return nil
 		})
 		})
 
 
-		json, err := ioutil.ReadFile("./test-data/graphite-alert.json")
+		json, err := ioutil.ReadFile("./testdata/graphite-alert.json")
 		So(err, ShouldBeNil)
 		So(err, ShouldBeNil)
 
 
 		Convey("Extractor should not modify the original json", func() {
 		Convey("Extractor should not modify the original json", func() {
@@ -118,6 +119,11 @@ func TestAlertRuleExtraction(t *testing.T) {
 					So(alerts[1].PanelId, ShouldEqual, 4)
 					So(alerts[1].PanelId, ShouldEqual, 4)
 				})
 				})
 
 
+				Convey("should extract for param", func() {
+					So(alerts[0].For, ShouldEqual, time.Minute*2)
+					So(alerts[1].For, ShouldEqual, time.Duration(0))
+				})
+
 				Convey("should extract name and desc", func() {
 				Convey("should extract name and desc", func() {
 					So(alerts[0].Name, ShouldEqual, "name1")
 					So(alerts[0].Name, ShouldEqual, "name1")
 					So(alerts[0].Message, ShouldEqual, "desc1")
 					So(alerts[0].Message, ShouldEqual, "desc1")
@@ -140,7 +146,7 @@ func TestAlertRuleExtraction(t *testing.T) {
 		})
 		})
 
 
 		Convey("Panels missing id should return error", func() {
 		Convey("Panels missing id should return error", func() {
-			panelWithoutId, err := ioutil.ReadFile("./test-data/panels-missing-id.json")
+			panelWithoutId, err := ioutil.ReadFile("./testdata/panels-missing-id.json")
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
 			dashJson, err := simplejson.NewJson(panelWithoutId)
 			dashJson, err := simplejson.NewJson(panelWithoutId)
@@ -156,7 +162,7 @@ func TestAlertRuleExtraction(t *testing.T) {
 		})
 		})
 
 
 		Convey("Panel with id set to zero should return error", func() {
 		Convey("Panel with id set to zero should return error", func() {
-			panelWithIdZero, err := ioutil.ReadFile("./test-data/panel-with-id-0.json")
+			panelWithIdZero, err := ioutil.ReadFile("./testdata/panel-with-id-0.json")
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
 			dashJson, err := simplejson.NewJson(panelWithIdZero)
 			dashJson, err := simplejson.NewJson(panelWithIdZero)
@@ -172,7 +178,7 @@ func TestAlertRuleExtraction(t *testing.T) {
 		})
 		})
 
 
 		Convey("Parse alerts from dashboard without rows", func() {
 		Convey("Parse alerts from dashboard without rows", func() {
-			json, err := ioutil.ReadFile("./test-data/v5-dashboard.json")
+			json, err := ioutil.ReadFile("./testdata/v5-dashboard.json")
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
 			dashJson, err := simplejson.NewJson(json)
 			dashJson, err := simplejson.NewJson(json)
@@ -192,7 +198,7 @@ func TestAlertRuleExtraction(t *testing.T) {
 		})
 		})
 
 
 		Convey("Parse and validate dashboard containing influxdb alert", func() {
 		Convey("Parse and validate dashboard containing influxdb alert", func() {
-			json, err := ioutil.ReadFile("./test-data/influxdb-alert.json")
+			json, err := ioutil.ReadFile("./testdata/influxdb-alert.json")
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
 			dashJson, err := simplejson.NewJson(json)
 			dashJson, err := simplejson.NewJson(json)
@@ -221,7 +227,7 @@ func TestAlertRuleExtraction(t *testing.T) {
 		})
 		})
 
 
 		Convey("Should be able to extract collapsed panels", func() {
 		Convey("Should be able to extract collapsed panels", func() {
-			json, err := ioutil.ReadFile("./test-data/collapsed-panels.json")
+			json, err := ioutil.ReadFile("./testdata/collapsed-panels.json")
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
 			dashJson, err := simplejson.NewJson(json)
 			dashJson, err := simplejson.NewJson(json)
@@ -242,7 +248,7 @@ func TestAlertRuleExtraction(t *testing.T) {
 		})
 		})
 
 
 		Convey("Parse and validate dashboard without id and containing an alert", func() {
 		Convey("Parse and validate dashboard without id and containing an alert", func() {
-			json, err := ioutil.ReadFile("./test-data/dash-without-id.json")
+			json, err := ioutil.ReadFile("./testdata/dash-without-id.json")
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
 			dashJSON, err := simplejson.NewJson(json)
 			dashJSON, err := simplejson.NewJson(json)

+ 47 - 0
pkg/services/alerting/notifiers/alertmanager_test.go

@@ -1,13 +1,60 @@
 package notifiers
 package notifiers
 
 
 import (
 import (
+	"context"
 	"testing"
 	"testing"
 
 
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
+	"github.com/grafana/grafana/pkg/log"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/alerting"
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
 )
 )
 
 
+func TestWhenAlertManagerShouldNotify(t *testing.T) {
+	tcs := []struct {
+		prevState m.AlertStateType
+		newState  m.AlertStateType
+
+		expect bool
+	}{
+		{
+			prevState: m.AlertStatePending,
+			newState:  m.AlertStateOK,
+			expect:    false,
+		},
+		{
+			prevState: m.AlertStateAlerting,
+			newState:  m.AlertStateOK,
+			expect:    true,
+		},
+		{
+			prevState: m.AlertStateOK,
+			newState:  m.AlertStatePending,
+			expect:    false,
+		},
+		{
+			prevState: m.AlertStateUnknown,
+			newState:  m.AlertStatePending,
+			expect:    false,
+		},
+	}
+
+	for _, tc := range tcs {
+		am := &AlertmanagerNotifier{log: log.New("test.logger")}
+		evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
+			State: tc.prevState,
+		})
+
+		evalContext.Rule.State = tc.newState
+
+		res := am.ShouldNotify(context.TODO(), evalContext, &m.AlertNotificationState{})
+		if res != tc.expect {
+			t.Errorf("got %v expected %v", res, tc.expect)
+		}
+	}
+}
+
 func TestAlertmanagerNotifier(t *testing.T) {
 func TestAlertmanagerNotifier(t *testing.T) {
 	Convey("Alertmanager notifier tests", t, func() {
 	Convey("Alertmanager notifier tests", t, func() {
 
 

+ 10 - 0
pkg/services/alerting/notifiers/base.go

@@ -67,6 +67,16 @@ func (n *NotifierBase) ShouldNotify(ctx context.Context, context *alerting.EvalC
 	}
 	}
 
 
 	// Do not notify when we become OK for the first time.
 	// Do not notify when we become OK for the first time.
+	if context.PrevAlertState == models.AlertStateUnknown && context.Rule.State == models.AlertStateOK {
+		return false
+	}
+
+	// Do not notify when we become OK for the first time.
+	if context.PrevAlertState == models.AlertStateUnknown && context.Rule.State == models.AlertStatePending {
+		return false
+	}
+
+	// Do not notify when we become OK from pending
 	if context.PrevAlertState == models.AlertStatePending && context.Rule.State == models.AlertStateOK {
 	if context.PrevAlertState == models.AlertStatePending && context.Rule.State == models.AlertStateOK {
 		return false
 		return false
 	}
 	}

+ 25 - 7
pkg/services/alerting/notifiers/base_test.go

@@ -29,7 +29,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
 			newState:     m.AlertStateOK,
 			newState:     m.AlertStateOK,
 			prevState:    m.AlertStatePending,
 			prevState:    m.AlertStatePending,
 			sendReminder: false,
 			sendReminder: false,
-			state:        &m.AlertNotificationState{},
 
 
 			expect: false,
 			expect: false,
 		},
 		},
@@ -38,7 +37,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
 			newState:     m.AlertStateAlerting,
 			newState:     m.AlertStateAlerting,
 			prevState:    m.AlertStateOK,
 			prevState:    m.AlertStateOK,
 			sendReminder: false,
 			sendReminder: false,
-			state:        &m.AlertNotificationState{},
 
 
 			expect: true,
 			expect: true,
 		},
 		},
@@ -47,7 +45,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
 			newState:     m.AlertStatePending,
 			newState:     m.AlertStatePending,
 			prevState:    m.AlertStateOK,
 			prevState:    m.AlertStateOK,
 			sendReminder: false,
 			sendReminder: false,
-			state:        &m.AlertNotificationState{},
 
 
 			expect: false,
 			expect: false,
 		},
 		},
@@ -56,7 +53,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
 			newState:     m.AlertStateOK,
 			newState:     m.AlertStateOK,
 			prevState:    m.AlertStateOK,
 			prevState:    m.AlertStateOK,
 			sendReminder: false,
 			sendReminder: false,
-			state:        &m.AlertNotificationState{},
 
 
 			expect: false,
 			expect: false,
 		},
 		},
@@ -65,7 +61,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
 			newState:     m.AlertStateOK,
 			newState:     m.AlertStateOK,
 			prevState:    m.AlertStateOK,
 			prevState:    m.AlertStateOK,
 			sendReminder: true,
 			sendReminder: true,
-			state:        &m.AlertNotificationState{},
 
 
 			expect: false,
 			expect: false,
 		},
 		},
@@ -74,7 +69,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
 			newState:     m.AlertStateOK,
 			newState:     m.AlertStateOK,
 			prevState:    m.AlertStateAlerting,
 			prevState:    m.AlertStateAlerting,
 			sendReminder: false,
 			sendReminder: false,
-			state:        &m.AlertNotificationState{},
 
 
 			expect: true,
 			expect: true,
 		},
 		},
@@ -94,7 +88,6 @@ func TestShouldSendAlertNotification(t *testing.T) {
 			prevState:    m.AlertStateAlerting,
 			prevState:    m.AlertStateAlerting,
 			frequency:    time.Minute * 10,
 			frequency:    time.Minute * 10,
 			sendReminder: true,
 			sendReminder: true,
-			state:        &m.AlertNotificationState{},
 
 
 			expect: true,
 			expect: true,
 		},
 		},
@@ -132,6 +125,27 @@ func TestShouldSendAlertNotification(t *testing.T) {
 			prevState: m.AlertStateOK,
 			prevState: m.AlertStateOK,
 			state:     &m.AlertNotificationState{State: m.AlertNotificationStatePending, UpdatedAt: tnow.Add(-2 * time.Minute).Unix()},
 			state:     &m.AlertNotificationState{State: m.AlertNotificationStatePending, UpdatedAt: tnow.Add(-2 * time.Minute).Unix()},
 
 
+			expect: true,
+		},
+		{
+			name:      "unknown -> ok",
+			prevState: m.AlertStateUnknown,
+			newState:  m.AlertStateOK,
+
+			expect: false,
+		},
+		{
+			name:      "unknown -> pending",
+			prevState: m.AlertStateUnknown,
+			newState:  m.AlertStatePending,
+
+			expect: false,
+		},
+		{
+			name:      "unknown -> alerting",
+			prevState: m.AlertStateUnknown,
+			newState:  m.AlertStateAlerting,
+
 			expect: true,
 			expect: true,
 		},
 		},
 	}
 	}
@@ -141,6 +155,10 @@ func TestShouldSendAlertNotification(t *testing.T) {
 			State: tc.prevState,
 			State: tc.prevState,
 		})
 		})
 
 
+		if tc.state == nil {
+			tc.state = &m.AlertNotificationState{}
+		}
+
 		evalContext.Rule.State = tc.newState
 		evalContext.Rule.State = tc.newState
 		nb := &NotifierBase{SendReminder: tc.sendReminder, Frequency: tc.frequency}
 		nb := &NotifierBase{SendReminder: tc.sendReminder, Frequency: tc.frequency}
 
 

+ 3 - 0
pkg/services/alerting/result_handler.go

@@ -73,6 +73,9 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
 			// when two servers are raising. This makes sure that the server
 			// when two servers are raising. This makes sure that the server
 			// with the last state change always sends a notification.
 			// with the last state change always sends a notification.
 			evalContext.Rule.StateChanges = cmd.Result.StateChanges
 			evalContext.Rule.StateChanges = cmd.Result.StateChanges
+
+			// Update the last state change of the alert rule in memory
+			evalContext.Rule.LastStateChange = time.Now()
 		}
 		}
 
 
 		// save annotation
 		// save annotation

+ 5 - 0
pkg/services/alerting/rule.go

@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"fmt"
 	"regexp"
 	"regexp"
 	"strconv"
 	"strconv"
+	"time"
 
 
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 
 
@@ -18,6 +19,8 @@ type Rule struct {
 	Frequency           int64
 	Frequency           int64
 	Name                string
 	Name                string
 	Message             string
 	Message             string
+	LastStateChange     time.Time
+	For                 time.Duration
 	NoDataState         m.NoDataOption
 	NoDataState         m.NoDataOption
 	ExecutionErrorState m.ExecutionErrorOption
 	ExecutionErrorState m.ExecutionErrorOption
 	State               m.AlertStateType
 	State               m.AlertStateType
@@ -100,6 +103,8 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
 	model.Message = ruleDef.Message
 	model.Message = ruleDef.Message
 	model.Frequency = ruleDef.Frequency
 	model.Frequency = ruleDef.Frequency
 	model.State = ruleDef.State
 	model.State = ruleDef.State
+	model.LastStateChange = ruleDef.NewStateDate
+	model.For = ruleDef.For
 	model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data"))
 	model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data"))
 	model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting"))
 	model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting"))
 	model.StateChanges = ruleDef.StateChanges
 	model.StateChanges = ruleDef.StateChanges

+ 0 - 0
pkg/services/alerting/test-data/collapsed-panels.json → pkg/services/alerting/testdata/collapsed-panels.json


+ 0 - 0
pkg/services/alerting/test-data/dash-without-id.json → pkg/services/alerting/testdata/dash-without-id.json


+ 1 - 0
pkg/services/alerting/test-data/graphite-alert.json → pkg/services/alerting/testdata/graphite-alert.json

@@ -23,6 +23,7 @@
           "message": "desc1",
           "message": "desc1",
           "handler": 1,
           "handler": 1,
           "frequency": "60s",
           "frequency": "60s",
+          "for": "2m",
           "conditions": [
           "conditions": [
           {
           {
             "type": "query",
             "type": "query",

+ 0 - 0
pkg/services/alerting/test-data/influxdb-alert.json → pkg/services/alerting/testdata/influxdb-alert.json


+ 0 - 0
pkg/services/alerting/test-data/panel-with-id-0.json → pkg/services/alerting/testdata/panel-with-id-0.json


+ 0 - 0
pkg/services/alerting/test-data/panels-missing-id.json → pkg/services/alerting/testdata/panels-missing-id.json


+ 0 - 0
pkg/services/alerting/test-data/v5-dashboard.json → pkg/services/alerting/testdata/v5-dashboard.json


+ 1 - 1
pkg/services/dashboards/dashboard_service.go

@@ -165,7 +165,7 @@ func (dr *dashboardServiceImpl) updateAlerting(cmd *models.SaveDashboardCommand,
 	}
 	}
 
 
 	if err := bus.Dispatch(&alertCmd); err != nil {
 	if err := bus.Dispatch(&alertCmd); err != nil {
-		return models.ErrDashboardFailedToUpdateAlertData
+		return err
 	}
 	}
 
 
 	return nil
 	return nil

+ 5 - 4
pkg/services/sqlstore/alert.go

@@ -193,7 +193,8 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS
 			if alertToUpdate.ContainsUpdates(alert) {
 			if alertToUpdate.ContainsUpdates(alert) {
 				alert.Updated = timeNow()
 				alert.Updated = timeNow()
 				alert.State = alertToUpdate.State
 				alert.State = alertToUpdate.State
-				sess.MustCols("message")
+				sess.MustCols("message", "for")
+
 				_, err := sess.ID(alert.Id).Update(alert)
 				_, err := sess.ID(alert.Id).Update(alert)
 				if err != nil {
 				if err != nil {
 					return err
 					return err
@@ -204,7 +205,7 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS
 		} else {
 		} else {
 			alert.Updated = timeNow()
 			alert.Updated = timeNow()
 			alert.Created = timeNow()
 			alert.Created = timeNow()
-			alert.State = m.AlertStatePending
+			alert.State = m.AlertStateUnknown
 			alert.NewStateDate = timeNow()
 			alert.NewStateDate = timeNow()
 
 
 			_, err := sess.Insert(alert)
 			_, err := sess.Insert(alert)
@@ -299,7 +300,7 @@ func PauseAlert(cmd *m.PauseAlertCommand) error {
 			params = append(params, string(m.AlertStatePaused))
 			params = append(params, string(m.AlertStatePaused))
 			params = append(params, timeNow())
 			params = append(params, timeNow())
 		} else {
 		} else {
-			params = append(params, string(m.AlertStatePending))
+			params = append(params, string(m.AlertStateUnknown))
 			params = append(params, timeNow())
 			params = append(params, timeNow())
 		}
 		}
 
 
@@ -323,7 +324,7 @@ func PauseAllAlerts(cmd *m.PauseAllAlertCommand) error {
 		if cmd.Paused {
 		if cmd.Paused {
 			newState = string(m.AlertStatePaused)
 			newState = string(m.AlertStatePaused)
 		} else {
 		} else {
-			newState = string(m.AlertStatePending)
+			newState = string(m.AlertStateUnknown)
 		}
 		}
 
 
 		res, err := sess.Exec(`UPDATE alert SET state = ?, new_state_date = ?`, newState, timeNow())
 		res, err := sess.Exec(`UPDATE alert SET state = ?, new_state_date = ?`, newState, timeNow())

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

@@ -109,7 +109,7 @@ func TestAlertingDataAccess(t *testing.T) {
 			So(alert.DashboardId, ShouldEqual, testDash.Id)
 			So(alert.DashboardId, ShouldEqual, testDash.Id)
 			So(alert.PanelId, ShouldEqual, 1)
 			So(alert.PanelId, ShouldEqual, 1)
 			So(alert.Name, ShouldEqual, "Alerting title")
 			So(alert.Name, ShouldEqual, "Alerting title")
-			So(alert.State, ShouldEqual, "pending")
+			So(alert.State, ShouldEqual, m.AlertStateUnknown)
 			So(alert.NewStateDate, ShouldNotBeNil)
 			So(alert.NewStateDate, ShouldNotBeNil)
 			So(alert.EvalData, ShouldNotBeNil)
 			So(alert.EvalData, ShouldNotBeNil)
 			So(alert.EvalData.Get("test").MustString(), ShouldEqual, "test")
 			So(alert.EvalData.Get("test").MustString(), ShouldEqual, "test")
@@ -154,7 +154,7 @@ func TestAlertingDataAccess(t *testing.T) {
 				So(query.Result[0].Name, ShouldEqual, "Name")
 				So(query.Result[0].Name, ShouldEqual, "Name")
 
 
 				Convey("Alert state should not be updated", func() {
 				Convey("Alert state should not be updated", func() {
-					So(query.Result[0].State, ShouldEqual, "pending")
+					So(query.Result[0].State, ShouldEqual, m.AlertStateUnknown)
 				})
 				})
 			})
 			})
 
 

+ 4 - 0
pkg/services/sqlstore/migrations/alert_mig.go

@@ -133,4 +133,8 @@ func addAlertMigrations(mg *Migrator) {
 	mg.AddMigration("create alert_notification_state table v1", NewAddTableMigration(alert_notification_state))
 	mg.AddMigration("create alert_notification_state table v1", NewAddTableMigration(alert_notification_state))
 	mg.AddMigration("add index alert_notification_state org_id & alert_id & notifier_id",
 	mg.AddMigration("add index alert_notification_state org_id & alert_id & notifier_id",
 		NewAddIndexMigration(alert_notification_state, alert_notification_state.Indices[0]))
 		NewAddIndexMigration(alert_notification_state, alert_notification_state.Indices[0]))
+
+	mg.AddMigration("Add for to alert table", NewAddColumnMigration(alertV1, &Column{
+		Name: "for", Type: DB_BigInt, Nullable: true,
+	}))
 }
 }

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

@@ -187,7 +187,7 @@ func TestAccountDataAccess(t *testing.T) {
 					err := DeleteOrg(&m.DeleteOrgCommand{Id: ac2.OrgId})
 					err := DeleteOrg(&m.DeleteOrgCommand{Id: ac2.OrgId})
 					So(err, ShouldBeNil)
 					So(err, ShouldBeNil)
 
 
-					// remove frome ac2 from ac1 org
+					// remove ac2 user from ac1 org
 					remCmd := m.RemoveOrgUserCommand{OrgId: ac1.OrgId, UserId: ac2.Id, ShouldDeleteOrphanedUser: true}
 					remCmd := m.RemoveOrgUserCommand{OrgId: ac1.OrgId, UserId: ac2.Id, ShouldDeleteOrphanedUser: true}
 					err = RemoveOrgUser(&remCmd)
 					err = RemoveOrgUser(&remCmd)
 					So(err, ShouldBeNil)
 					So(err, ShouldBeNil)

+ 6 - 6
pkg/services/sqlstore/quota.go

@@ -99,14 +99,14 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error {
 	return inTransaction(func(sess *DBSession) error {
 	return inTransaction(func(sess *DBSession) error {
 		//Check if quota is already defined in the DB
 		//Check if quota is already defined in the DB
 		quota := m.Quota{
 		quota := m.Quota{
-			Target:  cmd.Target,
-			OrgId:   cmd.OrgId,
-			Updated: time.Now(),
+			Target: cmd.Target,
+			OrgId:  cmd.OrgId,
 		}
 		}
 		has, err := sess.Get(&quota)
 		has, err := sess.Get(&quota)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
+		quota.Updated = time.Now()
 		quota.Limit = cmd.Limit
 		quota.Limit = cmd.Limit
 		if !has {
 		if !has {
 			quota.Created = time.Now()
 			quota.Created = time.Now()
@@ -201,14 +201,14 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error {
 	return inTransaction(func(sess *DBSession) error {
 	return inTransaction(func(sess *DBSession) error {
 		//Check if quota is already defined in the DB
 		//Check if quota is already defined in the DB
 		quota := m.Quota{
 		quota := m.Quota{
-			Target:  cmd.Target,
-			UserId:  cmd.UserId,
-			Updated: time.Now(),
+			Target: cmd.Target,
+			UserId: cmd.UserId,
 		}
 		}
 		has, err := sess.Get(&quota)
 		has, err := sess.Get(&quota)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
+		quota.Updated = time.Now()
 		quota.Limit = cmd.Limit
 		quota.Limit = cmd.Limit
 		if !has {
 		if !has {
 			quota.Created = time.Now()
 			quota.Created = time.Now()

+ 65 - 0
pkg/services/sqlstore/quota_test.go

@@ -2,6 +2,7 @@ package sqlstore
 
 
 import (
 import (
 	"testing"
 	"testing"
+	"time"
 
 
 	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"
@@ -168,5 +169,69 @@ func TestQuotaCommandsAndQueries(t *testing.T) {
 			So(query.Result.Limit, ShouldEqual, 5)
 			So(query.Result.Limit, ShouldEqual, 5)
 			So(query.Result.Used, ShouldEqual, 1)
 			So(query.Result.Used, ShouldEqual, 1)
 		})
 		})
+
+		// related: https://github.com/grafana/grafana/issues/14342
+		Convey("Should org quota updating is successful even if it called multiple time", func() {
+			orgCmd := m.UpdateOrgQuotaCmd{
+				OrgId:  orgId,
+				Target: "org_user",
+				Limit:  5,
+			}
+			err := UpdateOrgQuota(&orgCmd)
+			So(err, ShouldBeNil)
+
+			query := m.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: "org_user", Default: 1}
+			err = GetOrgQuotaByTarget(&query)
+			So(err, ShouldBeNil)
+			So(query.Result.Limit, ShouldEqual, 5)
+
+			// XXX: resolution of `Updated` column is 1sec, so this makes delay
+			time.Sleep(1 * time.Second)
+
+			orgCmd = m.UpdateOrgQuotaCmd{
+				OrgId:  orgId,
+				Target: "org_user",
+				Limit:  10,
+			}
+			err = UpdateOrgQuota(&orgCmd)
+			So(err, ShouldBeNil)
+
+			query = m.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: "org_user", Default: 1}
+			err = GetOrgQuotaByTarget(&query)
+			So(err, ShouldBeNil)
+			So(query.Result.Limit, ShouldEqual, 10)
+		})
+
+		// related: https://github.com/grafana/grafana/issues/14342
+		Convey("Should user quota updating is successful even if it called multiple time", func() {
+			userQuotaCmd := m.UpdateUserQuotaCmd{
+				UserId: userId,
+				Target: "org_user",
+				Limit:  5,
+			}
+			err := UpdateUserQuota(&userQuotaCmd)
+			So(err, ShouldBeNil)
+
+			query := m.GetUserQuotaByTargetQuery{UserId: userId, Target: "org_user", Default: 1}
+			err = GetUserQuotaByTarget(&query)
+			So(err, ShouldBeNil)
+			So(query.Result.Limit, ShouldEqual, 5)
+
+			// XXX: resolution of `Updated` column is 1sec, so this makes delay
+			time.Sleep(1 * time.Second)
+
+			userQuotaCmd = m.UpdateUserQuotaCmd{
+				UserId: userId,
+				Target: "org_user",
+				Limit:  10,
+			}
+			err = UpdateUserQuota(&userQuotaCmd)
+			So(err, ShouldBeNil)
+
+			query = m.GetUserQuotaByTargetQuery{UserId: userId, Target: "org_user", Default: 1}
+			err = GetUserQuotaByTarget(&query)
+			So(err, ShouldBeNil)
+			So(query.Result.Limit, ShouldEqual, 10)
+		})
 	})
 	})
 }
 }

+ 25 - 1
pkg/services/sqlstore/user.go

@@ -504,8 +504,18 @@ func UpdateUserPermissions(cmd *m.UpdateUserPermissionsCommand) error {
 
 
 		user.IsAdmin = cmd.IsGrafanaAdmin
 		user.IsAdmin = cmd.IsGrafanaAdmin
 		sess.UseBool("is_admin")
 		sess.UseBool("is_admin")
+
 		_, err := sess.ID(user.Id).Update(&user)
 		_, err := sess.ID(user.Id).Update(&user)
-		return err
+		if err != nil {
+			return err
+		}
+
+		// validate that after update there is at least one server admin
+		if err := validateOneAdminLeft(sess); err != nil {
+			return err
+		}
+
+		return nil
 	})
 	})
 }
 }
 
 
@@ -522,3 +532,17 @@ func SetUserHelpFlag(cmd *m.SetUserHelpFlagCommand) error {
 		return err
 		return err
 	})
 	})
 }
 }
+
+func validateOneAdminLeft(sess *DBSession) error {
+	// validate that there is an admin user left
+	count, err := sess.Where("is_admin=?", true).Count(&m.User{})
+	if err != nil {
+		return err
+	}
+
+	if count == 0 {
+		return m.ErrLastGrafanaAdmin
+	}
+
+	return nil
+}

+ 26 - 0
pkg/services/sqlstore/user_test.go

@@ -155,6 +155,32 @@ func TestUserDataAccess(t *testing.T) {
 				})
 				})
 			})
 			})
 		})
 		})
+
+		Convey("Given one grafana admin user", func() {
+			var err error
+			createUserCmd := &m.CreateUserCommand{
+				Email:   fmt.Sprint("admin", "@test.com"),
+				Name:    fmt.Sprint("admin"),
+				Login:   fmt.Sprint("admin"),
+				IsAdmin: true,
+			}
+			err = CreateUser(context.Background(), createUserCmd)
+			So(err, ShouldBeNil)
+
+			Convey("Cannot make themselves a non-admin", func() {
+				updateUserPermsCmd := m.UpdateUserPermissionsCommand{IsGrafanaAdmin: false, UserId: 1}
+				updatePermsError := UpdateUserPermissions(&updateUserPermsCmd)
+
+				So(updatePermsError, ShouldEqual, m.ErrLastGrafanaAdmin)
+
+				query := m.GetUserByIdQuery{Id: createUserCmd.Result.Id}
+				getUserError := GetUserById(&query)
+
+				So(getUserError, ShouldBeNil)
+
+				So(query.Result.IsAdmin, ShouldEqual, true)
+			})
+		})
 	})
 	})
 }
 }
 
 

+ 9 - 0
pkg/setting/setting.go

@@ -57,6 +57,9 @@ var (
 	IsEnterprise    bool
 	IsEnterprise    bool
 	ApplicationName string
 	ApplicationName string
 
 
+	// packaging
+	Packaging = "unknown"
+
 	// Paths
 	// Paths
 	HomePath       string
 	HomePath       string
 	PluginsPath    string
 	PluginsPath    string
@@ -112,6 +115,7 @@ var (
 	ExternalUserMngLinkUrl  string
 	ExternalUserMngLinkUrl  string
 	ExternalUserMngLinkName string
 	ExternalUserMngLinkName string
 	ExternalUserMngInfo     string
 	ExternalUserMngInfo     string
+	OAuthAutoLogin          bool
 	ViewersCanEdit          bool
 	ViewersCanEdit          bool
 
 
 	// Http auth
 	// Http auth
@@ -215,6 +219,8 @@ type Cfg struct {
 	DisableBruteForceLoginProtection bool
 	DisableBruteForceLoginProtection bool
 	TempDataLifetime                 time.Duration
 	TempDataLifetime                 time.Duration
 	MetricsEndpointEnabled           bool
 	MetricsEndpointEnabled           bool
+	MetricsEndpointBasicAuthUsername string
+	MetricsEndpointBasicAuthPassword string
 	EnableAlphaPanels                bool
 	EnableAlphaPanels                bool
 	EnterpriseLicensePath            string
 	EnterpriseLicensePath            string
 }
 }
@@ -626,6 +632,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
 	auth := iniFile.Section("auth")
 	auth := iniFile.Section("auth")
 	DisableLoginForm = auth.Key("disable_login_form").MustBool(false)
 	DisableLoginForm = auth.Key("disable_login_form").MustBool(false)
 	DisableSignoutMenu = auth.Key("disable_signout_menu").MustBool(false)
 	DisableSignoutMenu = auth.Key("disable_signout_menu").MustBool(false)
+	OAuthAutoLogin = auth.Key("oauth_auto_login").MustBool(false)
 	SignoutRedirectUrl = auth.Key("signout_redirect_url").String()
 	SignoutRedirectUrl = auth.Key("signout_redirect_url").String()
 
 
 	// anonymous access
 	// anonymous access
@@ -676,6 +683,8 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
 	cfg.PhantomDir = filepath.Join(HomePath, "tools/phantomjs")
 	cfg.PhantomDir = filepath.Join(HomePath, "tools/phantomjs")
 	cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24)
 	cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24)
 	cfg.MetricsEndpointEnabled = iniFile.Section("metrics").Key("enabled").MustBool(true)
 	cfg.MetricsEndpointEnabled = iniFile.Section("metrics").Key("enabled").MustBool(true)
+	cfg.MetricsEndpointBasicAuthUsername = iniFile.Section("metrics").Key("basic_auth_username").String()
+	cfg.MetricsEndpointBasicAuthPassword = iniFile.Section("metrics").Key("basic_auth_password").String()
 
 
 	analytics := iniFile.Section("analytics")
 	analytics := iniFile.Section("analytics")
 	ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true)
 	ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true)

+ 2 - 0
pkg/social/google_oauth.go

@@ -32,6 +32,7 @@ func (s *SocialGoogle) IsSignupAllowed() bool {
 
 
 func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
 func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
 	var data struct {
 	var data struct {
+		Id    string `json:"id"`
 		Name  string `json:"name"`
 		Name  string `json:"name"`
 		Email string `json:"email"`
 		Email string `json:"email"`
 	}
 	}
@@ -47,6 +48,7 @@ func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*Basi
 	}
 	}
 
 
 	return &BasicUserInfo{
 	return &BasicUserInfo{
+		Id:    data.Id,
 		Name:  data.Name,
 		Name:  data.Name,
 		Email: data.Email,
 		Email: data.Email,
 		Login: data.Email,
 		Login: data.Email,

+ 25 - 2
pkg/tsdb/cloudwatch/cloudwatch.go

@@ -126,6 +126,18 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
 		}
 		}
 
 
 		eg.Go(func() error {
 		eg.Go(func() error {
+			defer func() {
+				if err := recover(); err != nil {
+					plog.Error("Execute Query Panic", "error", err, "stack", log.Stack(1))
+					if theErr, ok := err.(error); ok {
+						resultChan <- &tsdb.QueryResult{
+							RefId: query.RefId,
+							Error: theErr,
+						}
+					}
+				}
+			}()
+
 			queryRes, err := e.executeQuery(ectx, query, queryContext)
 			queryRes, err := e.executeQuery(ectx, query, queryContext)
 			if ae, ok := err.(awserr.Error); ok && ae.Code() == "500" {
 			if ae, ok := err.(awserr.Error); ok && ae.Code() == "500" {
 				return err
 				return err
@@ -146,6 +158,17 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
 		for region, getMetricDataQuery := range getMetricDataQueries {
 		for region, getMetricDataQuery := range getMetricDataQueries {
 			q := getMetricDataQuery
 			q := getMetricDataQuery
 			eg.Go(func() error {
 			eg.Go(func() error {
+				defer func() {
+					if err := recover(); err != nil {
+						plog.Error("Execute Get Metric Data Query Panic", "error", err, "stack", log.Stack(1))
+						if theErr, ok := err.(error); ok {
+							resultChan <- &tsdb.QueryResult{
+								Error: theErr,
+							}
+						}
+					}
+				}()
+
 				queryResponses, err := e.executeGetMetricDataQuery(ectx, region, q, queryContext)
 				queryResponses, err := e.executeGetMetricDataQuery(ectx, region, q, queryContext)
 				if ae, ok := err.(awserr.Error); ok && ae.Code() == "500" {
 				if ae, ok := err.(awserr.Error); ok && ae.Code() == "500" {
 					return err
 					return err
@@ -188,8 +211,8 @@ func (e *CloudWatchExecutor) executeQuery(ctx context.Context, query *CloudWatch
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	if endTime.Before(startTime) {
-		return nil, fmt.Errorf("Invalid time range: End time can't be before start time")
+	if !startTime.Before(endTime) {
+		return nil, fmt.Errorf("Invalid time range: Start time must be before end time")
 	}
 	}
 
 
 	params := &cloudwatch.GetMetricStatisticsInput{
 	params := &cloudwatch.GetMetricStatisticsInput{

+ 22 - 0
pkg/tsdb/cloudwatch/cloudwatch_test.go

@@ -1,9 +1,13 @@
 package cloudwatch
 package cloudwatch
 
 
 import (
 import (
+	"context"
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
+	"github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/tsdb"
+
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/service/cloudwatch"
 	"github.com/aws/aws-sdk-go/service/cloudwatch"
 	"github.com/grafana/grafana/pkg/components/null"
 	"github.com/grafana/grafana/pkg/components/null"
@@ -14,6 +18,24 @@ import (
 func TestCloudWatch(t *testing.T) {
 func TestCloudWatch(t *testing.T) {
 	Convey("CloudWatch", t, func() {
 	Convey("CloudWatch", t, func() {
 
 
+		Convey("executeQuery", func() {
+			e := &CloudWatchExecutor{
+				DataSource: &models.DataSource{
+					JsonData: simplejson.New(),
+				},
+			}
+
+			Convey("End time before start time should result in error", func() {
+				_, err := e.executeQuery(context.Background(), &CloudWatchQuery{}, &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("now-1h", "now-2h")})
+				So(err.Error(), ShouldEqual, "Invalid time range: Start time must be before end time")
+			})
+
+			Convey("End time equals start time should result in error", func() {
+				_, err := e.executeQuery(context.Background(), &CloudWatchQuery{}, &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("now-1h", "now-1h")})
+				So(err.Error(), ShouldEqual, "Invalid time range: Start time must be before end time")
+			})
+		})
+
 		Convey("can parse cloudwatch json model", func() {
 		Convey("can parse cloudwatch json model", func() {
 			json := `
 			json := `
 				{
 				{

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

@@ -46,6 +46,8 @@ func init() {
 		"AWS/Billing":        {"EstimatedCharges"},
 		"AWS/Billing":        {"EstimatedCharges"},
 		"AWS/CloudFront":     {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"},
 		"AWS/CloudFront":     {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"},
 		"AWS/CloudSearch":    {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"},
 		"AWS/CloudSearch":    {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"},
+		"AWS/CloudHSM":       {"HsmUnhealthy", "HsmTemperature", "HsmKeysSessionOccupied", "HsmKeysTokenOccupied", "HsmSslCtxsOccupied", "HsmSessionCount", "HsmUsersAvailable", "HsmUsersMax", "InterfaceEth2OctetsInput", "InterfaceEth2OctetsOutput"},
+		"AWS/CodeBuild":      {"BuildDuration", "Builds", "DownloadSourceDuration", "Duration", "FailedBuilds", "FinalizingDuration", "InstallDuration", "PostBuildDuration", "PreBuildDuration", "ProvisioningDuration", "QueuedDuration", "SubmittedDuration", "SucceededBuilds", "UploadArtifactsDuration"},
 		"AWS/Connect":        {"CallsBreachingConcurrencyQuota", "CallBackNotDialableNumber", "CallRecordingUploadError", "CallsPerInterval", "ConcurrentCalls", "ConcurrentCallsPercentage", "ContactFlowErrors", "ContactFlowFatalErrors", "LongestQueueWaitTime", "MissedCalls", "MisconfiguredPhoneNumbers", "PublicSigningKeyUsage", "QueueCapacityExceededError", "QueueSize", "ThrottledCalls", "ToInstancePacketLossRate"},
 		"AWS/Connect":        {"CallsBreachingConcurrencyQuota", "CallBackNotDialableNumber", "CallRecordingUploadError", "CallsPerInterval", "ConcurrentCalls", "ConcurrentCallsPercentage", "ContactFlowErrors", "ContactFlowFatalErrors", "LongestQueueWaitTime", "MissedCalls", "MisconfiguredPhoneNumbers", "PublicSigningKeyUsage", "QueueCapacityExceededError", "QueueSize", "ThrottledCalls", "ToInstancePacketLossRate"},
 		"AWS/DMS":            {"FreeableMemory", "WriteIOPS", "ReadIOPS", "WriteThroughput", "ReadThroughput", "WriteLatency", "ReadLatency", "SwapUsage", "NetworkTransmitThroughput", "NetworkReceiveThroughput", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "CDCIncomingChanges", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CDCLatencySource", "CDCLatencyTarget"},
 		"AWS/DMS":            {"FreeableMemory", "WriteIOPS", "ReadIOPS", "WriteThroughput", "ReadThroughput", "WriteLatency", "ReadLatency", "SwapUsage", "NetworkTransmitThroughput", "NetworkReceiveThroughput", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "CDCIncomingChanges", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CDCLatencySource", "CDCLatencyTarget"},
 		"AWS/DX":             {"ConnectionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelTx", "ConnectionLightLevelRx"},
 		"AWS/DX":             {"ConnectionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelTx", "ConnectionLightLevelRx"},
@@ -121,6 +123,8 @@ func init() {
 		"AWS/Billing":          {"ServiceName", "LinkedAccount", "Currency"},
 		"AWS/Billing":          {"ServiceName", "LinkedAccount", "Currency"},
 		"AWS/CloudFront":       {"DistributionId", "Region"},
 		"AWS/CloudFront":       {"DistributionId", "Region"},
 		"AWS/CloudSearch":      {},
 		"AWS/CloudSearch":      {},
+		"AWS/CloudHSM":         {"Region", "ClusterId", "HsmId"},
+		"AWS/CodeBuild":        {"ProjectName"},
 		"AWS/Connect":          {"InstanceId", "MetricGroup", "Participant", "QueueName", "Stream Type", "Type of Connection"},
 		"AWS/Connect":          {"InstanceId", "MetricGroup", "Participant", "QueueName", "Stream Type", "Type of Connection"},
 		"AWS/DMS":              {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"},
 		"AWS/DMS":              {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"},
 		"AWS/DX":               {"ConnectionId"},
 		"AWS/DX":               {"ConnectionId"},

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

@@ -65,7 +65,7 @@ var NewClient = func(ctx context.Context, ds *models.DataSource, timeRange *tsdb
 	clientLog.Debug("Creating new client", "version", version, "timeField", timeField, "indices", strings.Join(indices, ", "))
 	clientLog.Debug("Creating new client", "version", version, "timeField", timeField, "indices", strings.Join(indices, ", "))
 
 
 	switch version {
 	switch version {
-	case 2, 5, 56:
+	case 2, 5, 56, 60:
 		return &baseClientImpl{
 		return &baseClientImpl{
 			ctx:       ctx,
 			ctx:       ctx,
 			ds:        ds,
 			ds:        ds,

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio