User: Support sort query param for user and org user, search endpoints (#75229)

* User: Add sort option to user search

* Switch to an approach that uses the dashboard search options

* Cable user sort on the org endpoint

* Alias user table with u in org store

* Add test and cover orgs/:orgID/users/search endpoint

* Add test to userimpl store

* Simplify the store_test with sortopts.ParseSortQueryParam

* Account for PR feedback

* Positive check

* Update docs

* Update docs

* Switch to ErrOrFallback

Co-authored-by: Karl Persson <kalle.persson@grafana.com>

---------

Co-authored-by: Karl Persson <kalle.persson@grafana.com>
This commit is contained in:
Gabriel MABILLE
2023-09-28 10:16:18 +02:00
committed by GitHub
co-authored by Karl Persson
parent 4563fc48af
commit 96cbe70b14
13 changed files with 279 additions and 78 deletions
+9 -2
View File
@@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana/pkg/api/response"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/searchusers/sortopts"
"github.com/grafana/grafana/pkg/services/user"
)
@@ -42,7 +43,7 @@ func ProvideUsersService(searchUserFilter user.SearchUserFilter, userService use
func (s *OSSService) SearchUsers(c *contextmodel.ReqContext) response.Response {
result, err := s.SearchUser(c)
if err != nil {
return response.Error(500, "Failed to fetch users", err)
return response.ErrOrFallback(500, "Failed to fetch users", err)
}
return response.JSON(http.StatusOK, result.Users)
@@ -61,7 +62,7 @@ func (s *OSSService) SearchUsers(c *contextmodel.ReqContext) response.Response {
func (s *OSSService) SearchUsersWithPaging(c *contextmodel.ReqContext) response.Response {
result, err := s.SearchUser(c)
if err != nil {
return response.Error(500, "Failed to fetch users", err)
return response.ErrOrFallback(500, "Failed to fetch users", err)
}
return response.JSON(http.StatusOK, result)
@@ -87,6 +88,11 @@ func (s *OSSService) SearchUser(c *contextmodel.ReqContext) (*user.SearchUserQue
}
}
sortOpts, err := sortopts.ParseSortQueryParam(c.Query("sort"))
if err != nil {
return nil, err
}
query := &user.SearchUsersQuery{
// added SignedInUser to the query, as to only list the users that the user has permission to read
SignedInUser: c.SignedInUser,
@@ -94,6 +100,7 @@ func (s *OSSService) SearchUser(c *contextmodel.ReqContext) (*user.SearchUserQue
Filters: filters,
Page: page,
Limit: perPage,
SortOpts: sortOpts,
}
res, err := s.userService.Search(c.Req.Context(), query)
if err != nil {
@@ -0,0 +1,91 @@
package sortopts
import (
"fmt"
"sort"
"strings"
"github.com/grafana/grafana/pkg/services/search/model"
"github.com/grafana/grafana/pkg/util/errutil"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
var (
// SortOptionsByQueryParam is a map to translate the "sort" query param values to SortOption(s)
SortOptionsByQueryParam = map[string]model.SortOption{
"login-asc": newSortOption("login", false, 0),
"login-desc": newSortOption("login", true, 0),
"email-asc": newSortOption("email", false, 1),
"email-desc": newSortOption("email", true, 1),
"name-asc": newSortOption("name", false, 2),
"name-desc": newSortOption("name", true, 2),
"lastSeenAtAge-asc": newTimeSortOption("last_seen_at", false, 3),
"lastSeenAtAge-desc": newTimeSortOption("last_seen_at", true, 3),
}
ErrorUnknownSortingOption = errutil.BadRequest("unknown sorting option")
)
type Sorter struct {
Field string
Descending bool
}
func (s Sorter) OrderBy() string {
if s.Descending {
return fmt.Sprintf("u.%v DESC", s.Field)
}
return fmt.Sprintf("u.%v ASC", s.Field)
}
func newSortOption(field string, desc bool, index int) model.SortOption {
direction := "asc"
description := ("A-Z")
if desc {
direction = "desc"
description = ("Z-A")
}
return model.SortOption{
Name: fmt.Sprintf("%v-%v", field, direction),
DisplayName: fmt.Sprintf("%v (%v)", cases.Title(language.Und).String(field), description),
Description: fmt.Sprintf("Sort %v in an alphabetically %vending order", field, direction),
Index: index,
Filter: []model.SortOptionFilter{Sorter{Field: field, Descending: desc}},
}
}
func newTimeSortOption(field string, desc bool, index int) model.SortOption {
direction := "asc"
description := ("Oldest-Newest")
if desc {
direction = "desc"
description = ("Newest-Oldest")
}
return model.SortOption{
Name: fmt.Sprintf("%v-%v", field, direction),
DisplayName: fmt.Sprintf("%v (%v)", cases.Title(language.Und).String(field), description),
Description: fmt.Sprintf("Sort %v in an alphabetically %vending order", field, direction),
Index: index,
Filter: []model.SortOptionFilter{Sorter{Field: field, Descending: desc}},
}
}
// ParseSortQueryParam parses the "sort" query param and returns an ordered list of SortOption(s)
func ParseSortQueryParam(param string) ([]model.SortOption, error) {
opts := []model.SortOption{}
if param != "" {
optsStr := strings.Split(param, ",")
for i := range optsStr {
if opt, ok := SortOptionsByQueryParam[optsStr[i]]; !ok {
return nil, ErrorUnknownSortingOption.Errorf("%v option unknown", optsStr[i])
} else {
opts = append(opts, opt)
}
}
sort.Slice(opts, func(i, j int) bool {
return opts[i].Index < opts[j].Index || (opts[i].Index == opts[j].Index && opts[i].Name < opts[j].Name)
})
}
return opts, nil
}