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 @@