Added post limit warning (#26793)

* Renamed user limit API to app limit API

* Added post warning limit

* Added tests

* Fixed types

* Renamed AppLimits to ServerLimits

* Fixed tests and review fixes

* Updated generated code

* Updated server i18n

* Fixed TestCreateUserOrGuest test

* Exclude deleted posts from post count for liims

* Reduced limits for ease of testing

* Restored original limts
This commit is contained in:
Harshil Sharma
2024-04-18 11:50:30 +05:30
committed by GitHub
parent 4571c6e3a3
commit b4a1b33d39
31 changed files with 448 additions and 153 deletions

View File

@@ -3637,7 +3637,7 @@ components:
state: state:
description: The current state of the installation description: The current state of the installation
type: string type: string
UserLimits: ServerLimits:
type: object type: object
properties: properties:
maxUsersLimit: maxUsersLimit:

View File

@@ -1,25 +1,25 @@
/api/v4/limits/users: /api/v4/limits/server:
get: get:
tags: tags:
- users - users
summary: Gets the user limits for the server summary: Gets the server limits for the server
description: > description: >
Gets the user limits for the server Gets the server limits for the server
##### Permissions ##### Permissions
Requires `sysconsole_read_user_management_users`. Requires `sysconsole_read_user_management_users`.
operationId: getUserLimits operationId: GetServerLimits
responses: responses:
"200": "200":
description: User limits for server description: App limits for server
content: content:
application/json: application/json:
schema: schema:
type: array type: array
items: items:
$ref: "#/components/schemas/UserLimits" $ref: "#/components/schemas/ServerLimits"
"400": "400":
$ref: "#/components/responses/BadRequest" $ref: "#/components/responses/BadRequest"
"401": "401":

View File

@@ -13,22 +13,22 @@ import (
) )
func (api *API) InitLimits() { func (api *API) InitLimits() {
api.BaseRoutes.Limits.Handle("/users", api.APISessionRequired(getUserLimits)).Methods("GET") api.BaseRoutes.Limits.Handle("/server", api.APISessionRequired(getServerLimits)).Methods("GET")
} }
func getUserLimits(c *Context, w http.ResponseWriter, r *http.Request) { func getServerLimits(c *Context, w http.ResponseWriter, r *http.Request) {
if !(c.IsSystemAdmin() && c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementUsers)) { if !(c.IsSystemAdmin() && c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadUserManagementUsers)) {
c.SetPermissionError(model.PermissionSysconsoleReadUserManagementUsers) c.SetPermissionError(model.PermissionSysconsoleReadUserManagementUsers)
return return
} }
userLimits, err := c.App.GetUserLimits() serverLimits, err := c.App.GetServerLimits()
if err != nil { if err != nil {
c.Err = err c.Err = err
return return
} }
if err := json.NewEncoder(w).Encode(userLimits); err != nil { if err := json.NewEncoder(w).Encode(serverLimits); err != nil {
c.Logger.Error("Error writing user limits response", mlog.Err(err)) c.Logger.Error("Error writing server limits response", mlog.Err(err))
} }
} }

View File

