diff --git a/api/v4/source/definitions.yaml b/api/v4/source/definitions.yaml index 358cb19528..54b8af9990 100644 --- a/api/v4/source/definitions.yaml +++ b/api/v4/source/definitions.yaml @@ -3637,7 +3637,7 @@ components: state: description: The current state of the installation type: string - UserLimits: + ServerLimits: type: object properties: maxUsersLimit: diff --git a/api/v4/source/limits.yaml b/api/v4/source/limits.yaml index 03d121ef80..f033058dea 100644 --- a/api/v4/source/limits.yaml +++ b/api/v4/source/limits.yaml @@ -1,25 +1,25 @@ - /api/v4/limits/users: + /api/v4/limits/server: get: tags: - users - summary: Gets the user limits for the server + summary: Gets the server limits for the server description: > - Gets the user limits for the server - + Gets the server limits for the server + ##### Permissions - + Requires `sysconsole_read_user_management_users`. - operationId: getUserLimits + operationId: GetServerLimits responses: "200": - description: User limits for server + description: App limits for server content: application/json: schema: type: array items: - $ref: "#/components/schemas/UserLimits" + $ref: "#/components/schemas/ServerLimits" "400": $ref: "#/components/responses/BadRequest" "401": diff --git a/server/channels/api4/limits.go b/server/channels/api4/limits.go index 54663a43c0..38ec13b6f4 100644 --- a/server/channels/api4/limits.go +++ b/server/channels/api4/limits.go @@ -13,22 +13,22 @@ import ( ) 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)) { c.SetPermissionError(model.PermissionSysconsoleReadUserManagementUsers) return } - userLimits, err := c.App.GetUserLimits() + serverLimits, err := c.App.GetServerLimits() if err != nil { c.Err = err return } - if err := json.NewEncoder(w).Encode(userLimits); err != nil { - c.Logger.Error("Error writing user limits response", mlog.Err(err)) + if err := json.NewEncoder(w).Encode(serverLimits); err != nil { + c.Logger.Error("Error writing server limits response", mlog.Err(err)) } } diff --git a/server/channels/app/app_iface.go b/server/channels/app/app_iface.go index 8443a67554..4e3c5df79c 100644 --- a/server/channels/app/app_iface.go +++ b/server/channels/app/app_iface.go @@ -806,6 +806,7 @@ type AppIface interface { GetSchemeRolesForTeam(teamID string) (string, string, string, *model.AppError) GetSchemes(scope string, offset int, limit 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) GetSessionById(c request.CTX, sessionID 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) GetUserCountForReport(filter *model.UserReportOptions) (*int64, *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) GetUsers(userIDs []string) ([]*model.User, *model.AppError) GetUsersByGroupChannelIds(c request.CTX, channelIDs []string, asAdmin bool) (map[string][]*model.User, *model.AppError) diff --git a/server/channels/app/limits.go b/server/channels/app/limits.go index 42e26d7190..5697cd224a 100644 --- a/server/channels/app/limits.go +++ b/server/channels/app/limits.go @@ -12,26 +12,39 @@ import ( ) const ( - maxUsersLimit = 10000 - maxUsersHardLimit = 11000 + maxUsersLimit = 10_000 + maxUsersHardLimit = 11_000 + + maxPostLimit = 5_000_000 ) -func (a *App) GetUserLimits() (*model.UserLimits, *model.AppError) { - if !a.shouldShowUserLimits() { - return &model.UserLimits{}, nil +func (a *App) GetServerLimits() (*model.ServerLimits, *model.AppError) { + var limits = &model.ServerLimits{} + + 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 appErr != nil { - mlog.Error("Failed to get active user count from database", mlog.String("error", appErr.Error())) - return nil, model.NewAppError("GetUsersLimits", "app.limits.get_user_limits.user_count.store_error", nil, "", http.StatusInternalServerError).Wrap(appErr) + if a.shouldShowPostLimits() { + postCount, appErr := a.Srv().Store().Post().AnalyticsPostCount(&model.PostCountOptions{ExcludeDeleted: true}) + if appErr != nil { + 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{ - ActiveUserCount: activeUserCount, - MaxUsersLimit: maxUsersLimit, - MaxUsersHardLimit: maxUsersHardLimit, - }, nil + return limits, nil } func (a *App) shouldShowUserLimits() bool { @@ -42,8 +55,16 @@ func (a *App) shouldShowUserLimits() bool { 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) { - userLimits, appErr := a.GetUserLimits() + userLimits, appErr := a.GetServerLimits() if appErr != nil { return false, appErr } diff --git a/server/channels/app/limits_test.go b/server/channels/app/limits_test.go index 72d36bc39a..bf1d40ff44 100644 --- a/server/channels/app/limits_test.go +++ b/server/channels/app/limits_test.go @@ -6,127 +6,177 @@ package app import ( "testing" + "github.com/mattermost/mattermost/server/public/shared/request" + "github.com/mattermost/mattermost/server/public/model" "github.com/stretchr/testify/require" ) -func TestGetUserLimits(t *testing.T) { - th := Setup(t).InitBasic() - defer th.TearDown() - +func TestGetServerLimits(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) // InitBasic creates 3 users by default - require.Equal(t, int64(3), userLimits.ActiveUserCount) - require.Equal(t, int64(10000), userLimits.MaxUsersLimit) + require.Equal(t, int64(3), serverLimits.ActiveUserCount) + 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) { - userLimits, appErr := th.App.GetUserLimits() + th := Setup(t).InitBasic() + defer th.TearDown() + + serverLimits, appErr := th.App.GetServerLimits() require.Nil(t, appErr) - require.Equal(t, int64(3), userLimits.ActiveUserCount) + require.Equal(t, int64(3), serverLimits.ActiveUserCount) // now we create a new user newUser := th.CreateUser() - userLimits, appErr = th.App.GetUserLimits() + serverLimits, appErr = th.App.GetServerLimits() require.Nil(t, appErr) - require.Equal(t, int64(4), userLimits.ActiveUserCount) + require.Equal(t, int64(4), serverLimits.ActiveUserCount) // now we'll delete the user _ = th.App.PermanentDeleteUser(th.Context, newUser) - userLimits, appErr = th.App.GetUserLimits() + serverLimits, appErr = th.App.GetServerLimits() 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) { - userLimits, appErr := th.App.GetUserLimits() + th := Setup(t).InitBasic() + defer th.TearDown() + + serverLimits, appErr := th.App.GetServerLimits() require.Nil(t, appErr) - require.Equal(t, int64(3), userLimits.ActiveUserCount) + require.Equal(t, int64(3), serverLimits.ActiveUserCount) // now we create a new user newGuestUser := th.CreateGuest() - userLimits, appErr = th.App.GetUserLimits() + serverLimits, appErr = th.App.GetServerLimits() require.Nil(t, appErr) - require.Equal(t, int64(4), userLimits.ActiveUserCount) + require.Equal(t, int64(4), serverLimits.ActiveUserCount) // now we'll delete the user _ = th.App.PermanentDeleteUser(th.Context, newGuestUser) - userLimits, appErr = th.App.GetUserLimits() + serverLimits, appErr = th.App.GetServerLimits() 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) { - userLimits, appErr := th.App.GetUserLimits() + th := Setup(t).InitBasic() + defer th.TearDown() + + serverLimits, appErr := th.App.GetServerLimits() require.Nil(t, appErr) - require.Equal(t, int64(3), userLimits.ActiveUserCount) + require.Equal(t, int64(3), serverLimits.ActiveUserCount) // now we create a new user newUser := th.CreateUser() - userLimits, appErr = th.App.GetUserLimits() + serverLimits, appErr = th.App.GetServerLimits() require.Nil(t, appErr) - require.Equal(t, int64(4), userLimits.ActiveUserCount) + require.Equal(t, int64(4), serverLimits.ActiveUserCount) // now we'll delete the user _, appErr = th.App.UpdateActive(th.Context, newUser, false) require.Nil(t, appErr) - userLimits, appErr = th.App.GetUserLimits() + serverLimits, appErr = th.App.GetServerLimits() 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) { - userLimits, appErr := th.App.GetUserLimits() + th := Setup(t).InitBasic() + defer th.TearDown() + + serverLimits, appErr := th.App.GetServerLimits() require.Nil(t, appErr) - require.Equal(t, int64(3), userLimits.ActiveUserCount) + require.Equal(t, int64(3), serverLimits.ActiveUserCount) // now we create a new user newGuestUser := th.CreateGuest() - userLimits, appErr = th.App.GetUserLimits() + serverLimits, appErr = th.App.GetServerLimits() require.Nil(t, appErr) - require.Equal(t, int64(4), userLimits.ActiveUserCount) + require.Equal(t, int64(4), serverLimits.ActiveUserCount) // now we'll delete the user _, appErr = th.App.UpdateActive(th.Context, newGuestUser, false) require.Nil(t, appErr) - userLimits, appErr = th.App.GetUserLimits() + serverLimits, appErr = th.App.GetServerLimits() 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) { - userLimits, appErr := th.App.GetUserLimits() + th := Setup(t).InitBasic() + defer th.TearDown() + + serverLimits, appErr := th.App.GetServerLimits() require.Nil(t, appErr) - require.Equal(t, int64(3), userLimits.ActiveUserCount) + require.Equal(t, int64(3), serverLimits.ActiveUserCount) // now we create a new bot newBot := th.CreateBot() - userLimits, appErr = th.App.GetUserLimits() + serverLimits, appErr = th.App.GetServerLimits() require.Nil(t, appErr) - require.Equal(t, int64(3), userLimits.ActiveUserCount) + require.Equal(t, int64(3), serverLimits.ActiveUserCount) // now we'll delete the bot _ = th.App.PermanentDeleteBot(th.Context, newBot.UserId) - userLimits, appErr = th.App.GetUserLimits() + serverLimits, appErr = th.App.GetServerLimits() 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) { + th := Setup(t).InitBasic() + defer th.TearDown() + th.App.Srv().SetLicense(model.NewTestLicense()) - userLimits, appErr := th.App.GetUserLimits() + serverLimits, appErr := th.App.GetServerLimits() require.Nil(t, appErr) - require.Equal(t, int64(0), userLimits.ActiveUserCount) - require.Equal(t, int64(0), userLimits.MaxUsersLimit) + require.Equal(t, int64(0), serverLimits.ActiveUserCount) + 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) }) } diff --git a/server/channels/app/opentracing/opentracing_layer.go b/server/channels/app/opentracing/opentracing_layer.go index d6c788c88e..bd46c5e179 100644 --- a/server/channels/app/opentracing/opentracing_layer.go +++ b/server/channels/app/opentracing/opentracing_layer.go @@ -9463,6 +9463,28 @@ func (a *OpenTracingAppLayer) GetSchemesPage(scope string, page int, perPage int 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) { origCtx := a.ctx 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 } -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) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetUserStatusesByIds") diff --git a/server/channels/app/user.go b/server/channels/app/user.go index f20780ddd7..b3e82223c3 100644 --- a/server/channels/app/user.go +++ b/server/channels/app/user.go @@ -333,7 +333,7 @@ func (a *App) createUserOrGuest(c request.CTX, user *model.User, guest bool) (*m }(ruser.Id) } - userLimits, limitErr := a.GetUserLimits() + userLimits, limitErr := a.GetServerLimits() if limitErr != nil { // we don't want to break the create user flow just because of this. // 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 { - userLimits, appErr := a.GetUserLimits() + userLimits, appErr := a.GetServerLimits() if appErr != nil { mlog.Error("Error fetching user limits in UpdateActive", mlog.Err(appErr)) } else { diff --git a/server/channels/app/user_test.go b/server/channels/app/user_test.go index b3906b7568..c8972b6dcc 100644 --- a/server/channels/app/user_test.go +++ b/server/channels/app/user_test.go @@ -2033,8 +2033,12 @@ func TestCreateUserOrGuest(t *testing.T) { mockUserStore := storemocks.UserStore{} 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.On("User").Return(&mockUserStore) + mockStore.On("Post").Return(&mockPostStore) user := &model.User{ Email: "TestCreateUserOrGuest@example.com", @@ -2147,8 +2151,12 @@ func userCreationMocks(t *testing.T, th *TestHelper, userID string, activeUserCo mockProductNoticeStore := storemocks.ProductNoticesStore{} 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.On("User").Return(&mockUserStore) + mockStore.On("Post").Return(&mockPostStore) mockStore.On("Group").Return(&mockGroupStore) mockStore.On("Channel").Return(&mockChannelStore) mockStore.On("Preference").Return(&mockPreferencesStore) diff --git a/server/i18n/en.json b/server/i18n/en.json index 06446acd1c..14c370b192 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -6039,9 +6039,13 @@ "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" }, + { + "id": "app.limits.get_server_limits.post_count.store_error", + "translation": "Failed to get post count" + }, { "id": "app.login.doLogin.updateLastLogin.error", "translation": "Could not update last login timestamp" diff --git a/server/public/model/client4.go b/server/public/model/client4.go index c0605cf4f6..74345ddb66 100644 --- a/server/public/model/client4.go +++ b/server/public/model/client4.go @@ -8953,20 +8953,20 @@ func (c *Client4) SubmitTrueUpReview(ctx context.Context, req map[string]any) (* 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", "") if err != nil { return nil, BuildResponse(r), err } defer closeBody(r) - var userLimits UserLimits + var serverLimits ServerLimits 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 { - return nil, nil, NewAppError("GetUserLimits", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err) + if err := json.NewDecoder(r.Body).Decode(&serverLimits); err != nil { + 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. diff --git a/server/public/model/limits.go b/server/public/model/limits.go index 97ba516142..89a9243af5 100644 --- a/server/public/model/limits.go +++ b/server/public/model/limits.go @@ -3,8 +3,11 @@ package model -type UserLimits struct { +type ServerLimits struct { MaxUsersLimit int64 `json:"maxUsersLimit"` // soft limit for max number of 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 + + MaxPostLimit int64 `json:"maxPostLimit"` // soft limit for max number of posts + PostCount int64 `json:"postCount"` // actual number of posts in system. } diff --git a/webapp/channels/src/components/admin_console/license_settings/index.ts b/webapp/channels/src/components/admin_console/license_settings/index.ts index e47e8dfa02..8a080fde60 100644 --- a/webapp/channels/src/components/admin_console/license_settings/index.ts +++ b/webapp/channels/src/components/admin_console/license_settings/index.ts @@ -7,7 +7,7 @@ import type {Dispatch} from 'redux'; import {uploadLicense, removeLicense, getPrevTrialLicense} from 'mattermost-redux/actions/admin'; 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 {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getFilteredUsersStats as selectFilteredUserStats} from 'mattermost-redux/selectors/entities/users'; @@ -43,7 +43,7 @@ function mapDispatchToProps(dispatch: Dispatch) { requestTrialLicense, openModal, getFilteredUsersStats, - getUsersLimits, + getServerLimits, }, dispatch), }; } diff --git a/webapp/channels/src/components/admin_console/license_settings/license_settings.test.tsx b/webapp/channels/src/components/admin_console/license_settings/license_settings.test.tsx index 3345102ea3..20e3dc4574 100644 --- a/webapp/channels/src/components/admin_console/license_settings/license_settings.test.tsx +++ b/webapp/channels/src/components/admin_console/license_settings/license_settings.test.tsx @@ -54,7 +54,7 @@ describe('components/admin_console/license_settings/LicenseSettings', () => { upgradeToE0Status: jest.fn().mockImplementation(() => Promise.resolve({percentage: 0, error: null})), openModal: jest.fn(), getFilteredUsersStats: jest.fn(), - getUsersLimits: jest.fn(), + getServerLimits: jest.fn(), }, }; diff --git a/webapp/channels/src/components/admin_console/license_settings/license_settings.tsx b/webapp/channels/src/components/admin_console/license_settings/license_settings.tsx index 4aab5adb9d..68d7419a09 100644 --- a/webapp/channels/src/components/admin_console/license_settings/license_settings.tsx +++ b/webapp/channels/src/components/admin_console/license_settings/license_settings.tsx @@ -7,7 +7,7 @@ import {FormattedMessage, defineMessages} from 'react-intl'; import type {StatusOK} from '@mattermost/types/client4'; import type {ClientLicense} from '@mattermost/types/config'; 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 {ActionResult} from 'mattermost-redux/types/actions'; @@ -55,7 +55,7 @@ type Props = { ping: () => Promise<{status: string}>; requestTrialLicense: (users: number, termsAccepted: boolean, receiveEmailsAccepted: boolean, featureName: string) => Promise; openModal:

