[MM-56399][MM-56397][MM-56456][MM-56269] Various changes for user reporting for admins (#25839)

* [MM-56399] Add user count endpoint for reporting

* [MM-56397] Added search term to user report filter

* Missing translation

* [MM-56456] Rename up/down to prev/next for reporting cursoring

* [MM-56269] Add DeleteAt, MfaActive and AuthService fields to UserReport

* PR feedback

* Fix test

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Devin Binnie
2024-01-10 09:08:23 -05:00
коммит произвёл GitHub
родитель bb88b92b4c
Коммит 0a4e9eeb92
18 изменённых файлов: 365 добавлений и 97 удалений

Просмотреть файл

@@ -11892,6 +11892,24 @@ func (s *OpenTracingLayerUserStore) GetUnreadCountForChannel(userID string, chan
return result, err
}
func (s *OpenTracingLayerUserStore) GetUserCountForReport(filter *model.UserReportOptions) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetUserCountForReport")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.UserStore.GetUserCountForReport(filter)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "UserStore.GetUserReport")

Просмотреть файл

@@ -13577,6 +13577,27 @@ func (s *RetryLayerUserStore) GetUnreadCountForChannel(userID string, channelID
}
func (s *RetryLayerUserStore) GetUserCountForReport(filter *model.UserReportOptions) (int64, error) {
tries := 0
for {
result, err := s.UserStore.GetUserCountForReport(filter)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
tries := 0

Просмотреть файл

@@ -2268,12 +2268,56 @@ func (us SqlUserStore) RefreshPostStatsForUsers() error {
return nil
}
func applyUserReportFilter(query sq.SelectBuilder, filter *model.UserReportOptions, isPostgres bool) sq.SelectBuilder {
query = applyRoleFilter(query, filter.Role, isPostgres)
if filter.HasNoTeam {
query = query.Where(sq.Expr("u.Id NOT IN (SELECT UserId FROM TeamMembers WHERE DeleteAt = 0)"))
} else if filter.Team != "" {
query = query.Join("TeamMembers tm ON (tm.UserId = u.Id AND tm.DeleteAt = 0)").
Where(sq.Eq{"tm.TeamId": filter.Team})
}
if filter.HideActive {
query = query.Where(sq.Gt{"u.DeleteAt": 0})
}
if filter.HideInactive {
query = query.Where(sq.Eq{"u.DeleteAt": 0})
}
if strings.TrimSpace(filter.SearchTerm) != "" {
query = generateSearchQuery(query, strings.Fields(sanitizeSearchTerm(filter.SearchTerm, "*")), UserSearchTypeAll, isPostgres)
}
return query
}
func (us SqlUserStore) GetUserCountForReport(filter *model.UserReportOptions) (int64, error) {
isPostgres := us.DriverName() == model.DatabaseDriverPostgres
query := us.getQueryBuilder().
Select("COUNT(u.Id)").
From("Users u")
if isPostgres {
query = query.LeftJoin("Bots ON u.Id = Bots.UserId").Where("Bots.UserId IS NULL")
} else {
query = query.Where(sq.Expr("u.Id NOT IN (SELECT UserId FROM Bots)"))
}
query = applyUserReportFilter(query, filter, isPostgres)
queryStr, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "user_count_report_tosql")
}
var v int64
err = us.GetReplicaX().Get(&v, queryStr, args...)
if err != nil {
return 0, errors.Wrap(err, "failed to count Users for report")
}
return v, nil
}
func (us SqlUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
isPostgres := us.DriverName() == model.DatabaseDriverPostgres
selectColumns := []string{"u.Id", "u.LastLogin", "MAX(s.LastActivityAt) AS LastStatusAt"}
for _, column := range model.UserReportSortColumns {
selectColumns = append(selectColumns, "u."+column)
}
selectColumns := []string{"u.*", "MAX(s.LastActivityAt) AS LastStatusAt"}
if isPostgres {
selectColumns = append(selectColumns,
"MAX(ps.LastPostDate) AS LastPostDate",
@@ -2303,7 +2347,7 @@ func (us SqlUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.
// no need to apply any filtering and pagination if there are no
// previous element ID and value provided.
if filter.FromId != "" && filter.FromColumnValue != "" {
if (filter.Direction == "up" && !filter.SortDesc) || (filter.Direction == "down" && filter.SortDesc) {
if (filter.Direction == "prev" && !filter.SortDesc) || (filter.Direction == "next" && filter.SortDesc) {
sortDirection = "DESC"
query = query.Where(sq.Or{
@@ -2364,19 +2408,7 @@ func (us SqlUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.
}
}
query = applyRoleFilter(query, filter.Role, isPostgres)
if filter.HasNoTeam {
query = query.Where(sq.Expr("u.Id NOT IN (SELECT UserId FROM TeamMembers WHERE DeleteAt = 0)"))
} else if filter.Team != "" {
query = query.Join("TeamMembers tm ON (tm.UserId = u.Id AND tm.DeleteAt = 0)").
Where(sq.Eq{"tm.TeamId": filter.Team})
}
if filter.HideActive {
query = query.Where(sq.Gt{"u.DeleteAt": 0})
}
if filter.HideInactive {
query = query.Where(sq.Eq{"u.DeleteAt": 0})
}
query = applyUserReportFilter(query, filter, isPostgres)
parentQuery := query
// If we're going a page back...
@@ -2384,7 +2416,7 @@ func (us SqlUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.
// The way pagination works, we get the previous page's rows
// in reverse order. So, we use parent query on it to
// reverse the order in database itself.
if filter.Direction == "up" {
if filter.Direction == "prev" {
reverseSortDirection := "ASC"
if sortDirection == "ASC" {
reverseSortDirection = "DESC"

Просмотреть файл

@@ -481,6 +481,7 @@ type UserStore interface {
InsertUsers(users []*model.User) error
RefreshPostStatsForUsers() error
GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error)
GetUserCountForReport(filter *model.UserReportOptions) (int64, error)
}
type BotStore interface {

Просмотреть файл

@@ -1153,6 +1153,30 @@ func (_m *UserStore) GetUnreadCountForChannel(userID string, channelID string) (
return r0, r1
}
// GetUserCountForReport provides a mock function with given fields: filter
func (_m *UserStore) GetUserCountForReport(filter *model.UserReportOptions) (int64, error) {
ret := _m.Called(filter)
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(*model.UserReportOptions) (int64, error)); ok {
return rf(filter)
}
if rf, ok := ret.Get(0).(func(*model.UserReportOptions) int64); ok {
r0 = rf(filter)
} else {
r0 = ret.Get(0).(int64)
}
if rf, ok := ret.Get(1).(func(*model.UserReportOptions) error); ok {
r1 = rf(filter)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUserReport provides a mock function with given fields: filter
func (_m *UserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
ret := _m.Called(filter)

Просмотреть файл

@@ -6336,7 +6336,7 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
Direction: "down",
Direction: "next",
PageSize: 50,
FromColumnValue: users[10].Username,
FromId: users[10].Id,
@@ -6353,7 +6353,7 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
SortDesc: true,
Direction: "down",
Direction: "next",
PageSize: 50,
FromColumnValue: users[10].Username,
FromId: users[10].Id,
@@ -6369,7 +6369,7 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
userReport, err = ss.User().GetUserReport(&model.UserReportOptions{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
Direction: "up",
Direction: "prev",
PageSize: 50,
FromColumnValue: users[10].Username,
FromId: users[10].Id,
@@ -6386,7 +6386,7 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
SortDesc: true,
Direction: "up",
Direction: "prev",
PageSize: 50,
FromColumnValue: users[10].Username,
FromId: users[10].Id,
@@ -6541,4 +6541,26 @@ func testGetUserReport(t *testing.T, rctx request.CTX, ss store.Store) {
require.NoError(t, err)
require.Len(t, userReport, 15)
})
t.Run("should filter on search term", func(t *testing.T) {
userReport, err := ss.User().GetUserReport(&model.UserReportOptions{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
PageSize: 50,
},
SearchTerm: "username_1",
})
require.NoError(t, err)
require.Len(t, userReport, 26)
userReport, err = ss.User().GetUserReport(&model.UserReportOptions{
ReportingBaseOptions: model.ReportingBaseOptions{
SortColumn: "Username",
PageSize: 50,
},
SearchTerm: "username_2",
})
require.NoError(t, err)
require.Len(t, userReport, 11)
})
}

Просмотреть файл

@@ -10709,6 +10709,22 @@ func (s *TimerLayerUserStore) GetUnreadCountForChannel(userID string, channelID
return result, err
}
func (s *TimerLayerUserStore) GetUserCountForReport(filter *model.UserReportOptions) (int64, error) {
start := time.Now()
result, err := s.UserStore.GetUserCountForReport(filter)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("UserStore.GetUserCountForReport", success, elapsed)
}
return result, err
}
func (s *TimerLayerUserStore) GetUserReport(filter *model.UserReportOptions) ([]*model.UserReportQuery, error) {
start := time.Now()