Просмотр исходного кода

Merge remote-tracking branch 'upstream/master' into postgres-query-builder

Sven Klemm 7 лет назад
Родитель
Сommit
19dcc1f41a
71 измененных файлов с 872 добавлено и 412 удалено
  1. 37 1
      .circleci/config.yml
  2. 1 0
      .gitignore
  3. 5 0
      CHANGELOG.md
  4. 0 1
      build.go
  5. 3 1
      docker/blocks/openldap/Dockerfile
  6. 5 4
      docker/blocks/openldap/entrypoint.sh
  7. 25 1
      docker/blocks/openldap/notes.md
  8. 14 0
      docker/blocks/openldap/prepopulate.sh
  9. 9 0
      docker/blocks/openldap/prepopulate/1_units.ldif
  10. 80 0
      docker/blocks/openldap/prepopulate/2_users.ldif
  11. 25 0
      docker/blocks/openldap/prepopulate/3_groups.ldif
  12. 0 10
      docker/blocks/openldap/prepopulate/admin.ldif
  13. 0 5
      docker/blocks/openldap/prepopulate/adminsgroup.ldif
  14. 0 10
      docker/blocks/openldap/prepopulate/editor.ldif
  15. 0 5
      docker/blocks/openldap/prepopulate/usersgroup.ldif
  16. 0 9
      docker/blocks/openldap/prepopulate/viewer.ldif
  17. 2 2
      docs/sources/installation/behind_proxy.md
  18. 1 1
      pkg/api/alerting_test.go
  19. 3 3
      pkg/api/annotations_test.go
  20. 136 147
      pkg/api/api.go
  21. 1 1
      pkg/api/app_routes.go
  22. 1 1
      pkg/api/common.go
  23. 2 2
      pkg/api/common_test.go
  24. 1 1
      pkg/api/dashboard_permission_test.go
  25. 2 2
      pkg/api/dashboard_test.go
  26. 1 0
      pkg/api/dtos/index.go
  27. 1 1
      pkg/api/folder_permission_test.go
  28. 2 2
      pkg/api/folder_test.go
  29. 1 0
      pkg/api/frontendsettings.go
  30. 31 7
      pkg/api/http_server.go
  31. 1 0
      pkg/api/index.go
  32. 2 3
      pkg/cmd/grafana-server/main.go
  33. 1 1
      pkg/extensions/main.go
  34. 17 1
      pkg/login/ext_user.go
  35. 2 0
      pkg/login/ldap.go
  36. 13 3
      pkg/login/ldap_test.go
  37. 9 0
      pkg/metrics/metrics.go
  38. 1 0
      pkg/models/team_member.go
  39. 6 0
      pkg/models/user_auth.go
  40. 11 1
      pkg/registry/registry.go
  41. 1 0
      pkg/services/sqlstore/migrations/team_mig.go
  42. 7 0
      pkg/services/sqlstore/sqlstore.go
  43. 9 1
      pkg/services/sqlstore/team.go
  44. 4 3
      pkg/setting/setting.go
  45. 1 0
      pkg/social/github_oauth.go
  46. 1 1
      pkg/tsdb/mssql/macros.go
  47. 2 2
      pkg/tsdb/mssql/macros_test.go
  48. 15 9
      pkg/tsdb/mssql/mssql_test.go
  49. 1 1
      pkg/tsdb/mysql/macros.go
  50. 2 2
      pkg/tsdb/mysql/macros_test.go
  51. 16 9
      pkg/tsdb/mysql/mysql_test.go
  52. 1 1
      pkg/tsdb/postgres/macros.go
  53. 2 2
      pkg/tsdb/postgres/macros_test.go
  54. 17 9
      pkg/tsdb/postgres/postgres_test.go
  55. 16 2
      public/app/core/config.ts
  56. 0 4
      public/app/core/services/context_srv.ts
  57. 35 0
      public/app/core/specs/table_model.jest.ts
  58. 14 0
      public/app/core/specs/time_series.jest.ts
  59. 5 12
      public/app/core/table_model.ts
  60. 1 3
      public/app/features/dashboard/specs/exporter.jest.ts
  61. 80 42
      public/app/features/org/partials/team_details.html
  62. 27 0
      public/app/features/org/team_details_ctrl.ts
  63. 14 11
      public/app/features/plugins/datasource_srv.ts
  64. 59 0
      public/app/features/plugins/specs/datasource_srv.jest.ts
  65. 0 64
      public/app/features/plugins/specs/datasource_srv_specs.ts
  66. 6 2
      public/app/features/templating/variable_srv.ts
  67. 2 2
      public/app/plugins/datasource/prometheus/datasource.ts
  68. 1 1
      public/app/plugins/datasource/prometheus/specs/datasource.jest.ts
  69. 78 0
      public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts
  70. 1 1
      public/views/index.template.html
  71. 2 2
      scripts/build/build_enterprise.sh

+ 37 - 1
.circleci/config.yml

@@ -160,7 +160,7 @@ jobs:
     steps:
       - checkout
       - run:
-          name: build and package grafana
+          name: build, test and package grafana enterprise
           command: './scripts/build/build_enterprise.sh'
       - run:
           name: sign packages
@@ -168,6 +168,26 @@ jobs:
       - run:
           name: sha-sum packages
           command: 'go run build.go sha-dist'
+      - run:
+          name: move enterprise packages into their own folder
+          command: 'mv dist enterprise-dist'
+      - persist_to_workspace:
+          root: .
+          paths:
+            - enterprise-dist/grafana-enterprise*
+
+  deploy-enterprise-master:
+    docker:
+      - image: circleci/python:2.7-stretch
+    steps:
+      - attach_workspace:
+          at: .
+      - run:
+          name: install awscli
+          command: 'sudo pip install awscli'
+      - run:
+          name: deploy to s3
+          command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/master'
 
   deploy-master:
     docker:
@@ -221,6 +241,8 @@ workflows:
     jobs:
       - build-all:
           filters: *filter-not-release
+      - build-enterprise:
+          filters: *filter-not-release
       - codespell:
           filters: *filter-not-release
       - gometalinter:
@@ -245,6 +267,20 @@ workflows:
           filters:
            branches:
              only: master
+      - deploy-enterprise-master:
+          requires:
+            - build-all
+            - test-backend
+            - test-frontend
+            - codespell
+            - gometalinter
+            - mysql-integration-test
+            - postgres-integration-test
+            - build-enterprise
+          filters:
+           branches:
+             only: master
+
   release:
     jobs:
       - build-all:

+ 1 - 0
.gitignore

@@ -43,6 +43,7 @@ fig.yml
 docker-compose.yml
 docker-compose.yaml
 /conf/provisioning/**/custom.yaml
+/conf/ldap_dev.toml
 profile.cov
 /grafana
 /local

+ 5 - 0
CHANGELOG.md

@@ -8,6 +8,11 @@
 * **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
 * **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248)
 * **Singlestat**: Make colorization of prefix and postfix optional in singlestat [#11892](https://github.com/grafana/grafana/pull/11892), thx [@ApsOps](https://github.com/ApsOps)
+* **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2)
+* **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
+* **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda)
+* **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)
+* **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)
 
 # 5.2.1 (2018-06-29)
 

+ 0 - 1
build.go

@@ -465,7 +465,6 @@ func ldflags() string {
 	b.WriteString(fmt.Sprintf(" -X main.version=%s", version))
 	b.WriteString(fmt.Sprintf(" -X main.commit=%s", getGitSha()))
 	b.WriteString(fmt.Sprintf(" -X main.buildstamp=%d", buildStamp()))
-	b.WriteString(fmt.Sprintf(" -X main.enterprise=%t", enterprise))
 	return b.String()
 }
 

+ 3 - 1
docker/blocks/openldap/Dockerfile

@@ -8,7 +8,8 @@ ENV OPENLDAP_VERSION 2.4.40
 
 RUN apt-get update && \
     DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