(modalData: ModalData

) => void; - getUsersLimits: () => Promise>; + getServerLimits: () => Promise>; getFilteredUsersStats: (filters: GetFilteredUsersStatsOpts) => Promise<{ data?: UsersStats; error?: ServerError; @@ -196,7 +196,7 @@ export default class LicenseSettings extends React.PureComponent { this.props.actions.getLicenseConfig(), ]); - await this.props.actions.getUsersLimits(); + await this.props.actions.getServerLimits(); this.setState({serverError: null, removing: false}); }; diff --git a/webapp/channels/src/components/announcement_bar/announcement_bar_controller.tsx b/webapp/channels/src/components/announcement_bar/announcement_bar_controller.tsx index 2df53ce9fd..a6363edf7b 100644 --- a/webapp/channels/src/components/announcement_bar/announcement_bar_controller.tsx +++ b/webapp/channels/src/components/announcement_bar/announcement_bar_controller.tsx @@ -6,6 +6,7 @@ import React from 'react'; import type {ClientLicense, ClientConfig, WarnMetricStatus} from '@mattermost/types/config'; 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 CloudAnnualRenewalAnnouncementBar from './cloud_annual_renewal'; @@ -98,10 +99,22 @@ class AnnouncementBarController extends React.PureComponent { ); } + // 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 ( <> {adminConfiguredAnnouncementBar} {errorBar} + { + 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); + }); +}); diff --git a/webapp/channels/src/components/announcement_bar/post_limits_announcement_bar/index.tsx b/webapp/channels/src/components/announcement_bar/post_limits_announcement_bar/index.tsx new file mode 100644 index 0000000000..a66400a6df --- /dev/null +++ b/webapp/channels/src/components/announcement_bar/post_limits_announcement_bar/index.tsx @@ -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 ( + + } + type={AnnouncementBarTypes.CRITICAL} + icon={} + showCTA={true} + showLinkAsButton={true} + ctaText={ + + } + 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; diff --git a/webapp/channels/src/components/announcement_bar/users_limits_announcement_bar/index.tsx b/webapp/channels/src/components/announcement_bar/users_limits_announcement_bar/index.tsx index 8f3a4bb09d..bd5fdcd28b 100644 --- a/webapp/channels/src/components/announcement_bar/users_limits_announcement_bar/index.tsx +++ b/webapp/channels/src/components/announcement_bar/users_limits_announcement_bar/index.tsx @@ -8,7 +8,7 @@ import {useSelector} from 'react-redux'; import {AlertOutlineIcon} from '@mattermost/compass-icons/components'; 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'; @@ -22,15 +22,15 @@ type Props = { const learnMoreExternalLink = 'https://mattermost.com/pl/error-code-error-safety-limits-exceeded'; function UsersLimitsAnnouncementBar(props: Props) { - const usersLimits = useSelector(getUsersLimits); + const serverLimits = useSelector(getServerLimits); const handleCTAClick = useCallback(() => { window.open(learnMoreExternalLink, '_blank'); }, []); const isLicensed = props?.license?.IsLicensed === 'true'; - const maxUsersLimit = usersLimits?.maxUsersLimit ?? 0; - const activeUserCount = usersLimits?.activeUserCount ?? 0; + const maxUsersLimit = serverLimits?.maxUsersLimit ?? 0; + const activeUserCount = serverLimits?.activeUserCount ?? 0; if (!shouldShowUserLimitsAnnouncementBar({userIsAdmin: props.userIsAdmin, isLicensed, maxUsersLimit, activeUserCount})) { return null; @@ -43,7 +43,10 @@ function UsersLimitsAnnouncementBar(props: Props) { message={ } type={AnnouncementBarTypes.CRITICAL} diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index b7819cbf85..7940ce8313 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -4584,6 +4584,7 @@ "post_info.tooltip.add_reactions": "Add Reaction", "post_info.unpin": "Unpin from Channel", "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_view.edited": "Edited", "post_message_view.view_post_edit_history": "Click to view history", @@ -5850,7 +5851,7 @@ "userGuideHelp.mattermostUserGuide": "Mattermost user guide", "userGuideHelp.reportAProblem": "Report a problem", "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", "userSettingsModal.pluginPreferences.header": "PLUGIN PREFERENCES", "version_bar.new": "A new version of Mattermost is available.", diff --git a/webapp/channels/src/packages/mattermost-redux/src/action_types/limits.ts b/webapp/channels/src/packages/mattermost-redux/src/action_types/limits.ts index e0dd51ec0a..dae93686f8 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/action_types/limits.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/action_types/limits.ts @@ -4,5 +4,5 @@ import keyMirror from 'mattermost-redux/utils/key_mirror'; export default keyMirror({ - RECIEVED_USERS_LIMITS: null, + RECIEVED_APP_LIMITS: null, }); diff --git a/webapp/channels/src/packages/mattermost-redux/src/action_types/users.ts b/webapp/channels/src/packages/mattermost-redux/src/action_types/users.ts index 7d78dcd7f8..a7eb46d1d9 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/action_types/users.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/action_types/users.ts @@ -65,7 +65,7 @@ export default keyMirror({ DISABLED_USER_ACCESS_TOKEN: null, ENABLED_USER_ACCESS_TOKEN: null, RECEIVED_USER_STATS: null, - RECIEVED_USERS_LIMITS: null, + RECIEVED_APP_LIMITS: null, RECEIVED_FILTERED_USER_STATS: null, PROFILE_NO_LONGER_VISIBLE: null, LOGIN: null, diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/admin.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/admin.ts index 2d1b4dcde0..62568dfe76 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/admin.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/admin.ts @@ -26,7 +26,7 @@ import type { import type {DeepPartial} from '@mattermost/types/utilities'; 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 type {ActionFuncAsync} from 'mattermost-redux/types/actions'; @@ -386,7 +386,7 @@ export function removeLicense(): ActionFuncAsync { return {error: error as ServerError}; } - await dispatch(getUsersLimits()); + await dispatch(getServerLimits()); return {data: true}; }; diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/limits.test.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/limits.test.ts index cc8243e25e..0b684a6c67 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/limits.test.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/limits.test.ts @@ -3,18 +3,22 @@ import nock from 'nock'; +import type {ServerLimits} from '@mattermost/types/limits'; + import * as Actions from 'mattermost-redux/actions/limits'; import {Client4} from 'mattermost-redux/client'; import TestHelper from '../../test/test_helper'; import configureStore from '../../test/test_store'; -describe('getUsersLimits', () => { - const URL_USERS_LIMITS = '/limits/users'; +describe('getServerLimits', () => { + const URL_USERS_LIMITS = '/limits/server'; - const defaultUserLimitsState = { + const defaultServerLimitsState: ServerLimits = { activeUserCount: 0, maxUsersLimit: 0, + maxPostLimit: 0, + postCount: 0, }; let store = configureStore(); @@ -62,26 +66,28 @@ describe('getUsersLimits', () => { }, }); - const {data} = await store.dispatch(Actions.getUsersLimits()); - expect(data).toEqual(defaultUserLimitsState); + const {data} = await store.dispatch(Actions.getServerLimits()); + expect(data).toEqual(defaultServerLimitsState); }); test('should not return default state for non admin users', async () => { - const {data} = await store.dispatch(Actions.getUsersLimits()); - expect(data).not.toEqual(defaultUserLimitsState); + const {data} = await store.dispatch(Actions.getServerLimits()); + expect(data).not.toEqual(defaultServerLimitsState); }); test('should return data if user is admin', async () => { - const userLimits = { + const userLimits: ServerLimits = { activeUserCount: 600, - maxUsersLimit: 10000, + maxUsersLimit: 10_000, + maxPostLimit: 5_000_000, + postCount: 10_000, }; nock(Client4.getBaseRoute()). get(URL_USERS_LIMITS). reply(200, userLimits); - const {data} = await store.dispatch(Actions.getUsersLimits()); + const {data} = await store.dispatch(Actions.getServerLimits()); expect(data).toEqual(userLimits); }); @@ -91,7 +97,7 @@ describe('getUsersLimits', () => { get(URL_USERS_LIMITS). reply(400, {message: errorMessage}); - const {error} = await store.dispatch(Actions.getUsersLimits()); + const {error} = await store.dispatch(Actions.getServerLimits()); console.log(error); expect(error.message).toEqual(errorMessage); }); diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/limits.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/limits.ts index 7e06fff600..2fa7bc9e12 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/limits.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/limits.ts @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. 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 {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 {isAdmin} from 'mattermost-redux/utils/user_utils'; -export function getUsersLimits(): ActionFuncAsync { +export function getServerLimits(): ActionFuncAsync { return async (dispatch, getState) => { const roles = getCurrentUserRoles(getState()); const amIAdmin = isAdmin(roles); @@ -21,26 +21,31 @@ export function getUsersLimits(): ActionFuncAsync { data: { activeUserCount: 0, maxUsersLimit: 0, + postCount: 0, + maxPostLimit: 0, }, }; } let response; try { - response = await Client4.getUsersLimits(); + response = await Client4.getServerLimits(); } catch (err) { forceLogoutIfNecessary(err, dispatch, getState); dispatch(logError(err)); return {error: err as ServerError}; } - const data: UsersLimits = { + const data: ServerLimits = { activeUserCount: response?.data?.activeUserCount ?? 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}; }; } + diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/users.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/users.ts index 2573542f80..1501e9b894 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/users.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/users.ts @@ -12,7 +12,7 @@ import {UserTypes, AdminTypes} from 'mattermost-redux/action_types'; import {logError} from 'mattermost-redux/actions/errors'; import {setServerVersion, getClientConfig, getLicenseConfig} from 'mattermost-redux/actions/general'; 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 {loadRolesIfNeeded} from 'mattermost-redux/actions/roles'; import {getMyTeams, getMyTeamMembers, getMyTeamUnreads} from 'mattermost-redux/actions/teams'; @@ -76,7 +76,7 @@ export function loadMe(): ActionFuncAsync { const isCollapsedThreads = isCollapsedThreadsEnabled(getState()); await dispatch(getMyTeamUnreads(isCollapsedThreads)); - await dispatch(getUsersLimits()); + await dispatch(getServerLimits()); } catch (error) { dispatch(logError(error as ServerError)); return {error: error as ServerError}; diff --git a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/limits.ts b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/limits.ts index 328bc5fd05..83f560dd02 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/limits.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/limits.ts @@ -6,13 +6,13 @@ import {combineReducers} from 'redux'; import {LimitsTypes} from 'mattermost-redux/action_types'; -function usersLimits(state = {}, action: AnyAction) { +function serverLimits(state = {}, action: AnyAction) { switch (action.type) { - case LimitsTypes.RECIEVED_USERS_LIMITS: { - const usersLimits = action.data; + case LimitsTypes.RECIEVED_APP_LIMITS: { + const serverLimits = action.data; return { ...state, - ...usersLimits, + ...serverLimits, }; } default: @@ -21,5 +21,5 @@ function usersLimits(state = {}, action: AnyAction) { } export default combineReducers({ - usersLimits, + serverLimits, }); diff --git a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/limits.ts b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/limits.ts index d8db2cddf3..f5f1d8e1c1 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/limits.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/limits.ts @@ -1,9 +1,9 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // 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'; -export function getUsersLimits(state: GlobalState): UsersLimits { - return state.entities.limits.usersLimits; +export function getServerLimits(state: GlobalState): ServerLimits { + return state.entities.limits.serverLimits; } diff --git a/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts b/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts index 3000cb76c5..94d8e66e20 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts @@ -36,9 +36,11 @@ const state: GlobalState = { dndEndTimes: {}, }, limits: { - usersLimits: { + serverLimits: { activeUserCount: 0, maxUsersLimit: 0, + postCount: 0, + maxPostLimit: 0, }, }, teams: { diff --git a/webapp/platform/client/src/client4.ts b/webapp/platform/client/src/client4.ts index bd3cb053d0..73b5b4eb06 100644 --- a/webapp/platform/client/src/client4.ts +++ b/webapp/platform/client/src/client4.ts @@ -103,7 +103,7 @@ import type { SubmitDialogResponse, } from '@mattermost/types/integrations'; import type {Job, JobTypeBase} from '@mattermost/types/jobs'; -import type {UsersLimits} from '@mattermost/types/limits'; +import type {ServerLimits} from '@mattermost/types/limits'; import type { MarketplaceApp, MarketplacePlugin, @@ -498,8 +498,8 @@ export default class Client4 { return `${this.getBaseRoute()}/limits`; } - getUsersLimitsRoute() { - return `${this.getLimitsRoute()}/users`; + getServerLimitsRoute() { + return `${this.getLimitsRoute()}/server`; } getCSRFFromCookie() { @@ -1220,9 +1220,9 @@ export default class Client4 { // Limits Routes - getUsersLimits = () => { - return this.doFetchWithResponse( - `${this.getUsersLimitsRoute()}`, + getServerLimits = () => { + return this.doFetchWithResponse( + `${this.getServerLimitsRoute()}`, { method: 'get', }, diff --git a/webapp/platform/types/src/limits.ts b/webapp/platform/types/src/limits.ts index 1d8d5b60e3..c6fe1e389d 100644 --- a/webapp/platform/types/src/limits.ts +++ b/webapp/platform/types/src/limits.ts @@ -2,10 +2,13 @@ // See LICENSE.txt for license information. export type LimitsState = { - usersLimits: UsersLimits; + serverLimits: ServerLimits; }; -export type UsersLimits = { +export type ServerLimits = { activeUserCount: number; maxUsersLimit: number; + + maxPostLimit: number; + postCount: number; };