@@ -806,6 +806,7 @@ type AppIface interface {
GetSchemeRolesForTeam(teamID string) (string, string, string, *model.AppError) GetSchemeRolesForTeam(teamID string) (string, string, string, *model.AppError)
GetSchemes(scope string, offset int, limit int) ([]*model.Scheme, *model.AppError) GetSchemes(scope string, offset int, limit int) ([]*model.Scheme, *model.AppError)
GetSchemesPage(scope string, page int, perPage int) ([]*model.Scheme, *model.AppError) GetSchemesPage(scope string, page int, perPage int) ([]*model.Scheme, *model.AppError)
GetServerLimits() (*model.ServerLimits, *model.AppError)
GetSession(token string) (*model.Session, *model.AppError) GetSession(token string) (*model.Session, *model.AppError)
GetSessionById(c request.CTX, sessionID string) (*model.Session, *model.AppError) GetSessionById(c request.CTX, sessionID string) (*model.Session, *model.AppError)
GetSessions(c request.CTX, userID string) ([]*model.Session, *model.AppError) GetSessions(c request.CTX, userID string) ([]*model.Session, *model.AppError)
@@ -864,7 +865,6 @@ type AppIface interface {
GetUserByUsername(username string) (*model.User, *model.AppError) GetUserByUsername(username string) (*model.User, *model.AppError)
GetUserCountForReport(filter *model.UserReportOptions) (*int64, *model.AppError) GetUserCountForReport(filter *model.UserReportOptions) (*int64, *model.AppError)
GetUserForLogin(c request.CTX, id, loginId string) (*model.User, *model.AppError) GetUserForLogin(c request.CTX, id, loginId string) (*model.User, *model.AppError)
GetUserLimits() (*model.UserLimits, *model.AppError)
GetUserTermsOfService(userID string) (*model.UserTermsOfService, *model.AppError) GetUserTermsOfService(userID string) (*model.UserTermsOfService, *model.AppError)
GetUsers(userIDs []string) ([]*model.User, *model.AppError) GetUsers(userIDs []string) ([]*model.User, *model.AppError)
GetUsersByGroupChannelIds(c request.CTX, channelIDs []string, asAdmin bool) (map[string][]*model.User, *model.AppError) GetUsersByGroupChannelIds(c request.CTX, channelIDs []string, asAdmin bool) (map[string][]*model.User, *model.AppError)

View File

@@ -12,26 +12,39 @@ import (
) )
const ( const (
maxUsersLimit = 10000 maxUsersLimit = 10_000
maxUsersHardLimit = 11000 maxUsersHardLimit = 11_000
maxPostLimit = 5_000_000
) )
func (a *App) GetUserLimits() (*model.UserLimits, *model.AppError) { func (a *App) GetServerLimits() (*model.ServerLimits, *model.AppError) {
if !a.shouldShowUserLimits() { var limits = &model.ServerLimits{}
return &model.UserLimits{}, nil
if a.shouldShowUserLimits() {
activeUserCount, appErr := a.Srv().Store().User().Count(model.UserCountOptions{})
if appErr != nil {
mlog.Error("Failed to get active user count from database", mlog.String("error", appErr.Error()))
return nil, model.NewAppError("GetServerLimits", "app.limits.get_app_limits.user_count.store_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
}
limits.ActiveUserCount = activeUserCount
limits.MaxUsersLimit = maxUsersLimit
limits.MaxUsersHardLimit = maxUsersHardLimit
} }
activeUserCount, appErr := a.Srv().Store().User().Count(model.UserCountOptions{}) if a.shouldShowPostLimits() {
if appErr != nil { postCount, appErr := a.Srv().Store().Post().AnalyticsPostCount(&model.PostCountOptions{ExcludeDeleted: true})
mlog.Error("Failed to get active user count from database", mlog.String("error", appErr.Error())) if appErr != nil {
return nil, model.NewAppError("GetUsersLimits", "app.limits.get_user_limits.user_count.store_error", nil, "", http.StatusInternalServerError).Wrap(appErr) mlog.Error("Failed to get post count from database", mlog.String("error", appErr.Error()))
return nil, model.NewAppError("GetServerLimits", "app.limits.get_server_limits.post_count.store_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
}
limits.MaxPostLimit = maxPostLimit
limits.PostCount = postCount
} }
return &model.UserLimits{ return limits, nil
ActiveUserCount: activeUserCount,
MaxUsersLimit: maxUsersLimit,
MaxUsersHardLimit: maxUsersHardLimit,
}, nil
} }
func (a *App) shouldShowUserLimits() bool { func (a *App) shouldShowUserLimits() bool {
@@ -42,8 +55,16 @@ func (a *App) shouldShowUserLimits() bool {
return a.License() == nil return a.License() == nil
} }
func (a *App) shouldShowPostLimits() bool {
if maxPostLimit == 0 {
return false
}
return a.License() == nil
}
func (a *App) isHardUserLimitExceeded() (bool, *model.AppError) { func (a *App) isHardUserLimitExceeded() (bool, *model.AppError) {
userLimits, appErr := a.GetUserLimits() userLimits, appErr := a.GetServerLimits()
if appErr != nil { if appErr != nil {
return false, appErr return false, appErr
} }

View File

@@ -6,127 +6,177 @@ package app
import ( import (
"testing" "testing"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/model"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestGetUserLimits(t *testing.T) { func TestGetServerLimits(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
t.Run("base case", func(t *testing.T) { t.Run("base case", func(t *testing.T) {
userLimits, appErr := th.App.GetUserLimits() th := Setup(t).InitBasic()
defer th.TearDown()
serverLimits, appErr := th.App.GetServerLimits()
require.Nil(t, appErr) require.Nil(t, appErr)
// InitBasic creates 3 users by default // InitBasic creates 3 users by default
require.Equal(t, int64(3), userLimits.ActiveUserCount) require.Equal(t, int64(3), serverLimits.ActiveUserCount)
require.Equal(t, int64(10000), userLimits.MaxUsersLimit) require.Equal(t, int64(10000), serverLimits.MaxUsersLimit)
// 5 posts are created by default
require.Equal(t, int64(5), serverLimits.PostCount)
require.Equal(t, int64(5_000_000), serverLimits.MaxPostLimit)
}) })
t.Run("user count should increase on creating new user and decrease on permanently deleting", func(t *testing.T) { t.Run("user count should increase on creating new user and decrease on permanently deleting", func(t *testing.T) {
userLimits, appErr := th.App.GetUserLimits() th := Setup(t).InitBasic()
defer th.TearDown()
serverLimits, appErr := th.App.GetServerLimits()
require.Nil(t, appErr) require.Nil(t, appErr)
require.Equal(t, int64(3), userLimits.ActiveUserCount) require.Equal(t, int64(3), serverLimits.ActiveUserCount)
// now we create a new user // now we create a new user
newUser := th.CreateUser() newUser := th.CreateUser()
userLimits, appErr = th.App.GetUserLimits() serverLimits, appErr = th.App.GetServerLimits()
require.Nil(t, appErr) require.Nil(t, appErr)
require.Equal(t, int64(4), userLimits.ActiveUserCount) require.Equal(t, int64(4), serverLimits.ActiveUserCount)
// now we'll delete the user // now we'll delete the user
_ = th.App.PermanentDeleteUser(th.Context, newUser) _ = th.App.PermanentDeleteUser(th.Context, newUser)
userLimits, appErr = th.App.GetUserLimits() serverLimits, appErr = th.App.GetServerLimits()
require.Nil(t, appErr) require.Nil(t, appErr)
require.Equal(t, int64(3), userLimits.ActiveUserCount) require.Equal(t, int64(3), serverLimits.ActiveUserCount)
}) })
t.Run("user count should increase on creating new guest user and decrease on permanently deleting", func(t *testing.T) { t.Run("user count should increase on creating new guest user and decrease on permanently deleting", func(t *testing.T) {
userLimits, appErr := th.App.GetUserLimits() th := Setup(t).InitBasic()
defer th.TearDown()
serverLimits, appErr := th.App.GetServerLimits()
require.Nil(t, appErr) require.Nil(t, appErr)
require.Equal(t, int64(3), userLimits.ActiveUserCount) require.Equal(t, int64(3), serverLimits.ActiveUserCount)
// now we create a new user // now we create a new user
newGuestUser := th.CreateGuest() newGuestUser := th.CreateGuest()
userLimits, appErr = th.App.GetUserLimits() serverLimits, appErr = th.App.GetServerLimits()
require.Nil(t, appErr) require.Nil(t, appErr)
require.Equal(t, int64(4), userLimits.ActiveUserCount) require.Equal(t, int64(4), serverLimits.ActiveUserCount)
// now we'll delete the user // now we'll delete the user
_ = th.App.PermanentDeleteUser(th.Context, newGuestUser) _ = th.App.PermanentDeleteUser(th.Context, newGuestUser)
userLimits, appErr = th.App.GetUserLimits() serverLimits, appErr = th.App.GetServerLimits()
require.Nil(t, appErr) require.Nil(t, appErr)
require.Equal(t, int64(3), userLimits.ActiveUserCount) require.Equal(t, int64(3), serverLimits.ActiveUserCount)
}) })
t.Run("user count should increase on creating new user and decrease on soft deleting", func(t *testing.T) { t.Run("user count should increase on creating new user and decrease on soft deleting", func(t *testing.T) {
userLimits, appErr := th.App.GetUserLimits() th := Setup(t).InitBasic()
defer th.TearDown()
serverLimits, appErr := th.App.GetServerLimits()
require.Nil(t, appErr) require.Nil(t, appErr)
require.Equal(t, int64(3), userLimits.ActiveUserCount) require.Equal(t, int64(3), serverLimits.ActiveUserCount)
// now we create a new user // now we create a new user
newUser := th.CreateUser() newUser := th.CreateUser()
userLimits, appErr = th.App.GetUserLimits() serverLimits, appErr = th.App.GetServerLimits()
require.Nil(t, appErr) require.Nil(t, appErr)
require.Equal(t, int64(4), userLimits.ActiveUserCount) require.Equal(t, int64(4), serverLimits.ActiveUserCount)
// now we'll delete the user // now we'll delete the user
_, appErr = th.App.UpdateActive(th.Context, newUser, false) _, appErr = th.App.UpdateActive(th.Context, newUser, false)
require.Nil(t, appErr) require.Nil(t, appErr)
userLimits, appErr = th.App.GetUserLimits() serverLimits, appErr = th.App.GetServerLimits()
require.Nil(t, appErr) require.Nil(t, appErr)
require.Equal(t, int64(3), userLimits.ActiveUserCount) require.Equal(t, int64(3), serverLimits.ActiveUserCount)
}) })
t.Run("user count should increase on creating new guest user and decrease on soft deleting", func(t *testing.T) { t.Run("user count should increase on creating new guest user and decrease on soft deleting", func(t *testing.T) {
userLimits, appErr := th.App.GetUserLimits() th := Setup(t).InitBasic()
defer th.TearDown()
serverLimits, appErr := th.App.GetServerLimits()
require.Nil(t, appErr) require.Nil(t, appErr)
require.Equal(t, int64(3), userLimits.ActiveUserCount) require.Equal(t, int64(3), serverLimits.ActiveUserCount)
// now we create a new user // now we create a new user
newGuestUser := th.CreateGuest() newGuestUser := th.CreateGuest()
userLimits, appErr = th.App.GetUserLimits() serverLimits, appErr = th.App.GetServerLimits()
require.Nil(t, appErr) require.Nil(t, appErr)
require.Equal(t, int64(4), userLimits.ActiveUserCount) require.Equal(t, int64(4), serverLimits.ActiveUserCount)
// now we'll delete the user // now we'll delete the user
_, appErr = th.App.UpdateActive(th.Context, newGuestUser, false) _, appErr = th.App.UpdateActive(th.Context, newGuestUser, false)
require.Nil(t, appErr) require.Nil(t, appErr)
userLimits, appErr = th.App.GetUserLimits() serverLimits, appErr = th.App.GetServerLimits()
require.Nil(t, appErr) require.Nil(t, appErr)
require.Equal(t, int64(3), userLimits.ActiveUserCount) require.Equal(t, int64(3), serverLimits.ActiveUserCount)
}) })
t.Run("user count should not change on creating or deleting bots", func(t *testing.T) { t.Run("user count should not change on creating or deleting bots", func(t *testing.T) {
userLimits, appErr := th.App.GetUserLimits() th := Setup(t).InitBasic()
defer th.TearDown()
serverLimits, appErr := th.App.GetServerLimits()
require.Nil(t, appErr) require.Nil(t, appErr)
require.Equal(t, int64(3), userLimits.ActiveUserCount) require.Equal(t, int64(3), serverLimits.ActiveUserCount)
// now we create a new bot // now we create a new bot
newBot := th.CreateBot() newBot := th.CreateBot()
userLimits, appErr = th.App.GetUserLimits() serverLimits, appErr = th.App.GetServerLimits()
require.Nil(t, appErr) require.Nil(t, appErr)
require.Equal(t, int64(3), userLimits.ActiveUserCount) require.Equal(t, int64(3), serverLimits.ActiveUserCount)
// now we'll delete the bot // now we'll delete the bot
_ = th.App.PermanentDeleteBot(th.Context, newBot.UserId) _ = th.App.PermanentDeleteBot(th.Context, newBot.UserId)
userLimits, appErr = th.App.GetUserLimits() serverLimits, appErr = th.App.GetServerLimits()
require.Nil(t, appErr) require.Nil(t, appErr)
require.Equal(t, int64(3), userLimits.ActiveUserCount) require.Equal(t, int64(3), serverLimits.ActiveUserCount)
}) })
t.Run("limits should be empty when there is a license", func(t *testing.T) { t.Run("limits should be empty when there is a license", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicense()) th.App.Srv().SetLicense(model.NewTestLicense())
userLimits, appErr := th.App.GetUserLimits() serverLimits, appErr := th.App.GetServerLimits()
require.Nil(t, appErr) require.Nil(t, appErr)
require.Equal(t, int64(0), userLimits.ActiveUserCount) require.Equal(t, int64(0), serverLimits.ActiveUserCount)
require.Equal(t, int64(0), userLimits.MaxUsersLimit) require.Equal(t, int64(0), serverLimits.MaxUsersLimit)
})
t.Run("post count should increase on creating new post and should decrease on deleting post", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
serverLimits, appErr := th.App.GetServerLimits()
require.Nil(t, appErr)
require.Equal(t, int64(5), serverLimits.PostCount)
// now we create a new post
team := th.CreateTeam()
channel := th.CreateChannel(request.TestContext(t), team)
post := th.CreatePost(channel)
serverLimits, appErr = th.App.GetServerLimits()
require.Nil(t, appErr)
require.Equal(t, int64(6), serverLimits.PostCount)
// now we'll delete the post
_, appErr = th.App.DeletePost(request.TestContext(t), post.Id, "")
require.Nil(t, appErr)
serverLimits, appErr = th.App.GetServerLimits()
require.Nil(t, appErr)
require.Equal(t, int64(5), serverLimits.PostCount)
}) })
} }

View File

@@ -9463,6 +9463,28 @@ func (a *OpenTracingAppLayer) GetSchemesPage(scope string, page int, perPage int
return resultVar0, resultVar1 return resultVar0, resultVar1
} }
func (a *OpenTracingAppLayer) GetServerLimits() (*model.ServerLimits, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetServerLimits")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetServerLimits()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetSession(token string) (*model.Session, *model.AppError) { func (a *OpenTracingAppLayer) GetSession(token string) (*model.Session, *model.AppError) {
origCtx := a.ctx origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSession") span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSession")
@@ -10851,28 +10873,6 @@ func (a *OpenTracingAppLayer) GetUserForLogin(c request.CTX, id string, loginId
return resultVar0, resultVar1 return resultVar0, resultVar1
} }
func (a *OpenTracingAppLayer) GetUserLimits() (*model.UserLimits, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserLimits")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetUserLimits()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetUserStatusesByIds(userIDs []string) ([]*model.Status, *model.AppError) { func (a *OpenTracingAppLayer) GetUserStatusesByIds(userIDs []string) ([]*model.Status, *model.AppError) {
origCtx := a.ctx origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserStatusesByIds") span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserStatusesByIds")

View File

@@ -333,7 +333,7 @@ func (a *App) createUserOrGuest(c request.CTX, user *model.User, guest bool) (*m
}(ruser.Id) }(ruser.Id)
} }
userLimits, limitErr := a.GetUserLimits() userLimits, limitErr := a.GetServerLimits()
if limitErr != nil { if limitErr != nil {
// we don't want to break the create user flow just because of this. // we don't want to break the create user flow just because of this.
// So, we log the error, not return // So, we log the error, not return
@@ -1070,7 +1070,7 @@ func (a *App) UpdateActive(c request.CTX, user *model.User, active bool) (*model
} }
if active { if active {
userLimits, appErr := a.GetUserLimits() userLimits, appErr := a.GetServerLimits()
if appErr != nil { if appErr != nil {
mlog.Error("Error fetching user limits in UpdateActive", mlog.Err(appErr)) mlog.Error("Error fetching user limits in UpdateActive", mlog.Err(appErr))
} else { } else {

View File

@@ -2033,8 +2033,12 @@ func TestCreateUserOrGuest(t *testing.T) {
mockUserStore := storemocks.UserStore{} mockUserStore := storemocks.UserStore{}
mockUserStore.On("Count", mock.Anything).Return(int64(12000), nil) mockUserStore.On("Count", mock.Anything).Return(int64(12000), nil)
mockPostStore := storemocks.PostStore{}
mockPostStore.On("AnalyticsPostCount", mock.Anything).Return(int64(1000), nil)
mockStore := th.App.Srv().Store().(*storemocks.Store) mockStore := th.App.Srv().Store().(*storemocks.Store)
mockStore.On("User").Return(&mockUserStore) mockStore.On("User").Return(&mockUserStore)
mockStore.On("Post").Return(&mockPostStore)
user := &model.User{ user := &model.User{
Email: "TestCreateUserOrGuest@example.com", Email: "TestCreateUserOrGuest@example.com",
@@ -2147,8 +2151,12 @@ func userCreationMocks(t *testing.T, th *TestHelper, userID string, activeUserCo
mockProductNoticeStore := storemocks.ProductNoticesStore{} mockProductNoticeStore := storemocks.ProductNoticesStore{}
mockProductNoticeStore.On("View", userID, mock.Anything).Return(nil) mockProductNoticeStore.On("View", userID, mock.Anything).Return(nil)
mockPostStore := storemocks.PostStore{}
mockPostStore.On("AnalyticsPostCount", mock.Anything).Return(int64(1000), nil)
mockStore := th.App.Srv().Store().(*storemocks.Store) mockStore := th.App.Srv().Store().(*storemocks.Store)
mockStore.On("User").Return(&mockUserStore) mockStore.On("User").Return(&mockUserStore)
mockStore.On("Post").Return(&mockPostStore)
mockStore.On("Group").Return(&mockGroupStore) mockStore.On("Group").Return(&mockGroupStore)
mockStore.On("Channel").Return(&mockChannelStore) mockStore.On("Channel").Return(&mockChannelStore)
mockStore.On("Preference").Return(&mockPreferencesStore) mockStore.On("Preference").Return(&mockPreferencesStore)

View File

@@ -6039,9 +6039,13 @@
"translation": "No license present" "translation": "No license present"
}, },
{ {
"id": "app.limits.get_user_limits.user_count.store_error", "id": "app.limits.get_app_limits.user_count.store_error",
"translation": "Failed to get user count" "translation": "Failed to get user count"
}, },
{
"id": "app.limits.get_server_limits.post_count.store_error",
"translation": "Failed to get post count"
},
{ {
"id": "app.login.doLogin.updateLastLogin.error", "id": "app.login.doLogin.updateLastLogin.error",
"translation": "Could not update last login timestamp" "translation": "Could not update last login timestamp"

View File

@@ -8953,20 +8953,20 @@ func (c *Client4) SubmitTrueUpReview(ctx context.Context, req map[string]any) (*
return BuildResponse(r), nil return BuildResponse(r), nil
} }
func (c *Client4) GetUserLimits(ctx context.Context) (*UserLimits, *Response, error) { func (c *Client4) GetServerLimits(ctx context.Context) (*ServerLimits, *Response, error) {
r, err := c.DoAPIGet(ctx, c.limitsRoute()+"/users", "") r, err := c.DoAPIGet(ctx, c.limitsRoute()+"/users", "")
if err != nil { if err != nil {
return nil, BuildResponse(r), err return nil, BuildResponse(r), err
} }
defer closeBody(r) defer closeBody(r)
var userLimits UserLimits var serverLimits ServerLimits
if r.StatusCode == http.StatusNotModified { if r.StatusCode == http.StatusNotModified {
return &userLimits, BuildResponse(r), nil return &serverLimits, BuildResponse(r), nil
} }
if err := json.NewDecoder(r.Body).Decode(&userLimits); err != nil { if err := json.NewDecoder(r.Body).Decode(&serverLimits); err != nil {
return nil, nil, NewAppError("GetUserLimits", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err) return nil, nil, NewAppError("GetServerLimits", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
} }
return &userLimits, BuildResponse(r), nil return &serverLimits, BuildResponse(r), nil
} }
// CreateChannelBookmark creates a channel bookmark based on the provided struct. // CreateChannelBookmark creates a channel bookmark based on the provided struct.

View File

@@ -3,8 +3,11 @@
package model package model
type UserLimits struct { type ServerLimits struct {
MaxUsersLimit int64 `json:"maxUsersLimit"` // soft limit for max number of users. MaxUsersLimit int64 `json:"maxUsersLimit"` // soft limit for max number of users.
MaxUsersHardLimit int64 `json:"maxUsersHardLimit"` // hard limit for max number of active users. MaxUsersHardLimit int64 `json:"maxUsersHardLimit"` // hard limit for max number of active users.
ActiveUserCount int64 `json:"activeUserCount"` // actual number of active users on server. Active = non deleted ActiveUserCount int64 `json:"activeUserCount"` // actual number of active users on server. Active = non deleted
MaxPostLimit int64 `json:"maxPostLimit"` // soft limit for max number of posts
PostCount int64 `json:"postCount"` // actual number of posts in system.
} }

View File

@@ -7,7 +7,7 @@ import type {Dispatch} from 'redux';
import {uploadLicense, removeLicense, getPrevTrialLicense} from 'mattermost-redux/actions/admin'; import {uploadLicense, removeLicense, getPrevTrialLicense} from 'mattermost-redux/actions/admin';
import {getLicenseConfig} from 'mattermost-redux/actions/general'; import {getLicenseConfig} from 'mattermost-redux/actions/general';
import {getUsersLimits} from 'mattermost-redux/actions/limits'; import {getServerLimits} from 'mattermost-redux/actions/limits';
import {getFilteredUsersStats} from 'mattermost-redux/actions/users'; import {getFilteredUsersStats} from 'mattermost-redux/actions/users';
import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getFilteredUsersStats as selectFilteredUserStats} from 'mattermost-redux/selectors/entities/users'; import {getFilteredUsersStats as selectFilteredUserStats} from 'mattermost-redux/selectors/entities/users';
@@ -43,7 +43,7 @@ function mapDispatchToProps(dispatch: Dispatch) {
requestTrialLicense, requestTrialLicense,
openModal, openModal,
getFilteredUsersStats, getFilteredUsersStats,
getUsersLimits, getServerLimits,
}, dispatch), }, dispatch),
}; };
} }

View File

@@ -54,7 +54,7 @@ describe('components/admin_console/license_settings/LicenseSettings', () => {
upgradeToE0Status: jest.fn().mockImplementation(() => Promise.resolve({percentage: 0, error: null})), upgradeToE0Status: jest.fn().mockImplementation(() => Promise.resolve({percentage: 0, error: null})),
openModal: jest.fn(), openModal: jest.fn(),
getFilteredUsersStats: jest.fn(), getFilteredUsersStats: jest.fn(),
getUsersLimits: jest.fn(), getServerLimits: jest.fn(),
}, },
}; };