-        slapd=${OPENLDAP_VERSION}* && \
+        slapd=${OPENLDAP_VERSION}* \
+        ldap-utils && \
     apt-get clean && \
     rm -rf /var/lib/apt/lists/*
 
@@ -22,6 +23,7 @@ COPY modules/ /etc/ldap.dist/modules
 COPY prepopulate/ /etc/ldap.dist/prepopulate
 
 COPY entrypoint.sh /entrypoint.sh
+COPY prepopulate.sh /prepopulate.sh
 
 ENTRYPOINT ["/entrypoint.sh"]
 

+ 5 - 4
docker/blocks/openldap/entrypoint.sh

@@ -76,13 +76,14 @@ EOF
         IFS=","; declare -a modules=($SLAPD_ADDITIONAL_MODULES); unset IFS
 
         for module in "${modules[@]}"; do
-             slapadd -n0 -F /etc/ldap/slapd.d -l "/etc/ldap/modules/${module}.ldif" >/dev/null 2>&1
+          echo "Adding module ${module}"
+          slapadd -n0 -F /etc/ldap/slapd.d -l "/etc/ldap/modules/${module}.ldif" >/dev/null 2>&1
         done
     fi
 
-    for file in `ls /etc/ldap/prepopulate/*.ldif`; do
-        slapadd -F /etc/ldap/slapd.d -l "$file"
-    done
+    # This needs to run in background
+    # Will prepopulate entries after ldap daemon has started
+    ./prepopulate.sh &
 
     chown -R openldap:openldap /etc/ldap/slapd.d/ /var/lib/ldap/ /var/run/slapd/
 else

+ 25 - 1
docker/blocks/openldap/notes.md

@@ -1,6 +1,6 @@
 # Notes on OpenLdap Docker Block
 
-Any ldif files added to the prepopulate subdirectory will be automatically imported into the OpenLdap database. 
+Any ldif files added to the prepopulate subdirectory will be automatically imported into the OpenLdap database.
 
 The ldif files add three users, `ldapviewer`, `ldapeditor` and `ldapadmin`. Two groups, `admins` and `users`, are added that correspond with the group mappings in the default conf/ldap.toml. `ldapadmin` is a member of `admins` and `ldapeditor` is a member of `users`.
 
@@ -22,3 +22,27 @@ enabled = true
 config_file = conf/ldap.toml
 ; allow_sign_up = true
 ```
+
+Test groups & users
+
+admins
+  ldap-admin
+  ldap-torkel
+  ldap-daniel
+backend
+  ldap-carl
+  ldap-torkel
+  ldap-leo
+frontend
+  ldap-torkel
+  ldap-tobias
+  ldap-daniel
+editors
+  ldap-editors
+
+
+no groups
+  ldap-viewer
+
+
+

+ 14 - 0
docker/blocks/openldap/prepopulate.sh

@@ -0,0 +1,14 @@
+#!/bin/bash
+
+echo "Pre-populating ldap entries, first waiting for ldap to start"
+
+sleep 3
+
+adminUserDn="cn=admin,dc=grafana,dc=org"
+adminPassword="grafana"
+
+for file in `ls /etc/ldap/prepopulate/*.ldif`; do
+  ldapadd -x -D $adminUserDn -w $adminPassword -f "$file"
+done
+
+

+ 9 - 0
docker/blocks/openldap/prepopulate/1_units.ldif

@@ -0,0 +1,9 @@
+dn: ou=groups,dc=grafana,dc=org
+ou: Groups
+objectclass: top
+objectclass: organizationalUnit
+
+dn: ou=users,dc=grafana,dc=org
+ou: Users
+objectclass: top
+objectclass: organizationalUnit

+ 80 - 0
docker/blocks/openldap/prepopulate/2_users.ldif

@@ -0,0 +1,80 @@
+# ldap-admin
+dn: cn=ldap-admin,ou=users,dc=grafana,dc=org
+mail: ldap-admin@grafana.com
+userPassword: grafana
+objectClass: person
+objectClass: top
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+sn: ldap-admin
+cn: ldap-admin
+
+dn: cn=ldap-editor,ou=users,dc=grafana,dc=org
+mail: ldap-editor@grafana.com
+userPassword: grafana
+objectClass: person
+objectClass: top
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+sn: ldap-editor
+cn: ldap-editor
+
+dn: cn=ldap-viewer,ou=users,dc=grafana,dc=org
+mail: ldap-viewer@grafana.com
+userPassword: grafana
+objectClass: person
+objectClass: top
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+sn: ldap-viewer
+cn: ldap-viewer
+
+dn: cn=ldap-carl,ou=users,dc=grafana,dc=org
+mail: ldap-carl@grafana.com
+userPassword: grafana
+objectClass: person
+objectClass: top
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+sn: ldap-carl
+cn: ldap-carl
+
+dn: cn=ldap-daniel,ou=users,dc=grafana,dc=org
+mail: ldap-daniel@grafana.com
+userPassword: grafana
+objectClass: person
+objectClass: top
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+sn: ldap-daniel
+cn: ldap-daniel
+
+dn: cn=ldap-leo,ou=users,dc=grafana,dc=org
+mail: ldap-leo@grafana.com
+userPassword: grafana
+objectClass: person
+objectClass: top
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+sn: ldap-leo
+cn: ldap-leo
+
+dn: cn=ldap-tobias,ou=users,dc=grafana,dc=org
+mail: ldap-tobias@grafana.com
+userPassword: grafana
+objectClass: person
+objectClass: top
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+sn: ldap-tobias
+cn: ldap-tobias
+
+dn: cn=ldap-torkel,ou=users,dc=grafana,dc=org
+mail: ldap-torkel@grafana.com
+userPassword: grafana
+objectClass: person
+objectClass: top
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+sn: ldap-torkel
+cn: ldap-torkel

+ 25 - 0
docker/blocks/openldap/prepopulate/3_groups.ldif

@@ -0,0 +1,25 @@
+dn: cn=admins,ou=groups,dc=grafana,dc=org
+cn: admins
+objectClass: groupOfNames
+objectClass: top
+member: cn=ldap-admin,ou=users,dc=grafana,dc=org
+member: cn=ldap-torkel,ou=users,dc=grafana,dc=org
+
+dn: cn=editors,ou=groups,dc=grafana,dc=org
+cn: editors
+objectClass: groupOfNames
+member: cn=ldap-editor,ou=users,dc=grafana,dc=org
+
+dn: cn=backend,ou=groups,dc=grafana,dc=org
+cn: backend
+objectClass: groupOfNames
+member: cn=ldap-carl,ou=users,dc=grafana,dc=org
+member: cn=ldap-leo,ou=users,dc=grafana,dc=org
+member: cn=ldap-torkel,ou=users,dc=grafana,dc=org
+
+dn: cn=frontend,ou=groups,dc=grafana,dc=org
+cn: frontend
+objectClass: groupOfNames
+member: cn=ldap-torkel,ou=users,dc=grafana,dc=org
+member: cn=ldap-daniel,ou=users,dc=grafana,dc=org
+member: cn=ldap-leo,ou=users,dc=grafana,dc=org

+ 0 - 10
docker/blocks/openldap/prepopulate/admin.ldif

@@ -1,10 +0,0 @@
-dn: cn=ldapadmin,dc=grafana,dc=org
-mail: ldapadmin@grafana.com
-userPassword: grafana
-objectClass: person
-objectClass: top
-objectClass: inetOrgPerson
-objectClass: organizationalPerson
-sn: ldapadmin
-cn: ldapadmin
-memberOf: cn=admins,dc=grafana,dc=org

+ 0 - 5
docker/blocks/openldap/prepopulate/adminsgroup.ldif

@@ -1,5 +0,0 @@
-dn: cn=admins,dc=grafana,dc=org
-cn: admins
-member: cn=ldapadmin,dc=grafana,dc=org
-objectClass: groupOfNames
-objectClass: top

+ 0 - 10
docker/blocks/openldap/prepopulate/editor.ldif

@@ -1,10 +0,0 @@
-dn: cn=ldapeditor,dc=grafana,dc=org
-mail: ldapeditor@grafana.com
-userPassword: grafana
-objectClass: person
-objectClass: top
-objectClass: inetOrgPerson
-objectClass: organizationalPerson
-sn: ldapeditor
-cn: ldapeditor
-memberOf: cn=users,dc=grafana,dc=org

+ 0 - 5
docker/blocks/openldap/prepopulate/usersgroup.ldif

@@ -1,5 +0,0 @@
-dn: cn=users,dc=grafana,dc=org
-cn: users
-member: cn=ldapeditor,dc=grafana,dc=org
-objectClass: groupOfNames
-objectClass: top

+ 0 - 9
docker/blocks/openldap/prepopulate/viewer.ldif

@@ -1,9 +0,0 @@
-dn: cn=ldapviewer,dc=grafana,dc=org
-mail: ldapviewer@grafana.com
-userPassword: grafana
-objectClass: person
-objectClass: top
-objectClass: inetOrgPerson
-objectClass: organizationalPerson
-sn: ldapviewer
-cn: ldapviewer

+ 2 - 2
docs/sources/installation/behind_proxy.md

@@ -26,7 +26,7 @@ Otherwise Grafana will not behave correctly. See example below.
 ## Examples
 Here are some example configurations for running Grafana behind a reverse proxy.
 
-### Grafana configuration (ex http://foo.bar.com)
+### Grafana configuration (ex http://foo.bar)
 
 ```bash
 [server]
@@ -47,7 +47,7 @@ server {
 }
 ```
 
-### Examples with **sub path** (ex http://foo.bar.com/grafana)
+### Examples with **sub path** (ex http://foo.bar/grafana)
 
 #### Grafana configuration with sub path
 ```bash

+ 1 - 1
pkg/api/alerting_test.go

@@ -135,7 +135,7 @@ func postAlertScenario(desc string, url string, routePattern string, role m.Role
 		defer bus.ClearBusHandlers()
 
 		sc := setupScenarioContext(url)
-		sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
 			sc.context = c
 			sc.context.UserId = TestUserID
 			sc.context.OrgId = TestOrgID

+ 3 - 3
pkg/api/annotations_test.go

@@ -223,7 +223,7 @@ func postAnnotationScenario(desc string, url string, routePattern string, role m
 		defer bus.ClearBusHandlers()
 
 		sc := setupScenarioContext(url)
-		sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
 			sc.context = c
 			sc.context.UserId = TestUserID
 			sc.context.OrgId = TestOrgID
@@ -246,7 +246,7 @@ func putAnnotationScenario(desc string, url string, routePattern string, role m.
 		defer bus.ClearBusHandlers()
 
 		sc := setupScenarioContext(url)
-		sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
 			sc.context = c
 			sc.context.UserId = TestUserID
 			sc.context.OrgId = TestOrgID
@@ -269,7 +269,7 @@ func deleteAnnotationsScenario(desc string, url string, routePattern string, rol
 		defer bus.ClearBusHandlers()
 
 		sc := setupScenarioContext(url)
-		sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
 			sc.context = c
 			sc.context.UserId = TestUserID
 			sc.context.OrgId = TestOrgID

+ 136 - 147
pkg/api/api.go

@@ -9,9 +9,7 @@ import (
 	m "github.com/grafana/grafana/pkg/models"
 )
 
-// Register adds http routes
 func (hs *HTTPServer) registerRoutes() {
-	macaronR := hs.macaron
 	reqSignedIn := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true})
 	reqGrafanaAdmin := middleware.Auth(&middleware.AuthOptions{ReqSignedIn: true, ReqGrafanaAdmin: true})
 	reqEditorRole := middleware.RoleAuth(m.ROLE_EDITOR, m.ROLE_ADMIN)
@@ -21,15 +19,12 @@ func (hs *HTTPServer) registerRoutes() {
 	quota := middleware.Quota
 	bind := binding.Bind
 
-	// automatically set HEAD for every GET
-	macaronR.SetAutoHead(true)
-
 	r := hs.RouteRegister
 
 	// not logged in views
 	r.Get("/", reqSignedIn, Index)
 	r.Get("/logout", Logout)
-	r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), wrap(LoginPost))
+	r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), Wrap(LoginPost))
 	r.Get("/login/:name", quota("session"), OAuthLogin)
 	r.Get("/login", LoginView)
 	r.Get("/invite/:code", Index)
@@ -88,20 +83,20 @@ func (hs *HTTPServer) registerRoutes() {
 
 	// sign up
 	r.Get("/signup", Index)
-	r.Get("/api/user/signup/options", wrap(GetSignUpOptions))
-	r.Post("/api/user/signup", quota("user"), bind(dtos.SignUpForm{}), wrap(SignUp))
-	r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), wrap(SignUpStep2))
+	r.Get("/api/user/signup/options", Wrap(GetSignUpOptions))
+	r.Post("/api/user/signup", quota("user"), bind(dtos.SignUpForm{}), Wrap(SignUp))
+	r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), Wrap(SignUpStep2))
 
 	// invited
-	r.Get("/api/user/invite/:code", wrap(GetInviteInfoByCode))
-	r.Post("/api/user/invite/complete", bind(dtos.CompleteInviteForm{}), wrap(CompleteInvite))
+	r.Get("/api/user/invite/:code", Wrap(GetInviteInfoByCode))
+	r.Post("/api/user/invite/complete", bind(dtos.CompleteInviteForm{}), Wrap(CompleteInvite))
 
 	// reset password
 	r.Get("/user/password/send-reset-email", Index)
 	r.Get("/user/password/reset", Index)
 
-	r.Post("/api/user/password/send-reset-email", bind(dtos.SendResetPasswordEmailForm{}), wrap(SendResetPasswordEmail))
-	r.Post("/api/user/password/reset", bind(dtos.ResetUserPasswordForm{}), wrap(ResetPassword))
+	r.Post("/api/user/password/send-reset-email", bind(dtos.SendResetPasswordEmailForm{}), Wrap(SendResetPasswordEmail))
+	r.Post("/api/user/password/reset", bind(dtos.ResetUserPasswordForm{}), Wrap(ResetPassword))
 
 	// dashboard snapshots
 	r.Get("/dashboard/snapshot/*", Index)
@@ -111,8 +106,8 @@ func (hs *HTTPServer) registerRoutes() {
 	r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
 	r.Get("/api/snapshot/shared-options/", GetSharingOptions)
 	r.Get("/api/snapshots/:key", GetDashboardSnapshot)
-	r.Get("/api/snapshots-delete/:deleteKey", wrap(DeleteDashboardSnapshotByDeleteKey))
-	r.Delete("/api/snapshots/:key", reqEditorRole, wrap(DeleteDashboardSnapshot))
+	r.Get("/api/snapshots-delete/:deleteKey", Wrap(DeleteDashboardSnapshotByDeleteKey))
+	r.Delete("/api/snapshots/:key", reqEditorRole, Wrap(DeleteDashboardSnapshot))
 
 	// api renew session based on remember cookie
 	r.Get("/api/login/ping", quota("session"), LoginAPIPing)
@@ -122,138 +117,138 @@ func (hs *HTTPServer) registerRoutes() {
 
 		// user (signed in)
 		apiRoute.Group("/user", func(userRoute routing.RouteRegister) {
-			userRoute.Get("/", wrap(GetSignedInUser))
-			userRoute.Put("/", bind(m.UpdateUserCommand{}), wrap(UpdateSignedInUser))
-			userRoute.Post("/using/:id", wrap(UserSetUsingOrg))
-			userRoute.Get("/orgs", wrap(GetSignedInUserOrgList))
+			userRoute.Get("/", Wrap(GetSignedInUser))
+			userRoute.Put("/", bind(m.UpdateUserCommand{}), Wrap(UpdateSignedInUser))
+			userRoute.Post("/using/:id", Wrap(UserSetUsingOrg))
+			userRoute.Get("/orgs", Wrap(GetSignedInUserOrgList))
 
-			userRoute.Post("/stars/dashboard/:id", wrap(StarDashboard))
-			userRoute.Delete("/stars/dashboard/:id", wrap(UnstarDashboard))
+			userRoute.Post("/stars/dashboard/:id", Wrap(StarDashboard))
+			userRoute.Delete("/stars/dashboard/:id", Wrap(UnstarDashboard))
 
-			userRoute.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword))
-			userRoute.Get("/quotas", wrap(GetUserQuotas))
-			userRoute.Put("/helpflags/:id", wrap(SetHelpFlag))
+			userRoute.Put("/password", bind(m.ChangeUserPasswordCommand{}), Wrap(ChangeUserPassword))
+			userRoute.Get("/quotas", Wrap(GetUserQuotas))
+			userRoute.Put("/helpflags/:id", Wrap(SetHelpFlag))
 			// For dev purpose
-			userRoute.Get("/helpflags/clear", wrap(ClearHelpFlags))
+			userRoute.Get("/helpflags/clear", Wrap(ClearHelpFlags))
 
-			userRoute.Get("/preferences", wrap(GetUserPreferences))
-			userRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateUserPreferences))
+			userRoute.Get("/preferences", Wrap(GetUserPreferences))
+			userRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateUserPreferences))
 		})
 
 		// users (admin permission required)
 		apiRoute.Group("/users", func(usersRoute routing.RouteRegister) {
-			usersRoute.Get("/", wrap(SearchUsers))
-			usersRoute.Get("/search", wrap(SearchUsersWithPaging))
-			usersRoute.Get("/:id", wrap(GetUserByID))
-			usersRoute.Get("/:id/orgs", wrap(GetUserOrgList))
+			usersRoute.Get("/", Wrap(SearchUsers))
+			usersRoute.Get("/search", Wrap(SearchUsersWithPaging))
+			usersRoute.Get("/:id", Wrap(GetUserByID))
+			usersRoute.Get("/:id/orgs", Wrap(GetUserOrgList))
 			// query parameters /users/lookup?loginOrEmail=admin@example.com
-			usersRoute.Get("/lookup", wrap(GetUserByLoginOrEmail))
-			usersRoute.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser))
-			usersRoute.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg))
+			usersRoute.Get("/lookup", Wrap(GetUserByLoginOrEmail))
+			usersRoute.Put("/:id", bind(m.UpdateUserCommand{}), Wrap(UpdateUser))
+			usersRoute.Post("/:id/using/:orgId", Wrap(UpdateUserActiveOrg))
 		}, reqGrafanaAdmin)
 
 		// team (admin permission required)
 		apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
-			teamsRoute.Post("/", bind(m.CreateTeamCommand{}), wrap(CreateTeam))
-			teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam))
-			teamsRoute.Delete("/:teamId", wrap(DeleteTeamByID))
-			teamsRoute.Get("/:teamId/members", wrap(GetTeamMembers))
-			teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember))
-			teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember))
+			teamsRoute.Post("/", bind(m.CreateTeamCommand{}), Wrap(CreateTeam))
+			teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), Wrap(UpdateTeam))
+			teamsRoute.Delete("/:teamId", Wrap(DeleteTeamByID))
+			teamsRoute.Get("/:teamId/members", Wrap(GetTeamMembers))
+			teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), Wrap(AddTeamMember))
+			teamsRoute.Delete("/:teamId/members/:userId", Wrap(RemoveTeamMember))
 		}, reqOrgAdmin)
 
 		// team without requirement of user to be org admin
 		apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
-			teamsRoute.Get("/:teamId", wrap(GetTeamByID))
-			teamsRoute.Get("/search", wrap(SearchTeams))
+			teamsRoute.Get("/:teamId", Wrap(GetTeamByID))
+			teamsRoute.Get("/search", Wrap(SearchTeams))
 		})
 
 		// org information available to all users.
 		apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
-			orgRoute.Get("/", wrap(GetOrgCurrent))
-			orgRoute.Get("/quotas", wrap(GetOrgQuotas))
+			orgRoute.Get("/", Wrap(GetOrgCurrent))
+			orgRoute.Get("/quotas", Wrap(GetOrgQuotas))
 		})
 
 		// current org
 		apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
-			orgRoute.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrgCurrent))
-			orgRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddressCurrent))
-			orgRoute.Post("/users", quota("user"), bind(m.AddOrgUserCommand{}), wrap(AddOrgUserToCurrentOrg))
-			orgRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUserForCurrentOrg))
-			orgRoute.Delete("/users/:userId", wrap(RemoveOrgUserForCurrentOrg))
+			orgRoute.Put("/", bind(dtos.UpdateOrgForm{}), Wrap(UpdateOrgCurrent))
+			orgRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), Wrap(UpdateOrgAddressCurrent))
+			orgRoute.Post("/users", quota("user"), bind(m.AddOrgUserCommand{}), Wrap(AddOrgUserToCurrentOrg))
+			orgRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), Wrap(UpdateOrgUserForCurrentOrg))
+			orgRoute.Delete("/users/:userId", Wrap(RemoveOrgUserForCurrentOrg))
 
 			// invites
-			orgRoute.Get("/invites", wrap(GetPendingOrgInvites))
-			orgRoute.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite))
-			orgRoute.Patch("/invites/:code/revoke", wrap(RevokeInvite))
+			orgRoute.Get("/invites", Wrap(GetPendingOrgInvites))
+			orgRoute.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), Wrap(AddOrgInvite))
+			orgRoute.Patch("/invites/:code/revoke", Wrap(RevokeInvite))
 
 			// prefs
-			orgRoute.Get("/preferences", wrap(GetOrgPreferences))
-			orgRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateOrgPreferences))
+			orgRoute.Get("/preferences", Wrap(GetOrgPreferences))
+			orgRoute.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateOrgPreferences))
 		}, reqOrgAdmin)
 
 		// current org without requirement of user to be org admin
 		apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
-			orgRoute.Get("/users", wrap(GetOrgUsersForCurrentOrg))
+			orgRoute.Get("/users", Wrap(GetOrgUsersForCurrentOrg))
 		})
 
 		// create new org
-		apiRoute.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), wrap(CreateOrg))
+		apiRoute.Post("/orgs", quota("org"), bind(m.CreateOrgCommand{}), Wrap(CreateOrg))
 
 		// search all orgs
-		apiRoute.Get("/orgs", reqGrafanaAdmin, wrap(SearchOrgs))
+		apiRoute.Get("/orgs", reqGrafanaAdmin, Wrap(SearchOrgs))
 
 		// orgs (admin routes)
 		apiRoute.Group("/orgs/:orgId", func(orgsRoute routing.RouteRegister) {
-			orgsRoute.Get("/", wrap(GetOrgByID))
-			orgsRoute.Put("/", bind(dtos.UpdateOrgForm{}), wrap(UpdateOrg))
-			orgsRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), wrap(UpdateOrgAddress))
-			orgsRoute.Delete("/", wrap(DeleteOrgByID))
-			orgsRoute.Get("/users", wrap(GetOrgUsers))
-			orgsRoute.Post("/users", bind(m.AddOrgUserCommand{}), wrap(AddOrgUser))
-			orgsRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), wrap(UpdateOrgUser))
-			orgsRoute.Delete("/users/:userId", wrap(RemoveOrgUser))
-			orgsRoute.Get("/quotas", wrap(GetOrgQuotas))
-			orgsRoute.Put("/quotas/:target", bind(m.UpdateOrgQuotaCmd{}), wrap(UpdateOrgQuota))
+			orgsRoute.Get("/", Wrap(GetOrgByID))
+			orgsRoute.Put("/", bind(dtos.UpdateOrgForm{}), Wrap(UpdateOrg))
+			orgsRoute.Put("/address", bind(dtos.UpdateOrgAddressForm{}), Wrap(UpdateOrgAddress))
+			orgsRoute.Delete("/", Wrap(DeleteOrgByID))
+			orgsRoute.Get("/users", Wrap(GetOrgUsers))
+			orgsRoute.Post("/users", bind(m.AddOrgUserCommand{}), Wrap(AddOrgUser))
+			orgsRoute.Patch("/users/:userId", bind(m.UpdateOrgUserCommand{}), Wrap(UpdateOrgUser))
+			orgsRoute.Delete("/users/:userId", Wrap(RemoveOrgUser))
+			orgsRoute.Get("/quotas", Wrap(GetOrgQuotas))
+			orgsRoute.Put("/quotas/:target", bind(m.UpdateOrgQuotaCmd{}), Wrap(UpdateOrgQuota))
 		}, reqGrafanaAdmin)
 
 		// orgs (admin routes)
 		apiRoute.Group("/orgs/name/:name", func(orgsRoute routing.RouteRegister) {
-			orgsRoute.Get("/", wrap(GetOrgByName))
+			orgsRoute.Get("/", Wrap(GetOrgByName))
 		}, reqGrafanaAdmin)
 
 		// auth api keys
 		apiRoute.Group("/auth/keys", func(keysRoute routing.RouteRegister) {
-			keysRoute.Get("/", wrap(GetAPIKeys))
-			keysRoute.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), wrap(AddAPIKey))
-			keysRoute.Delete("/:id", wrap(DeleteAPIKey))
+			keysRoute.Get("/", Wrap(GetAPIKeys))
+			keysRoute.Post("/", quota("api_key"), bind(m.AddApiKeyCommand{}), Wrap(AddAPIKey))
+			keysRoute.Delete("/:id", Wrap(DeleteAPIKey))
 		}, reqOrgAdmin)
 
 		// Preferences
 		apiRoute.Group("/preferences", func(prefRoute routing.RouteRegister) {
-			prefRoute.Post("/set-home-dash", bind(m.SavePreferencesCommand{}), wrap(SetHomeDashboard))
+			prefRoute.Post("/set-home-dash", bind(m.SavePreferencesCommand{}), Wrap(SetHomeDashboard))
 		})
 
 		// Data sources
 		apiRoute.Group("/datasources", func(datasourceRoute routing.RouteRegister) {
-			datasourceRoute.Get("/", wrap(GetDataSources))
-			datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), wrap(AddDataSource))
-			datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource))
-			datasourceRoute.Delete("/:id", wrap(DeleteDataSourceByID))
-			datasourceRoute.Delete("/name/:name", wrap(DeleteDataSourceByName))
-			datasourceRoute.Get("/:id", wrap(GetDataSourceByID))
-			datasourceRoute.Get("/name/:name", wrap(GetDataSourceByName))
+			datasourceRoute.Get("/", Wrap(GetDataSources))
+			datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), Wrap(AddDataSource))
+			datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), Wrap(UpdateDataSource))
+			datasourceRoute.Delete("/:id", Wrap(DeleteDataSourceByID))
+			datasourceRoute.Delete("/name/:name", Wrap(DeleteDataSourceByName))
+			datasourceRoute.Get("/:id", Wrap(GetDataSourceByID))
+			datasourceRoute.Get("/name/:name", Wrap(GetDataSourceByName))
 		}, reqOrgAdmin)
 
-		apiRoute.Get("/datasources/id/:name", wrap(GetDataSourceIDByName), reqSignedIn)
+		apiRoute.Get("/datasources/id/:name", Wrap(GetDataSourceIDByName), reqSignedIn)
 
-		apiRoute.Get("/plugins", wrap(GetPluginList))
-		apiRoute.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingByID))
-		apiRoute.Get("/plugins/:pluginId/markdown/:name", wrap(GetPluginMarkdown))
+		apiRoute.Get("/plugins", Wrap(GetPluginList))
+		apiRoute.Get("/plugins/:pluginId/settings", Wrap(GetPluginSettingByID))
+		apiRoute.Get("/plugins/:pluginId/markdown/:name", Wrap(GetPluginMarkdown))
 
 		apiRoute.Group("/plugins", func(pluginRoute routing.RouteRegister) {
-			pluginRoute.Get("/:pluginId/dashboards/", wrap(GetPluginDashboards))
-			pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), wrap(UpdatePluginSetting))
+			pluginRoute.Get("/:pluginId/dashboards/", Wrap(GetPluginDashboards))
+			pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), Wrap(UpdatePluginSetting))
 		}, reqOrgAdmin)
 
 		apiRoute.Get("/frontend/settings/", GetFrontendSettings)
@@ -262,106 +257,106 @@ func (hs *HTTPServer) registerRoutes() {
 
 		// Folders
 		apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
-			folderRoute.Get("/", wrap(GetFolders))
-			folderRoute.Get("/id/:id", wrap(GetFolderByID))
-			folderRoute.Post("/", bind(m.CreateFolderCommand{}), wrap(CreateFolder))
+			folderRoute.Get("/", Wrap(GetFolders))
+			folderRoute.Get("/id/:id", Wrap(GetFolderByID))
+			folderRoute.Post("/", bind(m.CreateFolderCommand{}), Wrap(CreateFolder))
 
 			folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
-				folderUidRoute.Get("/", wrap(GetFolderByUID))
-				folderUidRoute.Put("/", bind(m.UpdateFolderCommand{}), wrap(UpdateFolder))
-				folderUidRoute.Delete("/", wrap(DeleteFolder))
+				folderUidRoute.Get("/", Wrap(GetFolderByUID))
+				folderUidRoute.Put("/", bind(m.UpdateFolderCommand{}), Wrap(UpdateFolder))
+				folderUidRoute.Delete("/", Wrap(DeleteFolder))
 
 				folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) {
-					folderPermissionRoute.Get("/", wrap(GetFolderPermissionList))
-					folderPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateFolderPermissions))
+					folderPermissionRoute.Get("/", Wrap(GetFolderPermissionList))
+					folderPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), Wrap(UpdateFolderPermissions))
 				})
 			})
 		})
 
 		// Dashboard
 		apiRoute.Group("/dashboards", func(dashboardRoute routing.RouteRegister) {
-			dashboardRoute.Get("/uid/:uid", wrap(GetDashboard))
-			dashboardRoute.Delete("/uid/:uid", wrap(DeleteDashboardByUID))
+			dashboardRoute.Get("/uid/:uid", Wrap(GetDashboard))
+			dashboardRoute.Delete("/uid/:uid", Wrap(DeleteDashboardByUID))
 
-			dashboardRoute.Get("/db/:slug", wrap(GetDashboard))
-			dashboardRoute.Delete("/db/:slug", wrap(DeleteDashboard))
+			dashboardRoute.Get("/db/:slug", Wrap(GetDashboard))
+			dashboardRoute.Delete("/db/:slug", Wrap(DeleteDashboard))
 
-			dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), wrap(CalculateDashboardDiff))
+			dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), Wrap(CalculateDashboardDiff))
 
-			dashboardRoute.Post("/db", bind(m.SaveDashboardCommand{}), wrap(PostDashboard))
-			dashboardRoute.Get("/home", wrap(GetHomeDashboard))
+			dashboardRoute.Post("/db", bind(m.SaveDashboardCommand{}), Wrap(PostDashboard))
+			dashboardRoute.Get("/home", Wrap(GetHomeDashboard))
 			dashboardRoute.Get("/tags", GetDashboardTags)
-			dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard))
+			dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), Wrap(ImportDashboard))
 
 			dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute routing.RouteRegister) {
-				dashIdRoute.Get("/versions", wrap(GetDashboardVersions))
-				dashIdRoute.Get("/versions/:id", wrap(GetDashboardVersion))
-				dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
+				dashIdRoute.Get("/versions", Wrap(GetDashboardVersions))
+				dashIdRoute.Get("/versions/:id", Wrap(GetDashboardVersion))
+				dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), Wrap(RestoreDashboardVersion))
 
 				dashIdRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) {
-					dashboardPermissionRoute.Get("/", wrap(GetDashboardPermissionList))
-					dashboardPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardPermissions))
+					dashboardPermissionRoute.Get("/", Wrap(GetDashboardPermissionList))
+					dashboardPermissionRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), Wrap(UpdateDashboardPermissions))
 				})
 			})
 		})
 
 		// Dashboard snapshots
 		apiRoute.Group("/dashboard/snapshots", func(dashboardRoute routing.RouteRegister) {
-			dashboardRoute.Get("/", wrap(SearchDashboardSnapshots))
+			dashboardRoute.Get("/", Wrap(SearchDashboardSnapshots))
 		})
 
 		// Playlist
 		apiRoute.Group("/playlists", func(playlistRoute routing.RouteRegister) {
-			playlistRoute.Get("/", wrap(SearchPlaylists))
-			playlistRoute.Get("/:id", ValidateOrgPlaylist, wrap(GetPlaylist))
-			playlistRoute.Get("/:id/items", ValidateOrgPlaylist, wrap(GetPlaylistItems))
-			playlistRoute.Get("/:id/dashboards", ValidateOrgPlaylist, wrap(GetPlaylistDashboards))
-			playlistRoute.Delete("/:id", reqEditorRole, ValidateOrgPlaylist, wrap(DeletePlaylist))
-			playlistRoute.Put("/:id", reqEditorRole, bind(m.UpdatePlaylistCommand{}), ValidateOrgPlaylist, wrap(UpdatePlaylist))
-			playlistRoute.Post("/", reqEditorRole, bind(m.CreatePlaylistCommand{}), wrap(CreatePlaylist))
+			playlistRoute.Get("/", Wrap(SearchPlaylists))
+			playlistRoute.Get("/:id", ValidateOrgPlaylist, Wrap(GetPlaylist))
+			playlistRoute.Get("/:id/items", ValidateOrgPlaylist, Wrap(GetPlaylistItems))
+			playlistRoute.Get("/:id/dashboards", ValidateOrgPlaylist, Wrap(GetPlaylistDashboards))
+			playlistRoute.Delete("/:id", reqEditorRole, ValidateOrgPlaylist, Wrap(DeletePlaylist))
+			playlistRoute.Put("/:id", reqEditorRole, bind(m.UpdatePlaylistCommand{}), ValidateOrgPlaylist, Wrap(UpdatePlaylist))
+			playlistRoute.Post("/", reqEditorRole, bind(m.CreatePlaylistCommand{}), Wrap(CreatePlaylist))
 		})
 
 		// Search
 		apiRoute.Get("/search/", Search)
 
 		// metrics
-		apiRoute.Post("/tsdb/query", bind(dtos.MetricRequest{}), wrap(QueryMetrics))
-		apiRoute.Get("/tsdb/testdata/scenarios", wrap(GetTestDataScenarios))
-		apiRoute.Get("/tsdb/testdata/gensql", reqGrafanaAdmin, wrap(GenerateSQLTestData))
-		apiRoute.Get("/tsdb/testdata/random-walk", wrap(GetTestDataRandomWalk))
+		apiRoute.Post("/tsdb/query", bind(dtos.MetricRequest{}), Wrap(QueryMetrics))
+		apiRoute.Get("/tsdb/testdata/scenarios", Wrap(GetTestDataScenarios))
+		apiRoute.Get("/tsdb/testdata/gensql", reqGrafanaAdmin, Wrap(GenerateSQLTestData))
+		apiRoute.Get("/tsdb/testdata/random-walk", Wrap(GetTestDataRandomWalk))
 
 		apiRoute.Group("/alerts", func(alertsRoute routing.RouteRegister) {
-			alertsRoute.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
-			alertsRoute.Post("/:alertId/pause", reqEditorRole, bind(dtos.PauseAlertCommand{}), wrap(PauseAlert))
-			alertsRoute.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
-			alertsRoute.Get("/", wrap(GetAlerts))
-			alertsRoute.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard))
+			alertsRoute.Post("/test", bind(dtos.AlertTestCommand{}), Wrap(AlertTest))
+			alertsRoute.Post("/:alertId/pause", reqEditorRole, bind(dtos.PauseAlertCommand{}), Wrap(PauseAlert))
+			alertsRoute.Get("/:alertId", ValidateOrgAlert, Wrap(GetAlert))
+			alertsRoute.Get("/", Wrap(GetAlerts))
+			alertsRoute.Get("/states-for-dashboard", Wrap(GetAlertStatesForDashboard))
 		})
 
-		apiRoute.Get("/alert-notifications", wrap(GetAlertNotifications))
-		apiRoute.Get("/alert-notifiers", wrap(GetAlertNotifiers))
+		apiRoute.Get("/alert-notifications", Wrap(GetAlertNotifications))
+		apiRoute.Get("/alert-notifiers", Wrap(GetAlertNotifiers))
 
 		apiRoute.Group("/alert-notifications", func(alertNotifications routing.RouteRegister) {
-			alertNotifications.Post("/test", bind(dtos.NotificationTestCommand{}), wrap(NotificationTest))
-			alertNotifications.Post("/", bind(m.CreateAlertNotificationCommand{}), wrap(CreateAlertNotification))
-			alertNotifications.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), wrap(UpdateAlertNotification))
-			alertNotifications.Get("/:notificationId", wrap(GetAlertNotificationByID))
-			alertNotifications.Delete("/:notificationId", wrap(DeleteAlertNotification))
+			alertNotifications.Post("/test", bind(dtos.NotificationTestCommand{}), Wrap(NotificationTest))
+			alertNotifications.Post("/", bind(m.CreateAlertNotificationCommand{}), Wrap(CreateAlertNotification))
+			alertNotifications.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), Wrap(UpdateAlertNotification))
+			alertNotifications.Get("/:notificationId", Wrap(GetAlertNotificationByID))
+			alertNotifications.Delete("/:notificationId", Wrap(DeleteAlertNotification))
 		}, reqEditorRole)
 
-		apiRoute.Get("/annotations", wrap(GetAnnotations))
-		apiRoute.Post("/annotations/mass-delete", reqOrgAdmin, bind(dtos.DeleteAnnotationsCmd{}), wrap(DeleteAnnotations))
+		apiRoute.Get("/annotations", Wrap(GetAnnotations))
+		apiRoute.Post("/annotations/mass-delete", reqOrgAdmin, bind(dtos.DeleteAnnotationsCmd{}), Wrap(DeleteAnnotations))
 
 		apiRoute.Group("/annotations", func(annotationsRoute routing.RouteRegister) {
-			annotationsRoute.Post("/", bind(dtos.PostAnnotationsCmd{}), wrap(PostAnnotation))
-			annotationsRoute.Delete("/:annotationId", wrap(DeleteAnnotationByID))
-			annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), wrap(UpdateAnnotation))
-			annotationsRoute.Delete("/region/:regionId", wrap(DeleteAnnotationRegion))
-			annotationsRoute.Post("/graphite", reqEditorRole, bind(dtos.PostGraphiteAnnotationsCmd{}), wrap(PostGraphiteAnnotation))
+			annotationsRoute.Post("/", bind(dtos.PostAnnotationsCmd{}), Wrap(PostAnnotation))
+			annotationsRoute.Delete("/:annotationId", Wrap(DeleteAnnotationByID))
+			annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), Wrap(UpdateAnnotation))
+			annotationsRoute.Delete("/region/:regionId", Wrap(DeleteAnnotationRegion))
+			annotationsRoute.Post("/graphite", reqEditorRole, bind(dtos.PostGraphiteAnnotationsCmd{}), Wrap(PostGraphiteAnnotation))
 		})
 
 		// error test
-		r.Get("/metrics/error", wrap(GenerateError))
+		r.Get("/metrics/error", Wrap(GenerateError))
 
 	}, reqSignedIn)
 
@@ -372,10 +367,10 @@ func (hs *HTTPServer) registerRoutes() {
 		adminRoute.Put("/users/:id/password", bind(dtos.AdminUpdateUserPasswordForm{}), AdminUpdateUserPassword)
 		adminRoute.Put("/users/:id/permissions", bind(dtos.AdminUpdateUserPermissionsForm{}), AdminUpdateUserPermissions)
 		adminRoute.Delete("/users/:id", AdminDeleteUser)
-		adminRoute.Get("/users/:id/quotas", wrap(GetUserQuotas))
-		adminRoute.Put("/users/:id/quotas/:target", bind(m.UpdateUserQuotaCmd{}), wrap(UpdateUserQuota))
+		adminRoute.Get("/users/:id/quotas", Wrap(GetUserQuotas))
+		adminRoute.Put("/users/:id/quotas/:target", bind(m.UpdateUserQuotaCmd{}), Wrap(UpdateUserQuota))
 		adminRoute.Get("/stats", AdminGetStats)
-		adminRoute.Post("/pause-all-alerts", bind(dtos.PauseAllAlertsCommand{}), wrap(PauseAllAlerts))
+		adminRoute.Post("/pause-all-alerts", bind(dtos.PauseAllAlertsCommand{}), Wrap(PauseAllAlerts))
 	}, reqGrafanaAdmin)
 
 	// rendering
@@ -393,10 +388,4 @@ func (hs *HTTPServer) registerRoutes() {
 
 	// streams
 	//r.Post("/api/streams/push", reqSignedIn, bind(dtos.StreamMessage{}), liveConn.PushToStream)
-
-	r.Register(macaronR)
-
-	InitAppPluginRoutes(macaronR)
-
-	macaronR.NotFound(NotFoundHandler)
 }

+ 1 - 1
pkg/api/app_routes.go

@@ -18,7 +18,7 @@ import (
 
 var pluginProxyTransport *http.Transport
 
-func InitAppPluginRoutes(r *macaron.Macaron) {
+func (hs *HTTPServer) initAppPluginRoutes(r *macaron.Macaron) {
 	pluginProxyTransport = &http.Transport{
 		TLSClientConfig: &tls.Config{
 			InsecureSkipVerify: setting.PluginAppsSkipVerifyTLS,

+ 1 - 1
pkg/api/common.go

@@ -30,7 +30,7 @@ type NormalResponse struct {
 	err        error
 }
 
-func wrap(action interface{}) macaron.Handler {
+func Wrap(action interface{}) macaron.Handler {
 
 	return func(c *m.ReqContext) {
 		var res Response

+ 2 - 2
pkg/api/common_test.go

@@ -23,7 +23,7 @@ func loggedInUserScenarioWithRole(desc string, method string, url string, routeP
 		defer bus.ClearBusHandlers()
 
 		sc := setupScenarioContext(url)
-		sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
 			sc.context = c
 			sc.context.UserId = TestUserID
 			sc.context.OrgId = TestOrgID
@@ -51,7 +51,7 @@ func anonymousUserScenario(desc string, method string, url string, routePattern
 		defer bus.ClearBusHandlers()
 
 		sc := setupScenarioContext(url)
-		sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
 			sc.context = c
 			if sc.handlerFunc != nil {
 				return sc.handlerFunc(sc.context)

+ 1 - 1
pkg/api/dashboard_permission_test.go

@@ -194,7 +194,7 @@ func updateDashboardPermissionScenario(desc string, url string, routePattern str
 
 		sc := setupScenarioContext(url)
 
-		sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
 			sc.context = c
 			sc.context.OrgId = TestOrgID
 			sc.context.UserId = TestUserID

+ 2 - 2
pkg/api/dashboard_test.go

@@ -882,7 +882,7 @@ func postDashboardScenario(desc string, url string, routePattern string, mock *d
 		defer bus.ClearBusHandlers()
 
 		sc := setupScenarioContext(url)
-		sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
 			sc.context = c
 			sc.context.SignedInUser = &m.SignedInUser{OrgId: cmd.OrgId, UserId: cmd.UserId}
 
@@ -907,7 +907,7 @@ func postDiffScenario(desc string, url string, routePattern string, cmd dtos.Cal
 		defer bus.ClearBusHandlers()
 
 		sc := setupScenarioContext(url)
-		sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
 			sc.context = c
 			sc.context.SignedInUser = &m.SignedInUser{
 				OrgId:  TestOrgID,

+ 1 - 0
pkg/api/dtos/index.go

@@ -13,6 +13,7 @@ type IndexViewData struct {
 	Theme                   string
 	NewGrafanaVersionExists bool
 	NewGrafanaVersion       string
+	AppName                 string
 }
 
 type PluginCss struct {

+ 1 - 1
pkg/api/folder_permission_test.go

@@ -226,7 +226,7 @@ func updateFolderPermissionScenario(desc string, url string, routePattern string
 
 		sc := setupScenarioContext(url)
 
-		sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
 			sc.context = c
 			sc.context.OrgId = TestOrgID
 			sc.context.UserId = TestUserID

+ 2 - 2
pkg/api/folder_test.go

@@ -152,7 +152,7 @@ func createFolderScenario(desc string, url string, routePattern string, mock *fa
 		defer bus.ClearBusHandlers()
 
 		sc := setupScenarioContext(url)
-		sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
 			sc.context = c
 			sc.context.SignedInUser = &m.SignedInUser{OrgId: TestOrgID, UserId: TestUserID}
 
@@ -181,7 +181,7 @@ func updateFolderScenario(desc string, url string, routePattern string, mock *fa
 		defer bus.ClearBusHandlers()
 
 		sc := setupScenarioContext(url)
-		sc.defaultHandler = wrap(func(c *m.ReqContext) Response {
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
 			sc.context = c
 			sc.context.SignedInUser = &m.SignedInUser{OrgId: TestOrgID, UserId: TestUserID}
 

+ 1 - 0
pkg/api/frontendsettings.go

@@ -153,6 +153,7 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
 			"latestVersion": plugins.GrafanaLatestVersion,
 			"hasUpdate":     plugins.GrafanaHasUpdate,
 			"env":           setting.Env,
+			"isEnterprise":  setting.IsEnterprise,
 		},
 	}
 

+ 31 - 7
pkg/api/http_server.go

@@ -33,7 +33,11 @@ import (
 )
 
 func init() {
-	registry.RegisterService(&HTTPServer{})
+	registry.Register(&registry.Descriptor{
+		Name:         "HTTPServer",
+		Instance:     &HTTPServer{},
+		InitPriority: registry.High,
+	})
 }
 
 type HTTPServer struct {
@@ -54,6 +58,10 @@ func (hs *HTTPServer) Init() error {
 	hs.log = log.New("http.server")
 	hs.cache = gocache.New(5*time.Minute, 10*time.Minute)
 
+	hs.streamManager = live.NewStreamManager()
+	hs.macaron = hs.newMacaron()
+	hs.registerRoutes()
+
 	return nil
 }
 
@@ -61,10 +69,8 @@ func (hs *HTTPServer) Run(ctx context.Context) error {
 	var err error
 
 	hs.context = ctx
-	hs.streamManager = live.NewStreamManager()
-	hs.macaron = hs.newMacaron()
-	hs.registerRoutes()
 
+	hs.applyRoutes()
 	hs.streamManager.Run(ctx)
 
 	listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
@@ -164,6 +170,26 @@ func (hs *HTTPServer) newMacaron() *macaron.Macaron {
 	macaron.Env = setting.Env
 	m := macaron.New()
 
+	// automatically set HEAD for every GET
+	m.SetAutoHead(true)
+
+	return m
+}
+
+func (hs *HTTPServer) applyRoutes() {
+	// start with middlewares & static routes
+	hs.addMiddlewaresAndStaticRoutes()
+	// then add view routes & api routes
+	hs.RouteRegister.Register(hs.macaron)
+	// then custom app proxy routes
+	hs.initAppPluginRoutes(hs.macaron)
+	// lastly not found route
+	hs.macaron.NotFound(NotFoundHandler)
+}
+
+func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
+	m := hs.macaron
+
 	m.Use(middleware.Logger())
 
 	if setting.EnableGzip {
@@ -175,7 +201,7 @@ func (hs *HTTPServer) newMacaron() *macaron.Macaron {
 	for _, route := range plugins.StaticRoutes {
 		pluginRoute := path.Join("/public/plugins/", route.PluginId)
 		hs.log.Debug("Plugins: Adding route", "route", pluginRoute, "dir", route.Directory)
-		hs.mapStatic(m, route.Directory, "", pluginRoute)
+		hs.mapStatic(hs.macaron, route.Directory, "", pluginRoute)
 	}
 
 	hs.mapStatic(m, setting.StaticRootPath, "build", "public/build")
@@ -204,8 +230,6 @@ func (hs *HTTPServer) newMacaron() *macaron.Macaron {
 	}
 
 	m.Use(middleware.AddDefaultResponseHeaders())
-
-	return m
 }
 
 func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) {

+ 1 - 0
pkg/api/index.go

@@ -76,6 +76,7 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
 		BuildCommit:             setting.BuildCommit,
 		NewGrafanaVersion:       plugins.GrafanaLatestVersion,
 		NewGrafanaVersionExists: plugins.GrafanaHasUpdate,
+		AppName:                 setting.ApplicationName,
 	}
 
 	if setting.DisableGravatar {

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

@@ -18,7 +18,7 @@ import (
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/setting"
 
-	_ "github.com/grafana/grafana/pkg/extensions"
+	extensions "github.com/grafana/grafana/pkg/extensions"
 	_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
 	_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
 	_ "github.com/grafana/grafana/pkg/tsdb/cloudwatch"
@@ -35,7 +35,6 @@ import (
 var version = "5.0.0"
 var commit = "NA"
 var buildstamp string
-var enterprise string
 
 var configFile = flag.String("config", "", "path to config file")
 var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory")
@@ -78,7 +77,7 @@ func main() {
 	setting.BuildVersion = version
 	setting.BuildCommit = commit
 	setting.BuildStamp = buildstampInt64
-	setting.Enterprise, _ = strconv.ParseBool(enterprise)
+	setting.IsEnterprise = extensions.IsEnterprise
 
 	metrics.M_Grafana_Version.WithLabelValues(version).Set(1)
 

+ 1 - 1
pkg/extensions/main.go

@@ -1,3 +1,3 @@
 package extensions
 
-import _ "github.com/pkg/errors"
+var IsEnterprise bool = false

+ 17 - 1
pkg/login/ext_user.go

@@ -21,6 +21,7 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
 		Email:      extUser.Email,
 		Login:      extUser.Login,
 	}
+
 	err := bus.Dispatch(userQuery)
 	if err != m.ErrUserNotFound && err != nil {
 		return err
@@ -66,7 +67,21 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
 		}
 	}
 
-	return syncOrgRoles(cmd.Result, extUser)
+	err = syncOrgRoles(cmd.Result, extUser)
+	if err != nil {
+		return err
+	}
+
+	err = bus.Dispatch(&m.SyncTeamsCommand{
+		User:         cmd.Result,
+		ExternalUser: extUser,
+	})
+
+	if err == bus.ErrHandlerNotFound {
+		return nil
+	}
+
+	return err
 }
 
 func createUser(extUser *m.ExternalUserInfo) (*m.User, error) {
@@ -76,6 +91,7 @@ func createUser(extUser *m.ExternalUserInfo) (*m.User, error) {
 		Name:         extUser.Name,
 		SkipOrgSetup: len(extUser.OrgRoles) > 0,
 	}
+
 	if err := bus.Dispatch(cmd); err != nil {
 		return nil, err
 	}

+ 2 - 0
pkg/login/ldap.go

@@ -163,6 +163,7 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
 		Name:       fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName),
 		Login:      ldapUser.Username,
 		Email:      ldapUser.Email,
+		Groups:     ldapUser.MemberOf,
 		OrgRoles:   map[int64]m.RoleType{},
 	}
 
@@ -194,6 +195,7 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
 		ExternalUser:  extUser,
 		SignupAllowed: setting.LdapAllowSignup,
 	}
+
 	err := bus.Dispatch(userQuery)
 	if err != nil {
 		return nil, err

+ 13 - 3
pkg/login/ldap_test.go

@@ -1,6 +1,7 @@
 package login
 
 import (
+	"context"
 	"crypto/tls"
 	"testing"
 
@@ -14,6 +15,14 @@ func TestLdapAuther(t *testing.T) {
 
 	Convey("When translating ldap user to grafana user", t, func() {
 
+		var user1 = &m.User{}
+
+		bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.UpsertUserCommand) error {
+			cmd.Result = user1
+			cmd.Result.Login = "torkelo"
+			return nil
+		})
+
 		Convey("Given no ldap group map match", func() {
 			ldapAuther := NewLdapAuthenticator(&LdapServerConf{
 				LdapGroups: []*LdapGroupToOrgRole{{}},
@@ -23,8 +32,6 @@ func TestLdapAuther(t *testing.T) {
 			So(err, ShouldEqual, ErrInvalidCredentials)
 		})
 
-		var user1 = &m.User{}
-
 		ldapAutherScenario("Given wildcard group match", func(sc *scenarioContext) {
 			ldapAuther := NewLdapAuthenticator(&LdapServerConf{
 				LdapGroups: []*LdapGroupToOrgRole{
@@ -96,7 +103,6 @@ func TestLdapAuther(t *testing.T) {
 	})
 
 	Convey("When syncing ldap groups to grafana org roles", t, func() {
-
 		ldapAutherScenario("given no current user orgs", func(sc *scenarioContext) {
 			ldapAuther := NewLdapAuthenticator(&LdapServerConf{
 				LdapGroups: []*LdapGroupToOrgRole{
@@ -322,6 +328,10 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
 
 		bus.AddHandler("test", UpsertUser)
 
+		bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.SyncTeamsCommand) error {
+			return nil
+		})
+
 		bus.AddHandler("test", func(cmd *m.GetUserByAuthInfoQuery) error {
 			sc.getUserByAuthInfoQuery = cmd
 			sc.getUserByAuthInfoQuery.Result = &m.User{Login: cmd.Login}

+ 9 - 0
pkg/metrics/metrics.go

@@ -334,6 +334,14 @@ func updateTotalStats() {
 
 var usageStatsURL = "https://stats.grafana.org/grafana-usage-report"
 
+func getEdition() string {
+	if setting.IsEnterprise {
+		return "enterprise"
+	} else {
+		return "oss"
+	}
+}
+
 func sendUsageStats() {
 	if !setting.ReportingEnabled {
 		return
@@ -349,6 +357,7 @@ func sendUsageStats() {
 		"metrics": metrics,
 		"os":      runtime.GOOS,
 		"arch":    runtime.GOARCH,
+		"edition": getEdition(),
 	}
 
 	statsQuery := models.GetSystemStatsQuery{}

+ 1 - 0
pkg/models/team_member.go

@@ -42,6 +42,7 @@ type RemoveTeamMemberCommand struct {
 type GetTeamMembersQuery struct {
 	OrgId  int64
 	TeamId int64
+	UserId int64
 	Result []*TeamMemberDTO
 }
 

+ 6 - 0
pkg/models/user_auth.go

@@ -19,6 +19,7 @@ type ExternalUserInfo struct {
 	Email      string
 	Login      string
 	Name       string
+	Groups     []string
 	OrgRoles   map[int64]RoleType
 }
 
@@ -70,3 +71,8 @@ type GetAuthInfoQuery struct {
 
 	Result *UserAuth
 }
+
+type SyncTeamsCommand struct {
+	ExternalUser *ExternalUserInfo
+	User         *User
+}

+ 11 - 1
pkg/registry/registry.go

@@ -4,6 +4,8 @@ import (
 	"context"
 	"reflect"
 	"sort"
+
+	"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
 )
 
 type Descriptor struct {
@@ -57,13 +59,21 @@ type CanBeDisabled interface {
 // BackgroundService should be implemented for services that have
 // long running tasks in the background.
 type BackgroundService interface {
-
 	// Run starts the background process of the service after `Init` have been called
 	// on all services. The `context.Context` passed into the function should be used
 	// to subscribe to ctx.Done() so the service can be notified when Grafana shuts down.
 	Run(ctx context.Context) error
 }
 
+// DatabaseMigrator allows the caller to add migrations to
+// the migrator passed as argument
+type DatabaseMigrator interface {
+
+	// AddMigrations allows the service to add migrations to
+	// the database migrator.
+	AddMigration(mg *migrator.Migrator)
+}
+
 // IsDisabled takes an service and return true if its disabled
 func IsDisabled(srv Service) bool {
 	canBeDisabled, ok := srv.(CanBeDisabled)

+ 1 - 0
pkg/services/sqlstore/migrations/team_mig.go

@@ -50,4 +50,5 @@ func addTeamMigrations(mg *Migrator) {
 	mg.AddMigration("Add column email to team table", NewAddColumnMigration(teamV1, &Column{
 		Name: "email", Type: DB_NVarchar, Nullable: true, Length: 190,
 	}))
+
 }

+ 7 - 0
pkg/services/sqlstore/sqlstore.go

@@ -132,6 +132,13 @@ func (ss *SqlStore) Init() error {
 	migrator := migrator.NewMigrator(x)
 	migrations.AddMigrations(migrator)
 
+	for _, descriptor := range registry.GetServices() {
+		sc, ok := descriptor.Instance.(registry.DatabaseMigrator)
+		if ok {
+			sc.AddMigration(migrator)
+		}
+	}
+
 	if err := migrator.Start(); err != nil {
 		return fmt.Errorf("Migration failed err: %v", err)
 	}

+ 9 - 1
pkg/services/sqlstore/team.go

@@ -268,7 +268,15 @@ func GetTeamMembers(query *m.GetTeamMembersQuery) error {
 	query.Result = make([]*m.TeamMemberDTO, 0)
 	sess := x.Table("team_member")
 	sess.Join("INNER", "user", fmt.Sprintf("team_member.user_id=%s.id", x.Dialect().Quote("user")))
-	sess.Where("team_member.org_id=? and team_member.team_id=?", query.OrgId, query.TeamId)
+	if query.OrgId != 0 {
+		sess.Where("team_member.org_id=?", query.OrgId)
+	}
+	if query.TeamId != 0 {
+		sess.Where("team_member.team_id=?", query.TeamId)
+	}
+	if query.UserId != 0 {
+		sess.Where("team_member.user_id=?", query.UserId)
+	}
 	sess.Cols("user.org_id", "team_member.team_id", "team_member.user_id", "user.email", "user.login")
 	sess.Asc("user.login", "user.email")
 

+ 4 - 3
pkg/setting/setting.go

@@ -18,9 +18,10 @@ import (
 
 	"github.com/go-macaron/session"
 
+	"time"
+
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/util"
-	"time"
 )
 
 type Scheme string
@@ -49,7 +50,7 @@ var (
 	BuildVersion    string
 	BuildCommit     string
 	BuildStamp      int64
-	Enterprise      bool
+	IsEnterprise    bool
 	ApplicationName string
 
 	// Paths
@@ -517,7 +518,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
 	Raw = cfg.Raw
 
 	ApplicationName = "Grafana"
-	if Enterprise {
+	if IsEnterprise {
 		ApplicationName += " Enterprise"
 	}
 

+ 1 - 0
pkg/social/github_oauth.go

@@ -213,6 +213,7 @@ func (s *SocialGithub) UserInfo(client *http.Client, token *oauth2.Token) (*Basi
 	userInfo := &BasicUserInfo{
 		Name:  data.Login,
 		Login: data.Login,
+		Id:    fmt.Sprintf("%d", data.Id),
 		Email: data.Email,
 	}
 

+ 1 - 1
pkg/tsdb/mssql/macros.go

@@ -108,7 +108,7 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 				m.Query.Model.Set("fillValue", floatVal)
 			}
 		}
-		return fmt.Sprintf("CAST(ROUND(DATEDIFF(second, '1970-01-01', %s)/%.1f, 0) as bigint)*%.0f", args[0], interval.Seconds(), interval.Seconds()), nil
+		return fmt.Sprintf("FLOOR(DATEDIFF(second, '1970-01-01', %s)/%.0f)*%.0f", args[0], interval.Seconds(), interval.Seconds()), nil
 	case "__unixEpochFilter":
 		if len(args) == 0 {
 			return "", fmt.Errorf("missing time column argument for macro %v", name)

+ 2 - 2
pkg/tsdb/mssql/macros_test.go

@@ -56,14 +56,14 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, "GROUP BY CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)*300")
+				So(sql, ShouldEqual, "GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time_column)/300)*300")
 			})
 
 			Convey("interpolate __timeGroup function with spaces around arguments", func() {
 				sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, "GROUP BY CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)*300")
+				So(sql, ShouldEqual, "GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time_column)/300)*300")
 			})
 
 			Convey("interpolate __timeGroup function with fill (value = NULL)", func() {

+ 15 - 9
pkg/tsdb/mssql/mssql_test.go

@@ -210,11 +210,12 @@ func TestMSSQL(t *testing.T) {
 				So(queryResult.Error, ShouldBeNil)
 
 				points := queryResult.Series[0].Points
-				So(len(points), ShouldEqual, 6)
+				// without fill this should result in 4 buckets
+				So(len(points), ShouldEqual, 4)
 
 				dt := fromStart
 
-				for i := 0; i < 3; i++ {
+				for i := 0; i < 2; i++ {
 					aValue := points[i][0].Float64
 					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
 					So(aValue, ShouldEqual, 15)
@@ -222,9 +223,9 @@ func TestMSSQL(t *testing.T) {
 					dt = dt.Add(5 * time.Minute)
 				}
 
-				// adjust for 5 minute gap
-				dt = dt.Add(5 * time.Minute)
-				for i := 3; i < 6; i++ {
+				// adjust for 10 minute gap between first and second set of points
+				dt = dt.Add(10 * time.Minute)
+				for i := 2; i < 4; i++ {
 					aValue := points[i][0].Float64
 					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
 					So(aValue, ShouldEqual, 20)
@@ -260,7 +261,7 @@ func TestMSSQL(t *testing.T) {
 
 				dt := fromStart
 
-				for i := 0; i < 3; i++ {
+				for i := 0; i < 2; i++ {
 					aValue := points[i][0].Float64
 					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
 					So(aValue, ShouldEqual, 15)
@@ -268,17 +269,22 @@ func TestMSSQL(t *testing.T) {
 					dt = dt.Add(5 * time.Minute)
 				}
 
+				// check for NULL values inserted by fill
+				So(points[2][0].Valid, ShouldBeFalse)
 				So(points[3][0].Valid, ShouldBeFalse)
 
-				// adjust for 5 minute gap
-				dt = dt.Add(5 * time.Minute)
-				for i := 4; i < 7; i++ {
+				// adjust for 10 minute gap between first and second set of points
+				dt = dt.Add(10 * time.Minute)
+				for i := 4; i < 6; i++ {
 					aValue := points[i][0].Float64
 					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
 					So(aValue, ShouldEqual, 20)
 					So(aTime, ShouldEqual, dt)
 					dt = dt.Add(5 * time.Minute)
 				}
+
+				So(points[6][0].Valid, ShouldBeFalse)
+
 			})
 
 			Convey("When doing a metric query using timeGroup with float fill enabled", func() {

+ 1 - 1
pkg/tsdb/mysql/macros.go

@@ -103,7 +103,7 @@ func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
 				m.Query.Model.Set("fillValue", floatVal)
 			}
 		}
-		return fmt.Sprintf("cast(cast(UNIX_TIMESTAMP(%s)/(%.0f) as signed)*%.0f as signed)", args[0], interval.Seconds(), interval.Seconds()), nil
+		return fmt.Sprintf("UNIX_TIMESTAMP(%s) DIV %.0f * %.0f", args[0], interval.Seconds(), interval.Seconds()), nil
 	case "__unixEpochFilter":
 		if len(args) == 0 {
 			return "", fmt.Errorf("missing time column argument for macro %v", name)

+ 2 - 2
pkg/tsdb/mysql/macros_test.go

@@ -39,7 +39,7 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, "GROUP BY cast(cast(UNIX_TIMESTAMP(time_column)/(300) as signed)*300 as signed)")
+				So(sql, ShouldEqual, "GROUP BY UNIX_TIMESTAMP(time_column) DIV 300 * 300")
 			})
 
 			Convey("interpolate __timeGroup function with spaces around arguments", func() {
@@ -47,7 +47,7 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, "GROUP BY cast(cast(UNIX_TIMESTAMP(time_column)/(300) as signed)*300 as signed)")
+				So(sql, ShouldEqual, "GROUP BY UNIX_TIMESTAMP(time_column) DIV 300 * 300")
 			})
 
 			Convey("interpolate __timeFilter function", func() {

+ 16 - 9
pkg/tsdb/mysql/mysql_test.go

@@ -209,11 +209,12 @@ func TestMySQL(t *testing.T) {
 				So(queryResult.Error, ShouldBeNil)
 
 				points := queryResult.Series[0].Points
-				So(len(points), ShouldEqual, 6)
+				// without fill this should result in 4 buckets
+				So(len(points), ShouldEqual, 4)
 
 				dt := fromStart
 
-				for i := 0; i < 3; i++ {
+				for i := 0; i < 2; i++ {
 					aValue := points[i][0].Float64
 					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
 					So(aValue, ShouldEqual, 15)
@@ -221,9 +222,9 @@ func TestMySQL(t *testing.T) {
 					dt = dt.Add(5 * time.Minute)
 				}
 
-				// adjust for 5 minute gap
-				dt = dt.Add(5 * time.Minute)
-				for i := 3; i < 6; i++ {
+				// adjust for 10 minute gap between first and second set of points
+				dt = dt.Add(10 * time.Minute)
+				for i := 2; i < 4; i++ {
 					aValue := points[i][0].Float64
 					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
 					So(aValue, ShouldEqual, 20)
@@ -259,7 +260,7 @@ func TestMySQL(t *testing.T) {
 
 				dt := fromStart
 
-				for i := 0; i < 3; i++ {
+				for i := 0; i < 2; i++ {
 					aValue := points[i][0].Float64
 					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
 					So(aValue, ShouldEqual, 15)
@@ -267,17 +268,23 @@ func TestMySQL(t *testing.T) {
 					dt = dt.Add(5 * time.Minute)
 				}
 
+				// check for NULL values inserted by fill
+				So(points[2][0].Valid, ShouldBeFalse)
 				So(points[3][0].Valid, ShouldBeFalse)
 
-				// adjust for 5 minute gap
-				dt = dt.Add(5 * time.Minute)
-				for i := 4; i < 7; i++ {
+				// adjust for 10 minute gap between first and second set of points
+				dt = dt.Add(10 * time.Minute)
+				for i := 4; i < 6; i++ {
 					aValue := points[i][0].Float64
 					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
 					So(aValue, ShouldEqual, 20)
 					So(aTime, ShouldEqual, dt)
 					dt = dt.Add(5 * time.Minute)
 				}
+
+				// check for NULL values inserted by fill
+				So(points[6][0].Valid, ShouldBeFalse)
+
 			})
 
 			Convey("When doing a metric query using timeGroup with float fill enabled", func() {

+ 1 - 1
pkg/tsdb/postgres/macros.go

@@ -109,7 +109,7 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string,
 				m.Query.Model.Set("fillValue", floatVal)
 			}
 		}
-		return fmt.Sprintf("(extract(epoch from %s)/%v)::bigint*%v AS time", args[0], interval.Seconds(), interval.Seconds()), nil
+		return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v AS time", args[0], interval.Seconds(), interval.Seconds()), nil
 	case "__unixEpochFilter":
 		if len(args) == 0 {
 			return "", fmt.Errorf("missing time column argument for macro %v", name)

+ 2 - 2
pkg/tsdb/postgres/macros_test.go

@@ -53,7 +53,7 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, "GROUP BY (extract(epoch from time_column)/300)::bigint*300 AS time")
+				So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300 AS time")
 			})
 
 			Convey("interpolate __timeGroup function with spaces between args", func() {
@@ -61,7 +61,7 @@ func TestMacroEngine(t *testing.T) {
 				sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
 				So(err, ShouldBeNil)
 
-				So(sql, ShouldEqual, "GROUP BY (extract(epoch from time_column)/300)::bigint*300 AS time")
+				So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300 AS time")
 			})
 
 			Convey("interpolate __timeTo function", func() {

+ 17 - 9
pkg/tsdb/postgres/postgres_test.go

@@ -189,21 +189,23 @@ func TestPostgres(t *testing.T) {
 				So(queryResult.Error, ShouldBeNil)
 
 				points := queryResult.Series[0].Points
-				So(len(points), ShouldEqual, 6)
+				// without fill this should result in 4 buckets
+				So(len(points), ShouldEqual, 4)
 
 				dt := fromStart
 
-				for i := 0; i < 3; i++ {
+				for i := 0; i < 2; i++ {
 					aValue := points[i][0].Float64
 					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
 					So(aValue, ShouldEqual, 15)
 					So(aTime, ShouldEqual, dt)
+					So(aTime.Unix()%300, ShouldEqual, 0)
 					dt = dt.Add(5 * time.Minute)
 				}
 
-				// adjust for 5 minute gap
-				dt = dt.Add(5 * time.Minute)
-				for i := 3; i < 6; i++ {
+				// adjust for 10 minute gap between first and second set of points
+				dt = dt.Add(10 * time.Minute)
+				for i := 2; i < 4; i++ {
 					aValue := points[i][0].Float64
 					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
 					So(aValue, ShouldEqual, 20)
@@ -239,7 +241,7 @@ func TestPostgres(t *testing.T) {
 
 				dt := fromStart
 
-				for i := 0; i < 3; i++ {
+				for i := 0; i < 2; i++ {
 					aValue := points[i][0].Float64
 					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
 					So(aValue, ShouldEqual, 15)
@@ -247,17 +249,23 @@ func TestPostgres(t *testing.T) {
 					dt = dt.Add(5 * time.Minute)
 				}
 
+				// check for NULL values inserted by fill
+				So(points[2][0].Valid, ShouldBeFalse)
 				So(points[3][0].Valid, ShouldBeFalse)
 
-				// adjust for 5 minute gap
-				dt = dt.Add(5 * time.Minute)
-				for i := 4; i < 7; i++ {
+				// adjust for 10 minute gap between first and second set of points
+				dt = dt.Add(10 * time.Minute)
+				for i := 4; i < 6; i++ {
 					aValue := points[i][0].Float64
 					aTime := time.Unix(int64(points[i][1].Float64)/1000, 0)
 					So(aValue, ShouldEqual, 20)
 					So(aTime, ShouldEqual, dt)
 					dt = dt.Add(5 * time.Minute)
 				}
+
+				// check for NULL values inserted by fill
+				So(points[6][0].Valid, ShouldBeFalse)
+
 			})
 
 			Convey("When doing a metric query using timeGroup with float fill enabled", func() {

+ 16 - 2
public/app/core/config.ts

@@ -1,11 +1,18 @@
 import _ from 'lodash';
 
-class Settings {
+export interface BuildInfo {
+  version: string;
+  commit: string;
+  isEnterprise: boolean;
+  env: string;
+}
+
+export class Settings {
   datasources: any;
   panels: any;
   appSubUrl: string;
   window_title_prefix: string;
-  buildInfo: any;
+  buildInfo: BuildInfo;
   new_panel_title: string;
   bootData: any;
   externalUserMngLinkUrl: string;
@@ -32,7 +39,14 @@ class Settings {
       playlist_timespan: '1m',
       unsaved_changes_warning: true,
       appSubUrl: '',
+      buildInfo: {
+        version: 'v1.0',
+        commit: '1',
+        env: 'production',
+        isEnterprise: false,
+      },
     };
+
     _.extend(this, defaults, options);
   }
 }

+ 0 - 4
public/app/core/services/context_srv.ts

@@ -34,14 +34,10 @@ export class ContextSrv {
   constructor() {
     this.sidemenu = store.getBool('grafana.sidemenu', true);
 
-    if (!config.buildInfo) {
-      config.buildInfo = {};
-    }
     if (!config.bootData) {
       config.bootData = { user: {}, settings: {} };
     }
 
-    this.version = config.buildInfo.version;
     this.user = new User();
     this.isSignedIn = this.user.isSignedIn;
     this.isGrafanaAdmin = this.user.isGrafanaAdmin;

+ 35 - 0
public/app/core/specs/table_model.jest.ts

@@ -44,3 +44,38 @@ describe('when sorting table asc', () => {
     expect(table.rows[2][1]).toBe(15);
   });
 });
+
+describe('when sorting with nulls', () => {
+  var table;
+  var values;
+
+  beforeEach(() => {
+    table = new TableModel();
+    table.columns = [{}, {}];
+    table.rows = [[42, ''], [19, 'a'], [null, 'b'], [0, 'd'], [null, null], [2, 'c'], [0, null], [-8, '']];
+  });
+
+  it('numbers with nulls at end with asc sort', () => {
+    table.sort({ col: 0, desc: false });
+    values = table.rows.map(row => row[0]);
+    expect(values).toEqual([-8, 0, 0, 2, 19, 42, null, null]);
+  });
+
+  it('numbers with nulls at start with desc sort', () => {
+    table.sort({ col: 0, desc: true });
+    values = table.rows.map(row => row[0]);
+    expect(values).toEqual([null, null, 42, 19, 2, 0, 0, -8]);
+  });
+
+  it('strings with nulls at end with asc sort', () => {
+    table.sort({ col: 1, desc: false });
+    values = table.rows.map(row => row[1]);
+    expect(values).toEqual(['', '', 'a', 'b', 'c', 'd', null, null]);
+  });
+
+  it('strings with nulls at start with desc sort', () => {
+    table.sort({ col: 1, desc: true });
+    values = table.rows.map(row => row[1]);
+    expect(values).toEqual([null, null, 'd', 'c', 'b', 'a', '', '']);
+  });
+});

+ 14 - 0
public/app/core/specs/time_series.jest.ts

@@ -119,6 +119,20 @@ describe('TimeSeries', function() {
       series.getFlotPairs('null');
       expect(series.stats.avg).toBe(null);
     });
+
+    it('calculates timeStep', function() {
+      series = new TimeSeries({
+        datapoints: [[null, 1], [null, 2], [null, 3]],
+      });
+      series.getFlotPairs('null');
+      expect(series.stats.timeStep).toBe(1);
+
+      series = new TimeSeries({
+        datapoints: [[0, 1530529290], [0, 1530529305], [0, 1530529320]],
+      });
+      series.getFlotPairs('null');
+      expect(series.stats.timeStep).toBe(15);
+    });
   });
 
   describe('When checking if ms resolution is needed', function() {

+ 5 - 12
public/app/core/table_model.ts

@@ -19,23 +19,16 @@ export default class TableModel {
     this.rows.sort(function(a, b) {
       a = a[options.col];
       b = b[options.col];
-      if (a < b) {
-        return -1;
-      }
-      if (a > b) {
-        return 1;
-      }
-      return 0;
+      // Sort null or undefined seperately from comparable values
+      return +(a == null) - +(b == null) || +(a > b) || -(a < b);
     });
 
-    this.columns[options.col].sort = true;
-
     if (options.desc) {
       this.rows.reverse();
-      this.columns[options.col].desc = true;
-    } else {
-      this.columns[options.col].desc = false;
     }
+
+    this.columns[options.col].sort = true;
+    this.columns[options.col].desc = options.desc;
   }
 
   addColumn(col) {

+ 1 - 3
public/app/features/dashboard/specs/exporter.jest.ts

@@ -86,9 +86,7 @@ describe('given dashboard with repeated panels', () => {
       ],
     };
 
-    config.buildInfo = {
-      version: '3.0.2',
-    };
+    config.buildInfo.version = '3.0.2';
 
     //Stubs test function calls
     var datasourceSrvStub = { get: jest.fn(arg => getStub(arg)) };

+ 80 - 42
public/app/features/org/partials/team_details.html

@@ -1,22 +1,22 @@
 <page-header model="ctrl.navModel"></page-header>
 
 <div class="page-container page-body">
-	<h3 class="page-sub-heading">Team Details</h3>
+  <h3 class="page-sub-heading">Team Details</h3>
 
   <form name="teamDetailsForm" class="gf-form-group">
     <div class="gf-form max-width-30">
       <span class="gf-form-label width-10">Name</span>
       <input type="text" required ng-model="ctrl.team.name" class="gf-form-input max-width-22">
-		</div>
-		<div class="gf-form max-width-30">
-			<span class="gf-form-label width-10">
-				Email
-				<info-popover mode="right-normal">
-						This is optional and is primarily used for allowing custom team avatars.
-				</info-popover>
-			</span>
-			<input class="gf-form-input max-width-22" type="email" ng-model="ctrl.team.email" placeholder="email@test.com">
-		</div>
+    </div>
+    <div class="gf-form max-width-30">
+      <span class="gf-form-label width-10">
+        Email
+        <info-popover mode="right-normal">
+          This is optional and is primarily used for allowing custom team avatars.
+        </info-popover>
+      </span>
+      <input class="gf-form-input max-width-22" type="email" ng-model="ctrl.team.email" placeholder="email@test.com">
+    </div>
 
     <div class="gf-form-button-row">
       <button type="submit" class="btn btn-success" ng-click="ctrl.update()">Update</button>
@@ -26,42 +26,80 @@
   <div class="gf-form-group">
 
     <h3 class="page-heading">Team Members</h3>
-		<form name="ctrl.addMemberForm" class="gf-form-group">
+    <form name="ctrl.addMemberForm" class="gf-form-group">
       <div class="gf-form">
         <span class="gf-form-label width-10">Add member</span>
-				<!--
-				Old picker
-				<user-picker user-picked="ctrl.userPicked($user)"></user-picker>
-				-->
-				<select-user-picker  class="width-7" handlePicked="ctrl.userPicked" backendSrv="ctrl.backendSrv"></select-user-picker>
+        <!--
+        Old picker
+        <user-picker user-picked="ctrl.userPicked($user)"></user-picker>
+        -->
+        <select-user-picker class="width-7" handlePicked="ctrl.userPicked" backendSrv="ctrl.backendSrv"></select-user-picker>
       </div>
     </form>
 
     <table class="filter-table" ng-show="ctrl.teamMembers.length > 0">
-			<thead>
-				<tr>
-					<th></th>
-					<th>Username</th>
-					<th>Email</th>
-					<th></th>
-				</tr>
-			</thead>
-			<tr ng-repeat="member in ctrl.teamMembers">
-				<td class="width-4 text-center link-td">
-					<img class="filter-table__avatar" ng-src="{{member.avatarUrl}}"></img>
-				</td>
-				<td>{{member.login}}</td>
-				<td>{{member.email}}</td>
-				<td style="width: 1%">
-					<a ng-click="ctrl.removeTeamMember(member)" class="btn btn-danger btn-mini">
-						<i class="fa fa-remove"></i>
-					</a>
-				</td>
-			</tr>
-		</table>
-		<div>
-			<em class="muted" ng-hide="ctrl.teamMembers.length > 0">
-				This team has no members yet.
-			</em>
+      <thead>
+        <tr>
+          <th></th>
+          <th>Username</th>
+          <th>Email</th>
+          <th></th>
+        </tr>
+      </thead>
+      <tr ng-repeat="member in ctrl.teamMembers">
+        <td class="width-4 text-center link-td">
+          <img class="filter-table__avatar" ng-src="{{member.avatarUrl}}"></img>
+        </td>
+        <td>{{member.login}}</td>
+        <td>{{member.email}}</td>
+        <td style="width: 1%">
+          <a ng-click="ctrl.removeTeamMember(member)" class="btn btn-danger btn-mini">
+            <i class="fa fa-remove"></i>
+          </a>
+        </td>
+      </tr>
+    </table>
+    <div>
+      <em class="muted" ng-hide="ctrl.teamMembers.length > 0">
+        This team has no members yet.
+      </em>
+    </div>
+
+  </div>
+
+  <div class="gf-form-group" ng-if="ctrl.isMappingsEnabled">
+
+	<h3 class="page-heading">Mappings to external groups</h3>
+	<form name="ctrl.addGroupForm" class="gf-form-group">
+		<div class="gf-form">
+			<span class="gf-form-label width-10">Add group</span>
+			<input class="gf-form-input max-width-22" type="text" ng-model="ctrl.newGroupId">
 		</div>
+		<div class="gf-form-button-row">
+			<button type="submit" class="btn btn-success" ng-click="ctrl.addGroup()">Add</button>
+		</div>
+	</form>
+
+	<table class="filter-table" ng-show="ctrl.teamGroups.length > 0">
+		<thead>
+			<tr>
+				<th>Group</th>
+				<th></th>
+			</tr>
+		</thead>
+		<tr ng-repeat="group in ctrl.teamGroups">
+			<td>{{group.groupId}}</td>
+			<td style="width: 1%">
+				<a ng-click="ctrl.removeGroup(group)" class="btn btn-danger btn-mini">
+					<i class="fa fa-remove"></i>
+				</a>
+			</td>
+		</tr>
+	</table>
+	<div>
+		<em class="muted" ng-hide="ctrl.teamGroups.length > 0">
+			This team has no associated groups yet.
+		</em>
+	</div>
 
+	</div>

+ 27 - 0
public/app/features/org/team_details_ctrl.ts

@@ -1,15 +1,21 @@
 import coreModule from 'app/core/core_module';
+import config from 'app/core/config';
 
 export default class TeamDetailsCtrl {
   team: Team;
   teamMembers: User[] = [];
   navModel: any;
+  teamGroups: TeamGroup[] = [];
+  newGroupId: string;
+  isMappingsEnabled: boolean;
 
   /** @ngInject **/
   constructor(private $scope, private backendSrv, private $routeParams, navModelSrv) {
     this.navModel = navModelSrv.getNav('cfg', 'teams', 0);
     this.userPicked = this.userPicked.bind(this);
     this.get = this.get.bind(this);
+    this.newGroupId = '';
+    this.isMappingsEnabled = config.buildInfo.isEnterprise;
     this.get();
   }
 
