admin: adds paging to global user list

Currently there is a limit of 1000 users in the global
user list. This change introduces paging so that an
admin can see all users and not just the first 1000.

Adds a new route to the api - /api/users/search that
returns a list of users and a total count. It takes
two parameters perpage and page that enable paging.

Fixes #7469
This commit is contained in:
Daniel Lee
2017-02-13 12:59:36 +01:00
committed by bergquist
parent e80f673264
commit 193d468ed3
15 changed files with 360 additions and 100 deletions
+2 -1
View File
@@ -123,6 +123,7 @@ func (hs *HttpServer) registerRoutes() {
// users (admin permission required)
r.Group("/users", func() {
r.Get("/", wrap(SearchUsers))
r.Get("/search", wrap(SearchUsersWithPaging))
r.Get("/:id", wrap(GetUserById))
r.Get("/:id/orgs", wrap(GetUserOrgList))
// query parameters /users/lookup?loginOrEmail=admin@example.com
@@ -195,7 +196,7 @@ func (hs *HttpServer) registerRoutes() {
// Data sources
r.Group("/datasources", func() {
r.Get("/", GetDataSources)
r.Get("/", wrap(GetDataSources))
r.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), AddDataSource)
r.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource))
r.Delete("/:id", DeleteDataSourceById)
+4 -4
View File
@@ -11,12 +11,11 @@ import (
"github.com/grafana/grafana/pkg/util"
)
func GetDataSources(c *middleware.Context) {
func GetDataSources(c *middleware.Context) Response {
query := m.GetDataSourcesQuery{OrgId: c.OrgId}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to query datasources", err)
return
return ApiError(500, "Failed to query datasources", err)
}
result := make(dtos.DataSourceList, 0)
@@ -46,7 +45,8 @@ func GetDataSources(c *middleware.Context) {
}
sort.Sort(result)
c.JSON(200, result)
return Json(200, &result)
}
func GetDataSourceById(c *middleware.Context) Response {
+25 -6
View File
@@ -59,7 +59,9 @@ func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := &scenarioContext{}
sc := &scenarioContext{
url: url,
}
viewsPath, _ := filepath.Abs("../../public/views")
sc.m = macaron.New()
@@ -71,16 +73,18 @@ func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
sc.m.Use(middleware.GetContextHandler())
sc.m.Use(middleware.Sessioner(&session.Options{}))
sc.defaultHandler = func(c *middleware.Context) {
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
sc.context = c
sc.context.UserId = TestUserID
sc.context.OrgId = TestOrgID
sc.context.OrgRole = models.ROLE_EDITOR
if sc.handlerFunc != nil {
sc.handlerFunc(sc.context)
return sc.handlerFunc(sc.context)
}
}
sc.m.SetAutoHead(true)
return nil
})
sc.m.Get(url, sc.defaultHandler)
fn(sc)
@@ -96,6 +100,20 @@ func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext {
return sc
}
func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map[string]string) *scenarioContext {
sc.resp = httptest.NewRecorder()
req, err := http.NewRequest(method, url, nil)
q := req.URL.Query()
for k, v := range queryParams {
q.Add(k, v)
}
req.URL.RawQuery = q.Encode()
So(err, ShouldBeNil)
sc.req = req
return sc
}
type scenarioContext struct {
m *macaron.Macaron
context *middleware.Context
@@ -103,6 +121,7 @@ type scenarioContext struct {
handlerFunc handlerFunc
defaultHandler macaron.Handler
req *http.Request
url string
}
func (sc *scenarioContext) exec() {
@@ -110,4 +129,4 @@ func (sc *scenarioContext) exec() {
}
type scenarioFunc func(c *scenarioContext)
type handlerFunc func(c *middleware.Context)
type handlerFunc func(c *middleware.Context) Response
+34 -2
View File
@@ -210,14 +210,46 @@ func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand)
// GET /api/users
func SearchUsers(c *middleware.Context) Response {
query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 1000}
if err := bus.Dispatch(&query); err != nil {
query, err := searchUser(c)
if err != nil {
return ApiError(500, "Failed to fetch users", err)
}
return Json(200, query.Result.Users)
}
// GET /api/paged-users
func SearchUsersWithPaging(c *middleware.Context) Response {
query, err := searchUser(c)
if err != nil {
return ApiError(500, "Failed to fetch users", err)
}
return Json(200, query.Result)
}
func searchUser(c *middleware.Context) (*m.SearchUsersQuery, error) {
perPage := c.QueryInt("perpage")
if perPage <= 0 {
perPage = 1000
}
page := c.QueryInt("page")
if page < 1 {
page = 1
}
query := &m.SearchUsersQuery{Query: "", Page: page, Limit: perPage}
if err := bus.Dispatch(query); err != nil {
return nil, err
}
query.Result.Page = page
query.Result.PerPage = perPage
return query, nil
}
func SetHelpFlag(c *middleware.Context) Response {
flag := c.ParamsInt64(":id")
+109
View File
@@ -0,0 +1,109 @@
package api
import (
"testing"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
. "github.com/smartystreets/goconvey/convey"
)
func TestUserApiEndpoint(t *testing.T) {
Convey("Given a user is logged in", t, func() {
mockResult := models.SearchUserQueryResult{
Users: []*models.UserSearchHitDTO{
{Name: "user1"},
{Name: "user2"},
},
TotalCount: 2,
}
loggedInUserScenario("When calling GET on", "/api/users", func(sc *scenarioContext) {
var sentLimit int
var sendPage int
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
query.Result = mockResult
sentLimit = query.Limit
sendPage = query.Page
return nil
})
sc.handlerFunc = SearchUsers
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sentLimit, ShouldEqual, 1000)
So(sendPage, ShouldEqual, 1)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(len(respJSON.MustArray()), ShouldEqual, 2)
})
loggedInUserScenario("When calling GET with page and limit querystring parameters on", "/api/users", func(sc *scenarioContext) {
var sentLimit int
var sendPage int
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
query.Result = mockResult
sentLimit = query.Limit
sendPage = query.Page
return nil
})
sc.handlerFunc = SearchUsers
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
So(sentLimit, ShouldEqual, 10)
So(sendPage, ShouldEqual, 2)
})
loggedInUserScenario("When calling GET on", "/api/users/search", func(sc *scenarioContext) {
var sentLimit int
var sendPage int
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
query.Result = mockResult
sentLimit = query.Limit
sendPage = query.Page
return nil
})
sc.handlerFunc = SearchUsersWithPaging
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
So(sentLimit, ShouldEqual, 1000)
So(sendPage, ShouldEqual, 1)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(respJSON.Get("totalCount").MustInt(), ShouldEqual, 2)
So(len(respJSON.Get("users").MustArray()), ShouldEqual, 2)
})
loggedInUserScenario("When calling GET with page and perpage querystring parameters on", "/api/users/search", func(sc *scenarioContext) {
var sentLimit int
var sendPage int
bus.AddHandler("test", func(query *models.SearchUsersQuery) error {
query.Result = mockResult
sentLimit = query.Limit
sendPage = query.Page
return nil
})
sc.handlerFunc = SearchUsersWithPaging
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
So(sentLimit, ShouldEqual, 10)
So(sendPage, ShouldEqual, 2)
})
})
}