View File

@@ -7,7 +7,7 @@ import {FormattedMessage, defineMessages} from 'react-intl';
import type {StatusOK} from '@mattermost/types/client4'; import type {StatusOK} from '@mattermost/types/client4';
import type {ClientLicense} from '@mattermost/types/config'; import type {ClientLicense} from '@mattermost/types/config';
import type {ServerError} from '@mattermost/types/errors'; import type {ServerError} from '@mattermost/types/errors';
import type {UsersLimits} from '@mattermost/types/limits'; import type {ServerLimits} from '@mattermost/types/limits';
import type {GetFilteredUsersStatsOpts, UsersStats} from '@mattermost/types/users'; import type {GetFilteredUsersStatsOpts, UsersStats} from '@mattermost/types/users';
import type {ActionResult} from 'mattermost-redux/types/actions'; import type {ActionResult} from 'mattermost-redux/types/actions';
@@ -55,7 +55,7 @@ type Props = {
ping: () => Promise<{status: string}>; ping: () => Promise<{status: string}>;
requestTrialLicense: (users: number, termsAccepted: boolean, receiveEmailsAccepted: boolean, featureName: string) => Promise<ActionResult>; requestTrialLicense: (users: number, termsAccepted: boolean, receiveEmailsAccepted: boolean, featureName: string) => Promise<ActionResult>;
openModal: <P>(modalData: ModalData<P>) => void; openModal: <P>(modalData: ModalData<P>) => void;
getUsersLimits: () => Promise<ActionResult<UsersLimits, ServerError>>; getServerLimits: () => Promise<ActionResult<ServerLimits, ServerError>>;
getFilteredUsersStats: (filters: GetFilteredUsersStatsOpts) => Promise<{ getFilteredUsersStats: (filters: GetFilteredUsersStatsOpts) => Promise<{
data?: UsersStats; data?: UsersStats;
error?: ServerError; error?: ServerError;
@@ -196,7 +196,7 @@ export default class LicenseSettings extends React.PureComponent<Props, State> {
this.props.actions.getLicenseConfig(), this.props.actions.getLicenseConfig(),
]); ]);
await this.props.actions.getUsersLimits(); await this.props.actions.getServerLimits();
this.setState({serverError: null, removing: false}); this.setState({serverError: null, removing: false});
}; };