@@ -18,9 +24,16 @@ export default class TeamDetailsCtrl {
       this.backendSrv.get(`/api/teams/${this.$routeParams.id}`).then(result => {
         this.team = result;
       });
+
       this.backendSrv.get(`/api/teams/${this.$routeParams.id}/members`).then(result => {
         this.teamMembers = result;
       });
+
+      if (this.isMappingsEnabled) {
+        this.backendSrv.get(`/api/teams/${this.$routeParams.id}/groups`).then(result => {
+          this.teamGroups = result;
+        });
+      }
     }
   }
 
@@ -57,6 +70,20 @@ export default class TeamDetailsCtrl {
       this.get();
     });
   }
+
+  addGroup() {
+    this.backendSrv.post(`/api/teams/${this.$routeParams.id}/groups`, { groupId: this.newGroupId }).then(() => {
+      this.get();
+    });
+  }
+
+  removeGroup(group: TeamGroup) {
+    this.backendSrv.delete(`/api/teams/${this.$routeParams.id}/groups/${group.groupId}`).then(this.get);
+  }
+}
+
+export interface TeamGroup {
+  groupId: string;
 }
 
 export interface Team {

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

@@ -91,10 +91,20 @@ export class DatasourceSrv {
 
     _.each(config.datasources, function(value, key) {
       if (value.meta && value.meta.metrics) {
-        metricSources.push({ value: key, name: key, meta: value.meta });
+        let metricSource = { value: key, name: key, meta: value.meta, sort: key };
+
+        //Make sure grafana and mixed are sorted at the bottom
+        if (value.meta.id === 'grafana') {
+          metricSource.sort = String.fromCharCode(253);
+        } else if (value.meta.id === 'mixed') {
+          metricSource.sort = String.fromCharCode(254);
+        }
+
+        metricSources.push(metricSource);
 
         if (key === config.defaultDatasource) {
-          metricSources.push({ value: null, name: 'default', meta: value.meta });
+          metricSource = { value: null, name: 'default', meta: value.meta, sort: key };
+          metricSources.push(metricSource);
         }
       }
     });
@@ -104,17 +114,10 @@ export class DatasourceSrv {
     }
 
     metricSources.sort(function(a, b) {
-      // these two should always be at the bottom
-      if (a.meta.id === 'mixed' || a.meta.id === 'grafana') {
-        return 1;
-      }
-      if (b.meta.id === 'mixed' || b.meta.id === 'grafana') {
-        return -1;
-      }
-      if (a.name.toLowerCase() > b.name.toLowerCase()) {
+      if (a.sort.toLowerCase() > b.sort.toLowerCase()) {
         return 1;
       }
-      if (a.name.toLowerCase() < b.name.toLowerCase()) {
+      if (a.sort.toLowerCase() < b.sort.toLowerCase()) {
         return -1;
       }
       return 0;

+ 59 - 0
public/app/features/plugins/specs/datasource_srv.jest.ts

@@ -0,0 +1,59 @@
+import config from 'app/core/config';
+import 'app/features/plugins/datasource_srv';
+import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
+
+describe('datasource_srv', function() {
+  let _datasourceSrv = new DatasourceSrv({}, {}, {}, {});
+  let metricSources;
+
+  describe('when loading metric sources', () => {
+    let unsortedDatasources = {
+      mmm: {
+        type: 'test-db',
+        meta: { metrics: { m: 1 } },
+      },
+      '--Grafana--': {
+        type: 'grafana',
+        meta: { builtIn: true, metrics: { m: 1 }, id: 'grafana' },
+      },
+      '--Mixed--': {
+        type: 'test-db',
+        meta: { builtIn: true, metrics: { m: 1 }, id: 'mixed' },
+      },
+      ZZZ: {
+        type: 'test-db',
+        meta: { metrics: { m: 1 } },
+      },
+      aaa: {
+        type: 'test-db',
+        meta: { metrics: { m: 1 } },
+      },
+      BBB: {
+        type: 'test-db',
+        meta: { metrics: { m: 1 } },
+      },
+    };
+    beforeEach(() => {
+      config.datasources = unsortedDatasources;
+      metricSources = _datasourceSrv.getMetricSources({ skipVariables: true });
+    });
+
+    it('should return a list of sources sorted case insensitively with builtin sources last', () => {
+      expect(metricSources[0].name).toBe('aaa');
+      expect(metricSources[1].name).toBe('BBB');
+      expect(metricSources[2].name).toBe('mmm');
+      expect(metricSources[3].name).toBe('ZZZ');
+      expect(metricSources[4].name).toBe('--Grafana--');
+      expect(metricSources[5].name).toBe('--Mixed--');
+    });
+
+    beforeEach(() => {
+      config.defaultDatasource = 'BBB';
+    });
+
+    it('should set default data source', () => {
+      expect(metricSources[2].name).toBe('default');
+      expect(metricSources[2].sort).toBe('BBB');
+    });
+  });
+});

+ 0 - 64
public/app/features/plugins/specs/datasource_srv_specs.ts

@@ -1,64 +0,0 @@
-import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common';
-import config from 'app/core/config';
-import 'app/features/plugins/datasource_srv';
-
-describe('datasource_srv', function() {
-  var _datasourceSrv;
-  var metricSources;
-  var templateSrv = {};
-
-  beforeEach(angularMocks.module('grafana.core'));
-  beforeEach(
-    angularMocks.module(function($provide) {
-      $provide.value('templateSrv', templateSrv);
-    })
-  );
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(
-    angularMocks.inject(function(datasourceSrv) {
-      _datasourceSrv = datasourceSrv;
-    })
-  );
-
-  describe('when loading metric sources', function() {
-    var unsortedDatasources = {
-      mmm: {
-        type: 'test-db',
-        meta: { metrics: { m: 1 } },
-      },
-      '--Grafana--': {
-        type: 'grafana',
-        meta: { builtIn: true, metrics: { m: 1 }, id: 'grafana' },
-      },
-      '--Mixed--': {
-        type: 'test-db',
-        meta: { builtIn: true, metrics: { m: 1 }, id: 'mixed' },
-      },
-      ZZZ: {
-        type: 'test-db',
-        meta: { metrics: { m: 1 } },
-      },
-      aaa: {
-        type: 'test-db',
-        meta: { metrics: { m: 1 } },
-      },
-      BBB: {
-        type: 'test-db',
-        meta: { metrics: { m: 1 } },
-      },
-    };
-    beforeEach(function() {
-      config.datasources = unsortedDatasources;
-      metricSources = _datasourceSrv.getMetricSources({ skipVariables: true });
-    });
-
-    it('should return a list of sources sorted case insensitively with builtin sources last', function() {
-      expect(metricSources[0].name).to.be('aaa');
-      expect(metricSources[1].name).to.be('BBB');
-      expect(metricSources[2].name).to.be('mmm');
-      expect(metricSources[3].name).to.be('ZZZ');
-      expect(metricSources[4].name).to.be('--Grafana--');
-      expect(metricSources[5].name).to.be('--Mixed--');
-    });
-  });
-});

+ 6 - 2
public/app/features/templating/variable_srv.ts

@@ -38,7 +38,11 @@ export class VariableSrv {
       });
   }
 
-  onDashboardRefresh() {
+  onDashboardRefresh(evt, payload) {
+    if (payload && payload.fromVariableValueUpdated) {
+      return Promise.resolve({});
+    }
+
     var promises = this.variables.filter(variable => variable.refresh === 2).map(variable => {
       var previousOptions = variable.options.slice();
 
@@ -130,7 +134,7 @@ export class VariableSrv {
     return this.$q.all(promises).then(() => {
       if (emitChangeEvents) {
         this.$rootScope.$emit('template-variable-value-updated');
-        this.$rootScope.$broadcast('refresh');
+        this.$rootScope.$broadcast('refresh', { fromVariableValueUpdated: true });
       }
     });
   }

+ 2 - 2
public/app/plugins/datasource/prometheus/datasource.ts

@@ -162,8 +162,8 @@ export class PrometheusDatasource {
           format: activeTargets[index].format,
           step: queries[index].step,
           legendFormat: activeTargets[index].legendFormat,
-          start: start,
-          end: end,
+          start: queries[index].start,
+          end: queries[index].end,
           query: queries[index].expr,
           responseListLength: responseList.length,
           responseIndex: index,

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

@@ -68,7 +68,7 @@ describe('PrometheusDatasource', () => {
       ctx.query = {
         range: { from: moment(1443454528000), to: moment(1443454528000) },
         targets: [{ expr: 'test{job="testjob"}', format: 'heatmap', legendFormat: '{{le}}' }],
-        interval: '60s',
+        interval: '1s',
       };
     });
 

+ 78 - 0
public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts

@@ -127,4 +127,82 @@ describe('Prometheus Result Transformer', () => {
       ]);
     });
   });
+
+  describe('When resultFormat is time series', () => {
+    it('should transform matrix into timeseries', () => {
+      const response = {
+        status: 'success',
+        data: {
+          resultType: 'matrix',
+          result: [
+            {
+              metric: { __name__: 'test', job: 'testjob' },
+              values: [[0, '10'], [1, '10'], [2, '0']],
+            },
+          ],
+        },
+      };
+      let result = [];
+      let options = {
+        format: 'timeseries',
+        start: 0,
+        end: 2,
+      };
+
+      ctx.resultTransformer.transform(result, { data: response }, options);
+      expect(result).toEqual([{ target: 'test{job="testjob"}', datapoints: [[10, 0], [10, 1000], [0, 2000]] }]);
+    });
+
+    it('should fill timeseries with null values', () => {
+      const response = {
+        status: 'success',
+        data: {
+          resultType: 'matrix',
+          result: [
+            {
+              metric: { __name__: 'test', job: 'testjob' },
+              values: [[1, '10'], [2, '0']],
+            },
+          ],
+        },
+      };
+      let result = [];
+      let options = {
+        format: 'timeseries',
+        step: 1,
+        start: 0,
+        end: 2,
+      };
+
+      ctx.resultTransformer.transform(result, { data: response }, options);
+      expect(result).toEqual([{ target: 'test{job="testjob"}', datapoints: [[null, 0], [10, 1000], [0, 2000]] }]);
+    });
+
+    it('should align null values with step', () => {
+      const response = {
+        status: 'success',
+        data: {
+          resultType: 'matrix',
+          result: [
+            {
+              metric: { __name__: 'test', job: 'testjob' },
+              values: [[4, '10'], [8, '10']],
+            },
+          ],
+        },
+      };
+      let result = [];
+      let options = {
+        format: 'timeseries',
+        step: 2,
+        start: 0,
+        end: 8,
+      };
+
+      ctx.resultTransformer.transform(result, { data: response }, options);
+      expect(result).toEqual([
+        { target: 'test{job="testjob"}', datapoints: [[null, 0], [null, 2000], [10, 4000], [null, 6000], [10, 8000]] },
+      ]);
+    });
+  });
 });

+ 1 - 1
public/views/index.template.html

@@ -65,7 +65,7 @@
                 </a>
               </li>
               <li>
-                <a href="https://grafana.com" target="_blank">Grafana</a>
+                <a href="https://grafana.com" target="_blank">[[.AppName]]</a>
                 <span>v[[.BuildVersion]] (commit: [[.BuildCommit]])</span>
               </li>
               [[if .NewGrafanaVersionExists]]

+ 2 - 2
scripts/build/build_enterprise.sh

@@ -14,9 +14,9 @@ cd /go/src/github.com/grafana/grafana
 echo "current dir: $(pwd)"
 
 cd ..
-git clone -b ee_build --single-branch git@github.com:grafana/grafana-enterprise.git --depth 10
+git clone -b master --single-branch git@github.com:grafana/grafana-enterprise.git --depth 10
 cd grafana-enterprise
-git checkout 7fbae9c1be3467c4a39cf6ad85278a6896ceb49f
+#git checkout 7fbae9c1be3467c4a39cf6ad85278a6896ceb49f
 ./build.sh
 
 cd ../grafana