diff --git a/.circleci/config.yml b/.circleci/config.yml index 3aedd49c935..9b2f436adee 100644 --- a/.circleci/config.yml +++ b/.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: diff --git a/.gitignore b/.gitignore index 25325b37890..accc24d84cd 100644 --- a/.gitignore +++ b/.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 diff --git a/CHANGELOG.md b/CHANGELOG.md index b3ba9076774..6ddc9497dbc 100644 --- a/CHANGELOG.md +++ b/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) diff --git a/build.go b/build.go index 3f92f8833a2..77cbde50c41 100644 --- a/build.go +++ b/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() } diff --git a/docker/blocks/openldap/Dockerfile b/docker/blocks/openldap/Dockerfile index c9b928ad56a..76172e133a4 100644 --- a/docker/blocks/openldap/Dockerfile +++ b/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"] diff --git a/docker/blocks/openldap/entrypoint.sh b/docker/blocks/openldap/entrypoint.sh index d560b78d388..d202ed14b31 100755 --- a/docker/blocks/openldap/entrypoint.sh +++ b/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 diff --git a/docker/blocks/openldap/notes.md b/docker/blocks/openldap/notes.md index 483266f0d88..8de23d5ccf2 100644 --- a/docker/blocks/openldap/notes.md +++ b/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 + + + diff --git a/docker/blocks/openldap/prepopulate.sh b/docker/blocks/openldap/prepopulate.sh new file mode 100755 index 00000000000..aa11f8aba4f --- /dev/null +++ b/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 + + diff --git a/docker/blocks/openldap/prepopulate/1_units.ldif b/docker/blocks/openldap/prepopulate/1_units.ldif new file mode 100644 index 00000000000..22e06303688 --- /dev/null +++ b/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 diff --git a/docker/blocks/openldap/prepopulate/2_users.ldif b/docker/blocks/openldap/prepopulate/2_users.ldif new file mode 100644 index 00000000000..52e74b1e4b1 --- /dev/null +++ b/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 diff --git a/docker/blocks/openldap/prepopulate/3_groups.ldif b/docker/blocks/openldap/prepopulate/3_groups.ldif new file mode 100644 index 00000000000..8638a089cc8 --- /dev/null +++ b/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 diff --git a/docker/blocks/openldap/prepopulate/admin.ldif b/docker/blocks/openldap/prepopulate/admin.ldif deleted file mode 100644 index 3f4406d5810..00000000000 --- a/docker/blocks/openldap/prepopulate/admin.ldif +++ /dev/null @@ -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 diff --git a/docker/blocks/openldap/prepopulate/adminsgroup.ldif b/docker/blocks/openldap/prepopulate/adminsgroup.ldif deleted file mode 100644 index d8dece4e458..00000000000 --- a/docker/blocks/openldap/prepopulate/adminsgroup.ldif +++ /dev/null @@ -1,5 +0,0 @@ -dn: cn=admins,dc=grafana,dc=org -cn: admins -member: cn=ldapadmin,dc=grafana,dc=org -objectClass: groupOfNames -objectClass: top diff --git a/docker/blocks/openldap/prepopulate/editor.ldif b/docker/blocks/openldap/prepopulate/editor.ldif deleted file mode 100644 index eba3adc4352..00000000000 --- a/docker/blocks/openldap/prepopulate/editor.ldif +++ /dev/null @@ -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 diff --git a/docker/blocks/openldap/prepopulate/usersgroup.ldif b/docker/blocks/openldap/prepopulate/usersgroup.ldif deleted file mode 100644 index a1de3a50d38..00000000000 --- a/docker/blocks/openldap/prepopulate/usersgroup.ldif +++ /dev/null @@ -1,5 +0,0 @@ -dn: cn=users,dc=grafana,dc=org -cn: users -member: cn=ldapeditor,dc=grafana,dc=org -objectClass: groupOfNames -objectClass: top diff --git a/docker/blocks/openldap/prepopulate/viewer.ldif b/docker/blocks/openldap/prepopulate/viewer.ldif deleted file mode 100644 index f699a7df57b..00000000000 --- a/docker/blocks/openldap/prepopulate/viewer.ldif +++ /dev/null @@ -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 diff --git a/docs/sources/installation/behind_proxy.md b/docs/sources/installation/behind_proxy.md index 89711aecb46..6e3884456ac 100644 --- a/docs/sources/installation/behind_proxy.md +++ b/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 diff --git a/pkg/api/alerting_test.go b/pkg/api/alerting_test.go index abfdfb66322..9eba0e0d5b6 100644 --- a/pkg/api/alerting_test.go +++ b/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 diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index e5f63ce022b..6590eb19ff2 100644 --- a/pkg/api/annotations_test.go +++ b/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 diff --git a/pkg/api/api.go b/pkg/api/api.go index 39dae56eb69..8870b9b095e 100644 --- a/pkg/api/api.go +++ b/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) } diff --git a/pkg/api/app_routes.go b/pkg/api/app_routes.go index 0b7dcd32ce3..a2137089fc6 100644 --- a/pkg/api/app_routes.go +++ b/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, diff --git a/pkg/api/common.go b/pkg/api/common.go index 97f41ff7c72..7973c72c8fa 100644 --- a/pkg/api/common.go +++ b/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 diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 40c438b607a..8b66a7a468b 100644 --- a/pkg/api/common_test.go +++ b/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) diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index 24f0bdca365..f65c5f1f5fa 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/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 diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index ccde2382787..50a2e314f5c 100644 --- a/pkg/api/dashboard_test.go +++ b/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, diff --git a/pkg/api/dtos/index.go b/pkg/api/dtos/index.go index 8c7f505277d..77004899fc3 100644 --- a/pkg/api/dtos/index.go +++ b/pkg/api/dtos/index.go @@ -13,6 +13,7 @@ type IndexViewData struct { Theme string NewGrafanaVersionExists bool NewGrafanaVersion string + AppName string } type PluginCss struct { diff --git a/pkg/api/folder_permission_test.go b/pkg/api/folder_permission_test.go index f7458af6dce..64a746ca937 100644 --- a/pkg/api/folder_permission_test.go +++ b/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 diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 0d9b9495686..6e24e432535 100644 --- a/pkg/api/folder_test.go +++ b/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} diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index e7272e68997..da3c88566c1 100644 --- a/pkg/api/frontendsettings.go +++ b/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, }, } diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index e1a10fb468f..0de63ce5e08 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -33,7 +33,11 @@ import ( ) func init() { - registry.RegisterService(&HTTPServer{}) + registry.Register(®istry.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) { diff --git a/pkg/api/index.go b/pkg/api/index.go index a52bd3e77b0..ea10940d3ba 100644 --- a/pkg/api/index.go +++ b/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 { diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index 7b90cf8b1e8..f00e6bba0fd 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/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) diff --git a/pkg/extensions/main.go b/pkg/extensions/main.go index 34ac9da7e86..6e3461da8a8 100644 --- a/pkg/extensions/main.go +++ b/pkg/extensions/main.go @@ -1,3 +1,3 @@ package extensions -import _ "github.com/pkg/errors" +var IsEnterprise bool = false diff --git a/pkg/login/ext_user.go b/pkg/login/ext_user.go index e1d5e3e3b48..d6eaf9a975e 100644 --- a/pkg/login/ext_user.go +++ b/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 } diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index 026a94fa43e..bdf87b2db54 100644 --- a/pkg/login/ldap.go +++ b/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 diff --git a/pkg/login/ldap_test.go b/pkg/login/ldap_test.go index 34932926406..5080840704e 100644 --- a/pkg/login/ldap_test.go +++ b/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} diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 3d3cfc2e1b6..4dd84c12151 100644 --- a/pkg/metrics/metrics.go +++ b/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{} diff --git a/pkg/models/team_member.go b/pkg/models/team_member.go index 19cf657292d..9434dad8ecd 100644 --- a/pkg/models/team_member.go +++ b/pkg/models/team_member.go @@ -42,6 +42,7 @@ type RemoveTeamMemberCommand struct { type GetTeamMembersQuery struct { OrgId int64 TeamId int64 + UserId int64 Result []*TeamMemberDTO } diff --git a/pkg/models/user_auth.go b/pkg/models/user_auth.go index 0ecd144d52c..162a4d867a9 100644 --- a/pkg/models/user_auth.go +++ b/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 +} diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index 6bbc302c593..87fca27f6c1 100644 --- a/pkg/registry/registry.go +++ b/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) diff --git a/pkg/services/sqlstore/migrations/team_mig.go b/pkg/services/sqlstore/migrations/team_mig.go index eb0641fbc32..9800d27f8ab 100644 --- a/pkg/services/sqlstore/migrations/team_mig.go +++ b/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, })) + } diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index b0edc1676e0..13d706b6198 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/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) } diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index 7d53d114235..9378ca37f60 100644 --- a/pkg/services/sqlstore/team.go +++ b/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") diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index e71a3619aa5..d8c8e6431c0 100644 --- a/pkg/setting/setting.go +++ b/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" } diff --git a/pkg/social/github_oauth.go b/pkg/social/github_oauth.go index 815c684cf03..b07f112b8d3 100644 --- a/pkg/social/github_oauth.go +++ b/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, } diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go index bb9489cd654..fac25f58ff0 100644 --- a/pkg/tsdb/mssql/macros.go +++ b/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) diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/macros_test.go index ae0d4f67d2b..22d54aaa7da 100644 --- a/pkg/tsdb/mssql/macros_test.go +++ b/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() { diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index e62d30a6325..2ecd3cd9e96 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/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() { diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go index fadcbe4edbc..bf815143b3c 100644 --- a/pkg/tsdb/mysql/macros.go +++ b/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) diff --git a/pkg/tsdb/mysql/macros_test.go b/pkg/tsdb/mysql/macros_test.go index 66ec143eac8..7cdd92b34b4 100644 --- a/pkg/tsdb/mysql/macros_test.go +++ b/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() { diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go index 5650de237c5..22e98ac63ca 100644 --- a/pkg/tsdb/mysql/mysql_test.go +++ b/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() { diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index 05e39f2c762..61e88418ff4 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/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) diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go index c3c15691e42..8c581850430 100644 --- a/pkg/tsdb/postgres/macros_test.go +++ b/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() { diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go index 7f24d5a2063..18ceecb1080 100644 --- a/pkg/tsdb/postgres/postgres_test.go +++ b/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() { diff --git a/public/app/core/config.ts b/public/app/core/config.ts index e111d0d0e9f..e065ddb22fb 100644 --- a/public/app/core/config.ts +++ b/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); } } diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index be8a0af7b7b..8959573e731 100644 --- a/public/app/core/services/context_srv.ts +++ b/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; diff --git a/public/app/core/specs/table_model.jest.ts b/public/app/core/specs/table_model.jest.ts index a2c1eb5e1af..3d4c526cfea 100644 --- a/public/app/core/specs/table_model.jest.ts +++ b/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', '', '']); + }); +}); diff --git a/public/app/core/specs/time_series.jest.ts b/public/app/core/specs/time_series.jest.ts index f5245476218..bf50d807e03 100644 --- a/public/app/core/specs/time_series.jest.ts +++ b/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() { diff --git a/public/app/core/table_model.ts b/public/app/core/table_model.ts index 5716aac2be6..04857eb806d 100644 --- a/public/app/core/table_model.ts +++ b/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) { diff --git a/public/app/features/dashboard/specs/exporter.jest.ts b/public/app/features/dashboard/specs/exporter.jest.ts index aa574a4b85a..c7727a4af4d 100644 --- a/public/app/features/dashboard/specs/exporter.jest.ts +++ b/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)) }; diff --git a/public/app/features/org/partials/team_details.html b/public/app/features/org/partials/team_details.html index 3fce8b3c720..3ce851d5546 100644 --- a/public/app/features/org/partials/team_details.html +++ b/public/app/features/org/partials/team_details.html @@ -1,22 +1,22 @@
-