View File

@@ -6,6 +6,7 @@ import React from 'react';
import type {ClientLicense, ClientConfig, WarnMetricStatus} from '@mattermost/types/config'; import type {ClientLicense, ClientConfig, WarnMetricStatus} from '@mattermost/types/config';
import {ToPaidPlanBannerDismissable} from 'components/admin_console/billing/billing_subscriptions/to_paid_plan_nudge_banner'; import {ToPaidPlanBannerDismissable} from 'components/admin_console/billing/billing_subscriptions/to_paid_plan_nudge_banner';
import PostLimitsAnnouncementBar from 'components/announcement_bar/post_limits_announcement_bar';
import withGetCloudSubscription from 'components/common/hocs/cloud/with_get_cloud_subscription'; import withGetCloudSubscription from 'components/common/hocs/cloud/with_get_cloud_subscription';
import CloudAnnualRenewalAnnouncementBar from './cloud_annual_renewal'; import CloudAnnualRenewalAnnouncementBar from './cloud_annual_renewal';
@@ -98,10 +99,22 @@ class AnnouncementBarController extends React.PureComponent<Props> {
); );
} }
// The component specified further down takes priority over the component above it.
// For example, consider this-
// {
// Foo
// Bar
// Baz
// }
// Even if all Foo, Bar and Baz render, only Baz is visible as it's further down.
return ( return (
<> <>
{adminConfiguredAnnouncementBar} {adminConfiguredAnnouncementBar}
{errorBar} {errorBar}
<PostLimitsAnnouncementBar
license={this.props.license}
userIsAdmin={this.props.userIsAdmin}
/>
<UsersLimitsAnnouncementBar <UsersLimitsAnnouncementBar
license={this.props.license} license={this.props.license}
userIsAdmin={this.props.userIsAdmin} userIsAdmin={this.props.userIsAdmin}

View File

@@ -0,0 +1,90 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {
ShouldShowingPostLimitsAnnouncementBarProps} from 'components/announcement_bar/post_limits_announcement_bar/index';
import {shouldShowPostLimitsAnnouncementBar,
} from 'components/announcement_bar/post_limits_announcement_bar/index';
describe('shouldShowPostLimitsAnnouncementBar', () => {
const defaultProps: ShouldShowingPostLimitsAnnouncementBarProps = {
userIsAdmin: true,
isLicensed: false,
maxPostLimit: 10,
postCount: 5,
};
test('should not show when user is not admin', () => {
const props: ShouldShowingPostLimitsAnnouncementBarProps = {
...defaultProps,
userIsAdmin: false,
};
expect(shouldShowPostLimitsAnnouncementBar(props)).toBe(false);
});
test('should not show when post count is 0', () => {
const props: ShouldShowingPostLimitsAnnouncementBarProps = {
...defaultProps,
postCount: 0,
};
expect(shouldShowPostLimitsAnnouncementBar(props)).toBe(false);
});
test('should not show when max post limit is 0', () => {
const props: ShouldShowingPostLimitsAnnouncementBarProps = {
...defaultProps,
maxPostLimit: 0,
};
expect(shouldShowPostLimitsAnnouncementBar(props)).toBe(false);
});
test('should not show when post count is less than max users limit', () => {
const props: ShouldShowingPostLimitsAnnouncementBarProps = {
...defaultProps,
maxPostLimit: 10,
postCount: 5,
};
expect(shouldShowPostLimitsAnnouncementBar(props)).toBe(false);
});
test('should show when post count is equal to max post limit', () => {
const props: ShouldShowingPostLimitsAnnouncementBarProps = {
...defaultProps,
maxPostLimit: 10,
postCount: 10,
};
expect(shouldShowPostLimitsAnnouncementBar(props)).toBe(true);
});
test('should show for non licensed servers with post count is greater than max post limit', () => {
const props: ShouldShowingPostLimitsAnnouncementBarProps = {
...defaultProps,
isLicensed: false,
maxPostLimit: 5,
postCount: 10,
};
expect(shouldShowPostLimitsAnnouncementBar(props)).toBe(true);
});
test('should not show for licensed server', () => {
const props: ShouldShowingPostLimitsAnnouncementBarProps = {
...defaultProps,
isLicensed: true,
maxPostLimit: 0,
postCount: 0,
};
expect(shouldShowPostLimitsAnnouncementBar(props)).toBe(false);
});
test('should not show for licensed server even if post count is greater than max post limit', () => {
const props: ShouldShowingPostLimitsAnnouncementBarProps = {
...defaultProps,
isLicensed: true,
maxPostLimit: 10,
postCount: 11,
};
expect(shouldShowPostLimitsAnnouncementBar(props)).toBe(false);
});
});

View File

@@ -0,0 +1,86 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {FormattedMessage} from 'react-intl';
import {useSelector} from 'react-redux';
import {AlertOutlineIcon} from '@mattermost/compass-icons/components';
import type {ClientLicense} from '@mattermost/types/config';
import {getServerLimits} from 'mattermost-redux/selectors/entities/limits';
import AnnouncementBar from 'components/announcement_bar/default_announcement_bar';
import {AnnouncementBarTypes} from 'utils/constants';
type Props = {
license?: ClientLicense;
userIsAdmin: boolean;
};
const learnMoreExternalLink = 'https://mattermost.com/pl/error-code-error-safety-limits-exceeded';
function PostLimitsAnnouncementBar(props: Props) {
const serverLimits = useSelector(getServerLimits);
const handleCTAClick = useCallback(() => {
window.open(learnMoreExternalLink, '_blank');
}, []);
const isLicensed = props?.license?.IsLicensed === 'true';
const maxPostLimit = serverLimits?.maxPostLimit ?? 0;
const postCount = serverLimits?.postCount ?? 0;
if (!shouldShowPostLimitsAnnouncementBar({userIsAdmin: props.userIsAdmin, isLicensed, maxPostLimit, postCount})) {
return null;
}
return (
<AnnouncementBar
id='post_limits_announcement_bar'
showCloseButton={false}
message={
<FormattedMessage
id='post_limits_announcement_bar.copyText'
defaultMessage='Message limits exceeded. Contact administrator with: {ErrorCode}'
values={{
ErrorCode: 'ERROR_SAFETY_LIMITS_EXCEEDED',
}}
/>
}
type={AnnouncementBarTypes.CRITICAL}
icon={<AlertOutlineIcon size={16}/>}
showCTA={true}
showLinkAsButton={true}
ctaText={
<FormattedMessage
id='users_limits_announcement_bar.ctaText'
defaultMessage='Learn More'
/>
}
onButtonClick={handleCTAClick}
/>
);
}
export type ShouldShowingPostLimitsAnnouncementBarProps = {
userIsAdmin: boolean;
isLicensed: boolean;
maxPostLimit: number;
postCount: number;
};
export function shouldShowPostLimitsAnnouncementBar({userIsAdmin, isLicensed, maxPostLimit, postCount}: ShouldShowingPostLimitsAnnouncementBarProps) {
if (!userIsAdmin) {
return false;
}
if (maxPostLimit === 0 || postCount === 0) {
return false;
}
return !isLicensed && postCount >= maxPostLimit;
}
export default PostLimitsAnnouncementBar;

View File

@@ -8,7 +8,7 @@ import {useSelector} from 'react-redux';
import {AlertOutlineIcon} from '@mattermost/compass-icons/components'; import {AlertOutlineIcon} from '@mattermost/compass-icons/components';
import type {ClientLicense} from '@mattermost/types/config'; import type {ClientLicense} from '@mattermost/types/config';
import {getUsersLimits} from 'mattermost-redux/selectors/entities/limits'; import {getServerLimits} from 'mattermost-redux/selectors/entities/limits';
import AnnouncementBar from 'components/announcement_bar/default_announcement_bar'; import AnnouncementBar from 'components/announcement_bar/default_announcement_bar';
@@ -22,15 +22,15 @@ type Props = {
const learnMoreExternalLink = 'https://mattermost.com/pl/error-code-error-safety-limits-exceeded'; const learnMoreExternalLink = 'https://mattermost.com/pl/error-code-error-safety-limits-exceeded';
function UsersLimitsAnnouncementBar(props: Props) { function UsersLimitsAnnouncementBar(props: Props) {
const usersLimits = useSelector(getUsersLimits); const serverLimits = useSelector(getServerLimits);
const handleCTAClick = useCallback(() => { const handleCTAClick = useCallback(() => {
window.open(learnMoreExternalLink, '_blank'); window.open(learnMoreExternalLink, '_blank');
}, []); }, []);
const isLicensed = props?.license?.IsLicensed === 'true'; const isLicensed = props?.license?.IsLicensed === 'true';
const maxUsersLimit = usersLimits?.maxUsersLimit ?? 0; const maxUsersLimit = serverLimits?.maxUsersLimit ?? 0;
const activeUserCount = usersLimits?.activeUserCount ?? 0; const activeUserCount = serverLimits?.activeUserCount ?? 0;
if (!shouldShowUserLimitsAnnouncementBar({userIsAdmin: props.userIsAdmin, isLicensed, maxUsersLimit, activeUserCount})) { if (!shouldShowUserLimitsAnnouncementBar({userIsAdmin: props.userIsAdmin, isLicensed, maxUsersLimit, activeUserCount})) {
return null; return null;
@@ -43,7 +43,10 @@ function UsersLimitsAnnouncementBar(props: Props) {
message={ message={
<FormattedMessage <FormattedMessage
id='users_limits_announcement_bar.copyText' id='users_limits_announcement_bar.copyText'
defaultMessage='User limits exceeded. Contact administrator with: ERROR_SAFETY_LIMITS_EXCEEDED' defaultMessage='User limits exceeded. Contact administrator with: {ErrorCode}'
values={{
ErrorCode: 'ERROR_SAFETY_LIMITS_EXCEEDED',
}}
/> />
} }
type={AnnouncementBarTypes.CRITICAL} type={AnnouncementBarTypes.CRITICAL}

View File

@@ -4584,6 +4584,7 @@
"post_info.tooltip.add_reactions": "Add Reaction", "post_info.tooltip.add_reactions": "Add Reaction",
"post_info.unpin": "Unpin from Channel", "post_info.unpin": "Unpin from Channel",
"post_info.unread": "Mark as Unread", "post_info.unread": "Mark as Unread",
"post_limits_announcement_bar.copyText": "Message limits exceeded. Contact administrator with: {ErrorCode}",
"post_message_preview.channel": "Only visible to users in ~{channel}", "post_message_preview.channel": "Only visible to users in ~{channel}",
"post_message_view.edited": "Edited", "post_message_view.edited": "Edited",
"post_message_view.view_post_edit_history": "Click to view history", "post_message_view.view_post_edit_history": "Click to view history",
@@ -5850,7 +5851,7 @@
"userGuideHelp.mattermostUserGuide": "Mattermost user guide", "userGuideHelp.mattermostUserGuide": "Mattermost user guide",
"userGuideHelp.reportAProblem": "Report a problem", "userGuideHelp.reportAProblem": "Report a problem",
"userGuideHelp.trainingResources": "Training resources", "userGuideHelp.trainingResources": "Training resources",
"users_limits_announcement_bar.copyText": "User limits exceeded. Contact administrator with: ERROR_SAFETY_LIMITS_EXCEEDED", "users_limits_announcement_bar.copyText": "User limits exceeded. Contact administrator with: {ErrorCode}",
"users_limits_announcement_bar.ctaText": "Learn More", "users_limits_announcement_bar.ctaText": "Learn More",
"userSettingsModal.pluginPreferences.header": "PLUGIN PREFERENCES", "userSettingsModal.pluginPreferences.header": "PLUGIN PREFERENCES",
"version_bar.new": "A new version of Mattermost is available.", "version_bar.new": "A new version of Mattermost is available.",

View File

@@ -4,5 +4,5 @@
import keyMirror from 'mattermost-redux/utils/key_mirror'; import keyMirror from 'mattermost-redux/utils/key_mirror';
export default keyMirror({ export default keyMirror({
RECIEVED_USERS_LIMITS: null, RECIEVED_APP_LIMITS: null,
}); });

View File

@@ -65,7 +65,7 @@ export default keyMirror({
DISABLED_USER_ACCESS_TOKEN: null, DISABLED_USER_ACCESS_TOKEN: null,
ENABLED_USER_ACCESS_TOKEN: null, ENABLED_USER_ACCESS_TOKEN: null,
RECEIVED_USER_STATS: null, RECEIVED_USER_STATS: null,
RECIEVED_USERS_LIMITS: null, RECIEVED_APP_LIMITS: null,
RECEIVED_FILTERED_USER_STATS: null, RECEIVED_FILTERED_USER_STATS: null,
PROFILE_NO_LONGER_VISIBLE: null, PROFILE_NO_LONGER_VISIBLE: null,
LOGIN: null, LOGIN: null,

View File

@@ -26,7 +26,7 @@ import type {
import type {DeepPartial} from '@mattermost/types/utilities'; import type {DeepPartial} from '@mattermost/types/utilities';
import {AdminTypes} from 'mattermost-redux/action_types'; import {AdminTypes} from 'mattermost-redux/action_types';
import {getUsersLimits} from 'mattermost-redux/actions/limits'; import {getServerLimits} from 'mattermost-redux/actions/limits';
import {Client4} from 'mattermost-redux/client'; import {Client4} from 'mattermost-redux/client';
import type {ActionFuncAsync} from 'mattermost-redux/types/actions'; import type {ActionFuncAsync} from 'mattermost-redux/types/actions';
@@ -386,7 +386,7 @@ export function removeLicense(): ActionFuncAsync<boolean> {
return {error: error as ServerError}; return {error: error as ServerError};
} }
await dispatch(getUsersLimits()); await dispatch(getServerLimits());
return {data: true}; return {data: true};
}; };

View File

@@ -3,18 +3,22 @@
import nock from 'nock'; import nock from 'nock';
import type {ServerLimits} from '@mattermost/types/limits';
import * as Actions from 'mattermost-redux/actions/limits'; import * as Actions from 'mattermost-redux/actions/limits';
import {Client4} from 'mattermost-redux/client'; import {Client4} from 'mattermost-redux/client';
import TestHelper from '../../test/test_helper'; import TestHelper from '../../test/test_helper';
import configureStore from '../../test/test_store'; import configureStore from '../../test/test_store';
describe('getUsersLimits', () => { describe('getServerLimits', () => {
const URL_USERS_LIMITS = '/limits/users'; const URL_USERS_LIMITS = '/limits/server';
const defaultUserLimitsState = { const defaultServerLimitsState: ServerLimits = {
activeUserCount: 0, activeUserCount: 0,
maxUsersLimit: 0, maxUsersLimit: 0,
maxPostLimit: 0,
postCount: 0,
}; };
let store = configureStore(); let store = configureStore();
@@ -62,26 +66,28 @@ describe('getUsersLimits', () => {
}, },
}); });
const {data} = await store.dispatch(Actions.getUsersLimits()); const {data} = await store.dispatch(Actions.getServerLimits());
expect(data).toEqual(defaultUserLimitsState); expect(data).toEqual(defaultServerLimitsState);
}); });
test('should not return default state for non admin users', async () => { test('should not return default state for non admin users', async () => {
const {data} = await store.dispatch(Actions.getUsersLimits()); const {data} = await store.dispatch(Actions.getServerLimits());
expect(data).not.toEqual(defaultUserLimitsState); expect(data).not.toEqual(defaultServerLimitsState);
}); });
test('should return data if user is admin', async () => { test('should return data if user is admin', async () => {
const userLimits = { const userLimits: ServerLimits = {
activeUserCount: 600, activeUserCount: 600,
maxUsersLimit: 10000, maxUsersLimit: 10_000,
maxPostLimit: 5_000_000,
postCount: 10_000,
}; };
nock(Client4.getBaseRoute()). nock(Client4.getBaseRoute()).
get(URL_USERS_LIMITS). get(URL_USERS_LIMITS).
reply(200, userLimits); reply(200, userLimits);
const {data} = await store.dispatch(Actions.getUsersLimits()); const {data} = await store.dispatch(Actions.getServerLimits());
expect(data).toEqual(userLimits); expect(data).toEqual(userLimits);
}); });
@@ -91,7 +97,7 @@ describe('getUsersLimits', () => {
get(URL_USERS_LIMITS). get(URL_USERS_LIMITS).
reply(400, {message: errorMessage}); reply(400, {message: errorMessage});
const {error} = await store.dispatch(Actions.getUsersLimits()); const {error} = await store.dispatch(Actions.getServerLimits());
console.log(error); console.log(error);
expect(error.message).toEqual(errorMessage); expect(error.message).toEqual(errorMessage);
}); });

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import type {ServerError} from '@mattermost/types/errors'; import type {ServerError} from '@mattermost/types/errors';
import type {UsersLimits} from '@mattermost/types/limits'; import type {ServerLimits} from '@mattermost/types/limits';
import {LimitsTypes} from 'mattermost-redux/action_types'; import {LimitsTypes} from 'mattermost-redux/action_types';
import {logError} from 'mattermost-redux/actions/errors'; import {logError} from 'mattermost-redux/actions/errors';
@@ -12,7 +12,7 @@ import {getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
import type {ActionFuncAsync} from 'mattermost-redux/types/actions'; import type {ActionFuncAsync} from 'mattermost-redux/types/actions';
import {isAdmin} from 'mattermost-redux/utils/user_utils'; import {isAdmin} from 'mattermost-redux/utils/user_utils';
export function getUsersLimits(): ActionFuncAsync<UsersLimits> { export function getServerLimits(): ActionFuncAsync<ServerLimits> {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const roles = getCurrentUserRoles(getState()); const roles = getCurrentUserRoles(getState());
const amIAdmin = isAdmin(roles); const amIAdmin = isAdmin(roles);
@@ -21,26 +21,31 @@ export function getUsersLimits(): ActionFuncAsync<UsersLimits> {
data: { data: {
activeUserCount: 0, activeUserCount: 0,
maxUsersLimit: 0, maxUsersLimit: 0,
postCount: 0,
maxPostLimit: 0,
}, },
}; };
} }
let response; let response;
try { try {
response = await Client4.getUsersLimits(); response = await Client4.getServerLimits();
} catch (err) { } catch (err) {
forceLogoutIfNecessary(err, dispatch, getState); forceLogoutIfNecessary(err, dispatch, getState);
dispatch(logError(err)); dispatch(logError(err));
return {error: err as ServerError}; return {error: err as ServerError};
} }
const data: UsersLimits = { const data: ServerLimits = {
activeUserCount: response?.data?.activeUserCount ?? 0, activeUserCount: response?.data?.activeUserCount ?? 0,
maxUsersLimit: response?.data?.maxUsersLimit ?? 0, maxUsersLimit: response?.data?.maxUsersLimit ?? 0,
postCount: response?.data?.postCount ?? 0,
maxPostLimit: response?.data?.maxPostLimit ?? 0,
}; };
dispatch({type: LimitsTypes.RECIEVED_USERS_LIMITS, data}); dispatch({type: LimitsTypes.RECIEVED_APP_LIMITS, data});
return {data}; return {data};
}; };
} }

