mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
4563fc48af
commit
96cbe70b14
@ -28,7 +28,7 @@ API Tokens can be used with Organization HTTP API to get users of specific organ
|
||||
|
||||
## Search Users
|
||||
|
||||
`GET /api/users?perpage=10&page=1`
|
||||
`GET /api/users?perpage=10&page=1&sort=login-asc,email-asc`
|
||||
|
||||
**Required permissions**
|
||||
|
||||
@ -49,6 +49,8 @@ Authorization: Basic YWRtaW46YWRtaW4=
|
||||
|
||||
Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. Requires basic authentication and that the authenticated user is a Grafana Admin.
|
||||
|
||||
The `sort` param is an optional comma separated list of options to order the search result. Accepted values for the sort filter are: `login-asc`, `login-desc`, `email-asc`, `email-desc`, `name-asc`, `name-desc`, `lastSeenAtAge-asc`, `lastSeenAtAge-desc`. By default, if `sort` is not specified, the user list will be ordered by `login`, `email` in ascending order.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
@ -83,7 +85,7 @@ Content-Type: application/json
|
||||
|
||||
## Search Users with Paging
|
||||
|
||||
`GET /api/users/search?perpage=10&page=1&query=mygraf`
|
||||
`GET /api/users/search?perpage=10&page=1&query=mygraf&sort=login-asc,email-asc`
|
||||
|
||||
**Required permissions**
|
||||
|
||||
@ -104,6 +106,8 @@ Authorization: Basic YWRtaW46YWRtaW4=
|
||||
|
||||
Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. The `totalCount` field in the response can be used for pagination of the user list E.g. if `totalCount` is equal to 100 users and the `perpage` parameter is set to 10 then there are 10 pages of users. The `query` parameter is optional and it will return results where the query value is contained in one of the `name`, `login` or `email` fields. Query values with spaces need to be URL encoded e.g. `query=Jane%20Doe`.
|
||||
|
||||
The `sort` param is an optional comma separated list of options to order the search result. Accepted values for the sort filter are: `login-asc`, `login-desc`, `email-asc`, `email-desc`, `name-asc`, `name-desc`, `lastSeenAtAge-asc`, `lastSeenAtAge-desc`. By default, if `sort` is not specified, the user list will be ordered by `login`, `email` in ascending order.
|
||||
|
||||
Requires basic authentication and that the authenticated user is a Grafana Admin.
|
||||
|
||||
**Example Response**:
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/searchusers/sortopts"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
@ -236,12 +237,18 @@ func (hs *HTTPServer) SearchOrgUsers(c *contextmodel.ReqContext) response.Respon
|
||||
page = 1
|
||||
}
|
||||
|
||||
sortOpts, err := sortopts.ParseSortQueryParam(c.Query("sort"))
|
||||
if err != nil {
|
||||
return response.Err(err)
|
||||
}
|
||||
|
||||
result, err := hs.searchOrgUsersHelper(c, &org.SearchOrgUsersQuery{
|
||||
OrgID: orgID,
|
||||
Query: c.Query("query"),
|
||||
Page: page,
|
||||
Limit: perPage,
|
||||
User: c.SignedInUser,
|
||||
OrgID: orgID,
|
||||
Query: c.Query("query"),
|
||||
Page: page,
|
||||
Limit: perPage,
|
||||
User: c.SignedInUser,
|
||||
SortOpts: sortOpts,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@ -264,12 +271,18 @@ func (hs *HTTPServer) SearchOrgUsersWithPaging(c *contextmodel.ReqContext) respo
|
||||
page = 1
|
||||
}
|
||||
|
||||
sortOpts, err := sortopts.ParseSortQueryParam(c.Query("sort"))
|
||||
if err != nil {
|
||||
return response.Err(err)
|
||||
}
|
||||
|
||||
query := &org.SearchOrgUsersQuery{
|
||||
OrgID: c.SignedInUser.GetOrgID(),
|
||||
Query: c.Query("query"),
|
||||
Page: page,
|
||||
Limit: perPage,
|
||||
User: c.SignedInUser,
|
||||
OrgID: c.SignedInUser.GetOrgID(),
|
||||
Query: c.Query("query"),
|
||||
Page: page,
|
||||
Limit: perPage,
|
||||
User: c.SignedInUser,
|
||||
SortOpts: sortOpts,
|
||||
}
|
||||
|
||||
result, err := hs.searchOrgUsersHelper(c, query)
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/models/roletype"
|
||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||
"github.com/grafana/grafana/pkg/services/search/model"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
@ -178,11 +179,12 @@ type GetOrgUsersQuery struct {
|
||||
}
|
||||
|
||||
type SearchOrgUsersQuery struct {
|
||||
UserID int64 `xorm:"user_id"`
|
||||
OrgID int64 `xorm:"org_id"`
|
||||
Query string
|
||||
Page int
|
||||
Limit int
|
||||
UserID int64 `xorm:"user_id"`
|
||||
OrgID int64 `xorm:"org_id"`
|
||||
Query string
|
||||
Page int
|
||||
Limit int
|
||||
SortOpts []model.SortOption
|
||||
// Flag used to allow oss edition to query users without access control
|
||||
DontEnforceAccessControl bool
|
||||
|
||||
|
@ -546,7 +546,7 @@ func (ss *sqlStore) SearchOrgUsers(ctx context.Context, query *org.SearchOrgUser
|
||||
}
|
||||
err := ss.db.WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||
sess := dbSession.Table("org_user")
|
||||
sess.Join("INNER", ss.dialect.Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", ss.dialect.Quote("user")))
|
||||
sess.Join("INNER", []string{ss.dialect.Quote("user"), "u"}, "org_user.user_id=u.id")
|
||||
|
||||
whereConditions := make([]string, 0)
|
||||
whereParams := make([]any, 0)
|
||||
@ -559,7 +559,7 @@ func (ss *sqlStore) SearchOrgUsers(ctx context.Context, query *org.SearchOrgUser
|
||||
whereParams = append(whereParams, query.UserID)
|
||||
}
|
||||
|
||||
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = ?", ss.dialect.Quote("user")))
|
||||
whereConditions = append(whereConditions, "u.is_service_account = ?")
|
||||
whereParams = append(whereParams, ss.dialect.BooleanStr(false))
|
||||
|
||||
if query.User == nil {
|
||||
@ -593,16 +593,25 @@ func (ss *sqlStore) SearchOrgUsers(ctx context.Context, query *org.SearchOrgUser
|
||||
sess.Cols(
|
||||
"org_user.org_id",
|
||||
"org_user.user_id",
|
||||
"user.email",
|
||||
"user.name",
|
||||
"user.login",
|
||||
"u.email",
|
||||
"u.name",
|
||||
"u.login",
|
||||
"org_user.role",
|
||||
"user.last_seen_at",
|
||||
"user.created",
|
||||
"user.updated",
|
||||
"user.is_disabled",
|
||||
"u.last_seen_at",
|
||||
"u.created",
|
||||
"u.updated",
|
||||
"u.is_disabled",
|
||||
)
|
||||
sess.Asc("user.email", "user.login")
|
||||
|
||||
if len(query.SortOpts) > 0 {
|
||||
for i := range query.SortOpts {
|
||||
for j := range query.SortOpts[i].Filter {
|
||||
sess.OrderBy(query.SortOpts[i].Filter[j].OrderBy())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sess.Asc("u.login", "u.email")
|
||||
}
|
||||
|
||||
if err := sess.Find(&result.OrgUsers); err != nil {
|
||||
return err
|
||||
@ -611,7 +620,7 @@ func (ss *sqlStore) SearchOrgUsers(ctx context.Context, query *org.SearchOrgUser
|
||||
// get total count
|
||||
orgUser := org.OrgUser{}
|
||||
countSess := dbSession.Table("org_user").
|
||||
Join("INNER", ss.dialect.Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", ss.dialect.Quote("user")))
|
||||
Join("INNER", []string{ss.dialect.Quote("user"), "u"}, "org_user.user_id=u.id")
|
||||
|
||||
if len(whereConditions) > 0 {
|
||||
countSess.Where(strings.Join(whereConditions, " AND "), whereParams...)
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotaimpl"
|
||||
"github.com/grafana/grafana/pkg/services/searchusers/sortopts"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
@ -404,6 +405,42 @@ func TestIntegrationOrgUserDataAccess(t *testing.T) {
|
||||
require.Equal(t, len(result.OrgUsers), 1)
|
||||
require.Equal(t, result.OrgUsers[0].Email, ac1.Email)
|
||||
})
|
||||
t.Run("Can get organization users with custom ordering login-asc", func(t *testing.T) {
|
||||
sortOpts, err := sortopts.ParseSortQueryParam("login-asc,email-asc")
|
||||
require.NoError(t, err)
|
||||
query := org.SearchOrgUsersQuery{
|
||||
OrgID: ac1.OrgID,
|
||||
SortOpts: sortOpts,
|
||||
User: &user.SignedInUser{
|
||||
OrgID: ac1.OrgID,
|
||||
Permissions: map[int64]map[string][]string{ac1.OrgID: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}}},
|
||||
},
|
||||
}
|
||||
result, err := orgUserStore.SearchOrgUsers(context.Background(), &query)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(result.OrgUsers), 2)
|
||||
require.Equal(t, result.OrgUsers[0].Email, ac1.Email)
|
||||
require.Equal(t, result.OrgUsers[1].Email, ac2.Email)
|
||||
})
|
||||
t.Run("Can get organization users with custom ordering login-desc", func(t *testing.T) {
|
||||
sortOpts, err := sortopts.ParseSortQueryParam("login-desc,email-asc")
|
||||
require.NoError(t, err)
|
||||
query := org.SearchOrgUsersQuery{
|
||||
OrgID: ac1.OrgID,
|
||||
SortOpts: sortOpts,
|
||||
User: &user.SignedInUser{
|
||||
OrgID: ac1.OrgID,
|
||||
Permissions: map[int64]map[string][]string{ac1.OrgID: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}}},
|
||||
},
|
||||
}
|
||||
result, err := orgUserStore.SearchOrgUsers(context.Background(), &query)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(result.OrgUsers), 2)
|
||||
require.Equal(t, result.OrgUsers[0].Email, ac2.Email)
|
||||
require.Equal(t, result.OrgUsers[1].Email, ac1.Email)
|
||||
})
|
||||
t.Run("Cannot update role so no one is admin user", func(t *testing.T) {
|
||||
remCmd := org.RemoveOrgUserCommand{OrgID: ac1.OrgID, UserID: ac2.ID, ShouldDeleteOrphanedUser: true}
|
||||
err := orgUserStore.RemoveOrgUser(context.Background(), &remCmd)
|
||||
|
@ -2,10 +2,44 @@ package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||
)
|
||||
|
||||
// FilterWhere limits the set of dashboard IDs to the dashboards for
|
||||
// which the filter is applicable. Results where the first value is
|
||||
// an empty string are discarded.
|
||||
type FilterWhere interface {
|
||||
Where() (string, []any)
|
||||
}
|
||||
|
||||
// FilterWith returns any recursive CTE queries (if supported)
|
||||
// and their parameters
|
||||
type FilterWith interface {
|
||||
With() (string, []any)
|
||||
}
|
||||
|
||||
// FilterGroupBy should be used after performing an outer join on the
|
||||
// search result to ensure there is only one of each ID in the results.
|
||||
// The id column must be present in the result.
|
||||
type FilterGroupBy interface {
|
||||
GroupBy() (string, []any)
|
||||
}
|
||||
|
||||
// FilterOrderBy provides an ordering for the search result.
|
||||
type FilterOrderBy interface {
|
||||
OrderBy() string
|
||||
}
|
||||
|
||||
// FilterLeftJoin adds the returned string as a "LEFT OUTER JOIN" to
|
||||
// allow for fetching extra columns from a table outside of the
|
||||
// dashboard column.
|
||||
type FilterLeftJoin interface {
|
||||
LeftJoin() string
|
||||
}
|
||||
|
||||
type FilterSelect interface {
|
||||
Select() string
|
||||
}
|
||||
|
||||
type SortOption struct {
|
||||
Name string
|
||||
DisplayName string
|
||||
@ -16,7 +50,7 @@ type SortOption struct {
|
||||
}
|
||||
|
||||
type SortOptionFilter interface {
|
||||
searchstore.FilterOrderBy
|
||||
FilterOrderBy
|
||||
}
|
||||
|
||||
type HitType string
|
||||
|
@ -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 {
|
||||
|
91
pkg/services/searchusers/sortopts/sortopts.go
Normal file
91
pkg/services/searchusers/sortopts/sortopts.go
Normal file
@ -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
|
||||
}
|
@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/search/model"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
)
|
||||
|
||||
@ -61,11 +62,11 @@ func (b *Builder) buildSelect() {
|
||||
folder.title AS folder_title `)
|
||||
|
||||
for _, f := range b.Filters {
|
||||
if f, ok := f.(FilterSelect); ok {
|
||||
if f, ok := f.(model.FilterSelect); ok {
|
||||
b.sql.WriteString(fmt.Sprintf(", %s", f.Select()))
|
||||
}
|
||||
|
||||
if f, ok := f.(FilterWith); ok {
|
||||
if f, ok := f.(model.FilterWith); ok {
|
||||
recQuery, recQueryParams = f.With()
|
||||
}
|
||||
}
|
||||
@ -98,14 +99,14 @@ func (b *Builder) applyFilters() (ordering string) {
|
||||
orders := []string{}
|
||||
|
||||
for _, f := range b.Filters {
|
||||
if f, ok := f.(FilterLeftJoin); ok {
|
||||
if f, ok := f.(model.FilterLeftJoin); ok {
|
||||
s := f.LeftJoin()
|
||||
if s != "" {
|
||||
joins = append(joins, fmt.Sprintf(" LEFT OUTER JOIN %s ", s))
|
||||
}
|
||||
}
|
||||
|
||||
if f, ok := f.(FilterWhere); ok {
|
||||
if f, ok := f.(model.FilterWhere); ok {
|
||||
sql, params := f.Where()
|
||||
if sql != "" {
|
||||
wheres = append(wheres, sql)
|
||||
@ -113,7 +114,7 @@ func (b *Builder) applyFilters() (ordering string) {
|
||||
}
|
||||
}
|
||||
|
||||
if f, ok := f.(FilterGroupBy); ok {
|
||||
if f, ok := f.(model.FilterGroupBy); ok {
|
||||
sql, params := f.GroupBy()
|
||||
if sql != "" {
|
||||
groups = append(groups, sql)
|
||||
@ -121,8 +122,8 @@ func (b *Builder) applyFilters() (ordering string) {
|
||||
}
|
||||
}
|
||||
|
||||
if f, ok := f.(FilterOrderBy); ok {
|
||||
if f, ok := f.(FilterLeftJoin); ok {
|
||||
if f, ok := f.(model.FilterOrderBy); ok {
|
||||
if f, ok := f.(model.FilterLeftJoin); ok {
|
||||
orderJoins = append(orderJoins, fmt.Sprintf(" LEFT OUTER JOIN %s ", f.LeftJoin()))
|
||||
}
|
||||
orders = append(orders, f.OrderBy())
|
||||
|
@ -5,45 +5,10 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/search/model"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
)
|
||||
|
||||
// FilterWhere limits the set of dashboard IDs to the dashboards for
|
||||
// which the filter is applicable. Results where the first value is
|
||||
// an empty string are discarded.
|
||||
type FilterWhere interface {
|
||||
Where() (string, []any)
|
||||
}
|
||||
|
||||
// FilterWith returns any recursive CTE queries (if supported)
|
||||
// and their parameters
|
||||
type FilterWith interface {
|
||||
With() (string, []any)
|
||||
}
|
||||
|
||||
// FilterGroupBy should be used after performing an outer join on the
|
||||
// search result to ensure there is only one of each ID in the results.
|
||||
// The id column must be present in the result.
|
||||
type FilterGroupBy interface {
|
||||
GroupBy() (string, []any)
|
||||
}
|
||||
|
||||
// FilterOrderBy provides an ordering for the search result.
|
||||
type FilterOrderBy interface {
|
||||
OrderBy() string
|
||||
}
|
||||
|
||||
// FilterLeftJoin adds the returned string as a "LEFT OUTER JOIN" to
|
||||
// allow for fetching extra columns from a table outside of the
|
||||
// dashboard column.
|
||||
type FilterLeftJoin interface {
|
||||
LeftJoin() string
|
||||
}
|
||||
|
||||
type FilterSelect interface {
|
||||
Select() string
|
||||
}
|
||||
|
||||
const (
|
||||
TypeFolder = "dash-folder"
|
||||
TypeDashboard = "dash-db"
|
||||
@ -220,7 +185,7 @@ func sqlUIDin(column string, uids []string) (string, []any) {
|
||||
type FolderWithAlertsFilter struct {
|
||||
}
|
||||
|
||||
var _ FilterWhere = &FolderWithAlertsFilter{}
|
||||
var _ model.FilterWhere = &FolderWithAlertsFilter{}
|
||||
|
||||
func (f FolderWithAlertsFilter) Where() (string, []any) {
|
||||
return "EXISTS (SELECT 1 FROM alert_rule WHERE alert_rule.namespace_uid = dashboard.uid)", nil
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||
"github.com/grafana/grafana/pkg/services/search/model"
|
||||
)
|
||||
|
||||
type HelpFlags1 uint64
|
||||
@ -112,6 +113,7 @@ type SearchUsersQuery struct {
|
||||
Page int
|
||||
Limit int
|
||||
AuthModule string
|
||||
SortOpts []model.SortOption
|
||||
Filters []Filter
|
||||
|
||||
IsDisabled *bool
|
||||
|
@ -679,7 +679,17 @@ func (ss *sqlStore) Search(ctx context.Context, query *user.SearchUsersQuery) (*
|
||||
}
|
||||
|
||||
sess.Cols("u.id", "u.email", "u.name", "u.login", "u.is_admin", "u.is_disabled", "u.last_seen_at", "user_auth.auth_module")
|
||||
sess.Asc("u.login", "u.email")
|
||||
|
||||
if len(query.SortOpts) > 0 {
|
||||
for i := range query.SortOpts {
|
||||
for j := range query.SortOpts[i].Filter {
|
||||
sess.OrderBy(query.SortOpts[i].Filter[j].OrderBy())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sess.Asc("u.login", "u.email")
|
||||
}
|
||||
|
||||
if err := sess.Find(&result.Users); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/org/orgimpl"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotaimpl"
|
||||
"github.com/grafana/grafana/pkg/services/searchusers/sortopts"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
@ -822,6 +823,31 @@ func TestIntegrationUserDataAccess(t *testing.T) {
|
||||
require.Nil(t, err)
|
||||
require.Len(t, queryResult.Users, 1)
|
||||
require.EqualValues(t, queryResult.TotalCount, 1)
|
||||
|
||||
// Custom ordering
|
||||
sortOpts, err := sortopts.ParseSortQueryParam("login-asc,email-asc")
|
||||
require.NoError(t, err)
|
||||
query = user.SearchUsersQuery{Query: "", Page: 1, Limit: 3, SignedInUser: usr, SortOpts: sortOpts}
|
||||
queryResult, err = userStore.Search(context.Background(), &query)
|
||||
|
||||
require.Nil(t, err)
|
||||
require.Len(t, queryResult.Users, 3)
|
||||
require.EqualValues(t, queryResult.TotalCount, 5)
|
||||
for i := 0; i < 3; i++ {
|
||||
require.Equal(t, fmt.Sprint("loginuser", i), queryResult.Users[i].Login)
|
||||
}
|
||||
|
||||
sortOpts2, err := sortopts.ParseSortQueryParam("login-desc,email-asc")
|
||||
require.NoError(t, err)
|
||||
query = user.SearchUsersQuery{Query: "", Page: 1, Limit: 3, SignedInUser: usr, SortOpts: sortOpts2}
|
||||
queryResult, err = userStore.Search(context.Background(), &query)
|
||||
|
||||
require.Nil(t, err)
|
||||
require.Len(t, queryResult.Users, 3)
|
||||
require.EqualValues(t, queryResult.TotalCount, 5)
|
||||
for i := 0; i < 3; i++ {
|
||||
require.Equal(t, fmt.Sprint("loginuser", 4-i), queryResult.Users[i].Login)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Can get logged in user projection", func(t *testing.T) {
|
||||
|
Loading…
Reference in New Issue
Block a user