Team Details

+

Team Details

Name -
-
- - Email - - This is optional and is primarily used for allowing custom team avatars. - - - -
+
+
+ + Email + + This is optional and is primarily used for allowing custom team avatars. + + + +
@@ -26,42 +26,80 @@

Team Members

- +
Add member - - + +
- - - - - - - - - - - - - - -
UsernameEmail
{{member.login}}{{member.email}} - - - -
-
- - This team has no members yet. - -
+ + + + Username + Email + + + + + + + + {{member.login}} + {{member.email}} + + + + + + + +
+ + This team has no members yet. + +
+
+ +
+ +

Mappings to external groups

+
+
+ Add group + +
+
+ +
+
+ + + + + + + + + + + + +
Group
{{group.groupId}} + + + +
+
+ + This team has no associated groups yet. + +
+ +
diff --git a/public/app/features/org/team_details_ctrl.ts b/public/app/features/org/team_details_ctrl.ts index 3d193880635..6e0fddafa9d 100644 --- a/public/app/features/org/team_details_ctrl.ts +++ b/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 { diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts index 19bdf599ce1..b5e0316163c 100644 --- a/public/app/features/plugins/datasource_srv.ts +++ b/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') { + if (a.sort.toLowerCase() > b.sort.toLowerCase()) { return 1; } - if (b.meta.id === 'mixed' || b.meta.id === 'grafana') { - return -1; - } - if (a.name.toLowerCase() > b.name.toLowerCase()) { - return 1; - } - if (a.name.toLowerCase() < b.name.toLowerCase()) { + if (a.sort.toLowerCase() < b.sort.toLowerCase()) { return -1; } return 0; diff --git a/public/app/features/plugins/specs/datasource_srv.jest.ts b/public/app/features/plugins/specs/datasource_srv.jest.ts new file mode 100644 index 00000000000..f261c4e2249 --- /dev/null +++ b/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'); + }); + }); +}); diff --git a/public/app/features/plugins/specs/datasource_srv_specs.ts b/public/app/features/plugins/specs/datasource_srv_specs.ts deleted file mode 100644 index 85a66b59ee7..00000000000 --- a/public/app/features/plugins/specs/datasource_srv_specs.ts +++ /dev/null @@ -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--'); - }); - }); -}); diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index 8a096dd9ad2..8ad3c2845e2 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/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 }); } }); } diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 46431a08ab1..d7d33264c99 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/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, diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts b/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts index 0157322da58..219b990e5dd 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.jest.ts +++ b/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', }; }); diff --git a/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts b/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts index 56a05d5aedb..b94cca79059 100644 --- a/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts +++ b/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]] }, + ]); + }); + }); }); diff --git a/public/views/index.template.html b/public/views/index.template.html index 79da1d7179c..4140321d633 100644 --- a/public/views/index.template.html +++ b/public/views/index.template.html @@ -65,7 +65,7 @@
  • - Grafana + [[.AppName]] v[[.BuildVersion]] (commit: [[.BuildCommit]])
  • [[if .NewGrafanaVersionExists]] diff --git a/scripts/build/build_enterprise.sh b/scripts/build/build_enterprise.sh index 02d8c78c885..cda3952c36a 100755 --- a/scripts/build/build_enterprise.sh +++ b/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