From e96900cb58059a883c0bcde72cdfde37280a60c9 Mon Sep 17 00:00:00 2001 From: stonezdj Date: Thu, 15 Dec 2022 16:16:33 +0800 Subject: [PATCH] Sort user and usergroup by most match order fixes #17859 Signed-off-by: stonezdj --- src/common/utils/utils.go | 26 +++++++++++ src/common/utils/utils_test.go | 42 +++++++++++++++++ src/server/v2.0/handler/user.go | 4 ++ src/server/v2.0/handler/usergroup.go | 25 ++-------- src/server/v2.0/handler/usergroup_test.go | 56 ----------------------- 5 files changed, 76 insertions(+), 77 deletions(-) delete mode 100644 src/server/v2.0/handler/usergroup_test.go diff --git a/src/common/utils/utils.go b/src/common/utils/utils.go index afb97a7abae..789807dfd4e 100644 --- a/src/common/utils/utils.go +++ b/src/common/utils/utils.go @@ -300,3 +300,29 @@ func NextSchedule(cron string, curTime time.Time) time.Time { func CronParser() cronlib.Parser { return cronlib.NewParser(cronlib.Second | cronlib.Minute | cronlib.Hour | cronlib.Dom | cronlib.Month | cronlib.Dow) } + +// MostMatchSorter is a sorter for the most match, usually invoked in sort Less function +// usage: +// +// sort.Slice(input, func(i, j int) bool { +// return MostMatchSorter(input[i].GroupName, input[j].GroupName, matchWord) +// }) +// a is the field to be used for sorting, b is the other field, matchWord is the word to be matched +// the return value is true if a is less than b +// for example, search with "user", input is {"harbor_user", "user", "users, "admin_user"} +// it returns with this order {"user", "users", "admin_user", "harbor_user"} + +func MostMatchSorter(a, b string, matchWord string) bool { + // exact match always first + if a == matchWord { + return true + } + if b == matchWord { + return false + } + // sort by length, then sort by alphabet + if len(a) == len(b) { + return a < b + } + return len(a) < len(b) +} diff --git a/src/common/utils/utils_test.go b/src/common/utils/utils_test.go index 3f6416c3fac..0258069942d 100644 --- a/src/common/utils/utils_test.go +++ b/src/common/utils/utils_test.go @@ -18,6 +18,7 @@ import ( "encoding/base64" "net/http/httptest" "reflect" + "sort" "strconv" "strings" "testing" @@ -394,3 +395,44 @@ func TestNextSchedule(t *testing.T) { }) } } + +type UserGroupSearchItem struct { + GroupName string +} + +func Test_sortMostMatch(t *testing.T) { + type args struct { + input []*UserGroupSearchItem + matchWord string + expected []*UserGroupSearchItem + } + tests := []struct { + name string + args args + }{ + {"normal", args{[]*UserGroupSearchItem{ + {GroupName: "user"}, {GroupName: "harbor_user"}, {GroupName: "admin_user"}, {GroupName: "users"}, + }, "user", []*UserGroupSearchItem{ + {GroupName: "user"}, {GroupName: "users"}, {GroupName: "admin_user"}, {GroupName: "harbor_user"}, + }}}, + {"duplicate_item", args{[]*UserGroupSearchItem{ + {GroupName: "user"}, {GroupName: "user"}, {GroupName: "harbor_user"}, {GroupName: "admin_user"}, {GroupName: "users"}, + }, "user", []*UserGroupSearchItem{ + {GroupName: "user"}, {GroupName: "user"}, {GroupName: "users"}, {GroupName: "admin_user"}, {GroupName: "harbor_user"}, + }}}, + {"miss_exact_match", args{[]*UserGroupSearchItem{ + {GroupName: "harbor_user"}, {GroupName: "admin_user"}, {GroupName: "users"}, + }, "user", []*UserGroupSearchItem{ + {GroupName: "users"}, {GroupName: "admin_user"}, {GroupName: "harbor_user"}, + }}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + sort.Slice(tt.args.input, func(i, j int) bool { + return MostMatchSorter(tt.args.input[i].GroupName, tt.args.input[j].GroupName, tt.args.matchWord) + }) + assert.True(t, reflect.DeepEqual(tt.args.input, tt.args.expected)) + }) + } +} diff --git a/src/server/v2.0/handler/user.go b/src/server/v2.0/handler/user.go index cf9bfc57351..6df2073f67c 100644 --- a/src/server/v2.0/handler/user.go +++ b/src/server/v2.0/handler/user.go @@ -18,6 +18,7 @@ import ( "context" "fmt" "regexp" + "sort" "strings" "time" @@ -290,6 +291,9 @@ func (u *usersAPI) SearchUsers(ctx context.Context, params operation.SearchUsers m := &model.User{User: us} result = append(result, m.ToSearchRespItem()) } + sort.Slice(result, func(i, j int) bool { + return utils.MostMatchSorter(result[i].Username, result[j].Username, params.Username) + }) return operation.NewSearchUsersOK(). WithXTotalCount(total). WithPayload(result). diff --git a/src/server/v2.0/handler/usergroup.go b/src/server/v2.0/handler/usergroup.go index f95cb4aa15b..f26eefc52cf 100644 --- a/src/server/v2.0/handler/usergroup.go +++ b/src/server/v2.0/handler/usergroup.go @@ -24,6 +24,7 @@ import ( "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/rbac" + "github.com/goharbor/harbor/src/common/utils" ugCtl "github.com/goharbor/harbor/src/controller/usergroup" "github.com/goharbor/harbor/src/lib/config" "github.com/goharbor/harbor/src/lib/errors" @@ -208,28 +209,10 @@ func (u *userGroupAPI) SearchUserGroups(ctx context.Context, params operation.Se return u.SendError(ctx, err) } result := getUserGroupSearchItem(ug) - sortMostMatch(result, params.Groupname) + sort.Slice(result, func(i, j int) bool { + return utils.MostMatchSorter(result[i].GroupName, result[j].GroupName, params.Groupname) + }) return operation.NewSearchUserGroupsOK().WithXTotalCount(total). WithPayload(result). WithLink(u.Links(ctx, params.HTTPRequest.URL, total, query.PageNumber, query.PageSize).String()) } - -// sortMostMatch given a matchWord, sort the input by the most match, -// for example, search with "user", input is {"harbor_user", "user", "users, "admin_user"} -// it returns with this order {"user", "users", "admin_user", "harbor_user"} -func sortMostMatch(input []*models.UserGroupSearchItem, matchWord string) { - sort.Slice(input, func(i, j int) bool { - // exact match always first - if input[i].GroupName == matchWord { - return true - } - if input[j].GroupName == matchWord { - return false - } - // sort by length, then sort by alphabet - if len(input[i].GroupName) == len(input[j].GroupName) { - return input[i].GroupName < input[j].GroupName - } - return len(input[i].GroupName) < len(input[j].GroupName) - }) -} diff --git a/src/server/v2.0/handler/usergroup_test.go b/src/server/v2.0/handler/usergroup_test.go deleted file mode 100644 index c9fc39b2ce5..00000000000 --- a/src/server/v2.0/handler/usergroup_test.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright Project Harbor Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package handler - -import ( - "github.com/goharbor/harbor/src/server/v2.0/models" - "github.com/stretchr/testify/assert" - "reflect" - "testing" -) - -func Test_sortMostMatch(t *testing.T) { - type args struct { - input []*models.UserGroupSearchItem - matchWord string - expected []*models.UserGroupSearchItem - } - tests := []struct { - name string - args args - }{ - {"normal", args{[]*models.UserGroupSearchItem{ - {GroupName: "user"}, {GroupName: "harbor_user"}, {GroupName: "admin_user"}, {GroupName: "users"}, - }, "user", []*models.UserGroupSearchItem{ - {GroupName: "user"}, {GroupName: "users"}, {GroupName: "admin_user"}, {GroupName: "harbor_user"}, - }}}, - {"duplicate_item", args{[]*models.UserGroupSearchItem{ - {GroupName: "user"}, {GroupName: "user"}, {GroupName: "harbor_user"}, {GroupName: "admin_user"}, {GroupName: "users"}, - }, "user", []*models.UserGroupSearchItem{ - {GroupName: "user"}, {GroupName: "user"}, {GroupName: "users"}, {GroupName: "admin_user"}, {GroupName: "harbor_user"}, - }}}, - {"miss_exact_match", args{[]*models.UserGroupSearchItem{ - {GroupName: "harbor_user"}, {GroupName: "admin_user"}, {GroupName: "users"}, - }, "user", []*models.UserGroupSearchItem{ - {GroupName: "users"}, {GroupName: "admin_user"}, {GroupName: "harbor_user"}, - }}}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - sortMostMatch(tt.args.input, tt.args.matchWord) - assert.True(t, reflect.DeepEqual(tt.args.input, tt.args.expected)) - }) - } -}