View File

@@ -12,7 +12,7 @@ import {UserTypes, AdminTypes} from 'mattermost-redux/action_types';
import {logError} from 'mattermost-redux/actions/errors'; import {logError} from 'mattermost-redux/actions/errors';
import {setServerVersion, getClientConfig, getLicenseConfig} from 'mattermost-redux/actions/general'; import {setServerVersion, getClientConfig, getLicenseConfig} from 'mattermost-redux/actions/general';
import {bindClientFunc, forceLogoutIfNecessary, debounce} from 'mattermost-redux/actions/helpers'; import {bindClientFunc, forceLogoutIfNecessary, debounce} from 'mattermost-redux/actions/helpers';
import {getUsersLimits} from 'mattermost-redux/actions/limits'; import {getServerLimits} from 'mattermost-redux/actions/limits';
import {getMyPreferences} from 'mattermost-redux/actions/preferences'; import {getMyPreferences} from 'mattermost-redux/actions/preferences';
import {loadRolesIfNeeded} from 'mattermost-redux/actions/roles'; import {loadRolesIfNeeded} from 'mattermost-redux/actions/roles';
import {getMyTeams, getMyTeamMembers, getMyTeamUnreads} from 'mattermost-redux/actions/teams'; import {getMyTeams, getMyTeamMembers, getMyTeamUnreads} from 'mattermost-redux/actions/teams';
@@ -76,7 +76,7 @@ export function loadMe(): ActionFuncAsync<boolean> {
const isCollapsedThreads = isCollapsedThreadsEnabled(getState()); const isCollapsedThreads = isCollapsedThreadsEnabled(getState());
await dispatch(getMyTeamUnreads(isCollapsedThreads)); await dispatch(getMyTeamUnreads(isCollapsedThreads));
await dispatch(getUsersLimits()); await dispatch(getServerLimits());
} catch (error) { } catch (error) {
dispatch(logError(error as ServerError)); dispatch(logError(error as ServerError));
return {error: error as ServerError}; return {error: error as ServerError};

View File

@@ -6,13 +6,13 @@ import {combineReducers} from 'redux';
import {LimitsTypes} from 'mattermost-redux/action_types'; import {LimitsTypes} from 'mattermost-redux/action_types';
function usersLimits(state = {}, action: AnyAction) { function serverLimits(state = {}, action: AnyAction) {
switch (action.type) { switch (action.type) {
case LimitsTypes.RECIEVED_USERS_LIMITS: { case LimitsTypes.RECIEVED_APP_LIMITS: {
const usersLimits = action.data; const serverLimits = action.data;
return { return {
...state, ...state,
...usersLimits, ...serverLimits,
}; };
} }
default: default:
@@ -21,5 +21,5 @@ function usersLimits(state = {}, action: AnyAction) {
} }
export default combineReducers({ export default combineReducers({
usersLimits, serverLimits,
}); });

View File

@@ -1,9 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import type {UsersLimits} from '@mattermost/types/limits'; import type {ServerLimits} from '@mattermost/types/limits';
import type {GlobalState} from '@mattermost/types/store'; import type {GlobalState} from '@mattermost/types/store';
export function getUsersLimits(state: GlobalState): UsersLimits { export function getServerLimits(state: GlobalState): ServerLimits {
return state.entities.limits.usersLimits; return state.entities.limits.serverLimits;
} }

View File

@@ -36,9 +36,11 @@ const state: GlobalState = {
dndEndTimes: {}, dndEndTimes: {},
}, },
limits: { limits: {
usersLimits: { serverLimits: {
activeUserCount: 0, activeUserCount: 0,
maxUsersLimit: 0, maxUsersLimit: 0,
postCount: 0,
maxPostLimit: 0,
}, },
}, },
teams: { teams: {

View File

@@ -103,7 +103,7 @@ import type {
SubmitDialogResponse, SubmitDialogResponse,
} from '@mattermost/types/integrations'; } from '@mattermost/types/integrations';
import type {Job, JobTypeBase} from '@mattermost/types/jobs'; import type {Job, JobTypeBase} from '@mattermost/types/jobs';
import type {UsersLimits} from '@mattermost/types/limits'; import type {ServerLimits} from '@mattermost/types/limits';
import type { import type {
MarketplaceApp, MarketplaceApp,
MarketplacePlugin, MarketplacePlugin,
@@ -498,8 +498,8 @@ export default class Client4 {
return `${this.getBaseRoute()}/limits`; return `${this.getBaseRoute()}/limits`;
} }
getUsersLimitsRoute() { getServerLimitsRoute() {
return `${this.getLimitsRoute()}/users`; return `${this.getLimitsRoute()}/server`;
} }
getCSRFFromCookie() { getCSRFFromCookie() {
@@ -1220,9 +1220,9 @@ export default class Client4 {
// Limits Routes // Limits Routes
getUsersLimits = () => { getServerLimits = () => {
return this.doFetchWithResponse<UsersLimits>( return this.doFetchWithResponse<ServerLimits>(
`${this.getUsersLimitsRoute()}`, `${this.getServerLimitsRoute()}`,
{ {
method: 'get', method: 'get',
}, },

View File

@@ -2,10 +2,13 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
export type LimitsState = { export type LimitsState = {
usersLimits: UsersLimits; serverLimits: ServerLimits;
}; };
export type UsersLimits = { export type ServerLimits = {
activeUserCount: number; activeUserCount: number;
maxUsersLimit: number; maxUsersLimit: number;
maxPostLimit: number;
postCount: number;
}; };