Team Edition User Limit Update (#33888)

* Lower user limit for TE to final limit

* Added warning banner for user soft limit

* Linting

* Fix test

* Fix tests
This commit is contained in:
Maria A Nunez
2025-09-15 11:39:52 -04:00
committed by GitHub
parent 06b1bf3a51
commit 072c402e49
9 changed files with 282 additions and 89 deletions
+2 -2
View File
@@ -29,8 +29,8 @@ func TestGetServerLimits(t *testing.T) {
// Should have full access to all limits data
require.Greater(t, serverLimits.ActiveUserCount, int64(0))
require.Equal(t, int64(2500), serverLimits.MaxUsersLimit)
require.Equal(t, int64(5000), serverLimits.MaxUsersHardLimit)
require.Equal(t, int64(200), serverLimits.MaxUsersLimit)
require.Equal(t, int64(250), serverLimits.MaxUsersHardLimit)
require.Equal(t, int64(0), serverLimits.PostHistoryLimit)
require.Equal(t, int64(0), serverLimits.LastAccessiblePostTime)
})
+2 -2
View File
@@ -10,8 +10,8 @@ import (
)
const (
maxUsersLimit = 2_500
maxUsersHardLimit = 5_000
maxUsersLimit = 200
maxUsersHardLimit = 250
)
func (a *App) GetServerLimits() (*model.ServerLimits, *model.AppError) {
+7 -7
View File
@@ -28,8 +28,8 @@ func TestGetServerLimits(t *testing.T) {
// InitBasic creates 3 users by default
require.Equal(t, int64(3), serverLimits.ActiveUserCount)
require.Equal(t, int64(2500), serverLimits.MaxUsersLimit)
require.Equal(t, int64(5000), serverLimits.MaxUsersHardLimit)
require.Equal(t, int64(200), serverLimits.MaxUsersLimit)
require.Equal(t, int64(250), serverLimits.MaxUsersHardLimit)
})
t.Run("user count should increase on creating new user and decrease on permanently deleting", func(t *testing.T) {
@@ -279,7 +279,7 @@ func TestIsAtUserLimit(t *testing.T) {
th.App.Srv().SetLicense(nil)
mockUserStore := storemocks.UserStore{}
mockUserStore.On("Count", mock.Anything).Return(int64(4000), nil) // Under hard limit of 5000
mockUserStore.On("Count", mock.Anything).Return(int64(200), nil) // Under hard limit of 250
mockStore := th.App.Srv().Store().(*storemocks.Store)
mockStore.On("User").Return(&mockUserStore)
@@ -295,7 +295,7 @@ func TestIsAtUserLimit(t *testing.T) {
th.App.Srv().SetLicense(nil)
mockUserStore := storemocks.UserStore{}
mockUserStore.On("Count", mock.Anything).Return(int64(5000), nil) // At hard limit of 5000
mockUserStore.On("Count", mock.Anything).Return(int64(250), nil) // At hard limit of 250
mockStore := th.App.Srv().Store().(*storemocks.Store)
mockStore.On("User").Return(&mockUserStore)
@@ -311,7 +311,7 @@ func TestIsAtUserLimit(t *testing.T) {
th.App.Srv().SetLicense(nil)
mockUserStore := storemocks.UserStore{}
mockUserStore.On("Count", mock.Anything).Return(int64(6000), nil) // Over hard limit of 5000
mockUserStore.On("Count", mock.Anything).Return(int64(300), nil) // Over hard limit of 250
mockStore := th.App.Srv().Store().(*storemocks.Store)
mockStore.On("User").Return(&mockUserStore)
@@ -547,8 +547,8 @@ func TestExtraUsersBehavior(t *testing.T) {
require.Nil(t, appErr)
// Unlicensed servers use hard-coded limits without extra users
require.Equal(t, int64(2500), serverLimits.MaxUsersLimit)
require.Equal(t, int64(5000), serverLimits.MaxUsersHardLimit)
require.Equal(t, int64(200), serverLimits.MaxUsersLimit)
require.Equal(t, int64(250), serverLimits.MaxUsersHardLimit)
})
}
@@ -149,6 +149,8 @@ export default class AnnouncementBar extends React.PureComponent<Props, State> {
barClass = 'announcement-bar announcement-bar-advisor-ack';
} else if (this.props.type === AnnouncementBarTypes.GENERAL) {
barClass = 'announcement-bar announcement-bar-general';
} else if (this.props.type === AnnouncementBarTypes.WARNING) {
barClass = 'announcement-bar announcement-bar-warning';
}
if (this.props.className) {
@@ -1,88 +1,185 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shouldShowUserLimitsAnnouncementBar} from './index';
import {shouldShowCriticalBanner, shouldShowWarningBanner} from './index';
import type {ShouldShowingUserLimitsAnnouncementBarProps} from './index';
describe('shouldShowUserLimitsAnnouncementBar', () => {
const defaultProps: ShouldShowingUserLimitsAnnouncementBarProps = {
describe('shouldShowCriticalBanner', () => {
const defaultCriticalProps: ShouldShowingUserLimitsAnnouncementBarProps = {
userIsAdmin: true,
isLicensed: false,
maxUsersLimit: 10,
activeUserCount: 5,
maxUsersHardLimit: 20,
activeUserCount: 15,
};
test('should not show when user is not admin', () => {
const props = {
...defaultProps,
...defaultCriticalProps,
userIsAdmin: false,
};
expect(shouldShowUserLimitsAnnouncementBar(props)).toBe(false);
expect(shouldShowCriticalBanner(props)).toBe(false);
});
test('should not show when active users count is 0', () => {
const props = {
...defaultProps,
...defaultCriticalProps,
activeUserCount: 0,
};
expect(shouldShowUserLimitsAnnouncementBar(props)).toBe(false);
expect(shouldShowCriticalBanner(props)).toBe(false);
});
test('should not show when max users limit is 0', () => {
test('should not show when max users hard limit is 0', () => {
const props = {
...defaultProps,
maxUsersLimit: 0,
...defaultCriticalProps,
maxUsersHardLimit: 0,
};
expect(shouldShowUserLimitsAnnouncementBar(props)).toBe(false);
expect(shouldShowCriticalBanner(props)).toBe(false);
});
test('should not show when active users count is less than max users limit', () => {
test('should not show when active users count is less than max users hard limit', () => {
const props = {
...defaultProps,
activeUserCount: 5,
maxUsersLimit: 10,
};
expect(shouldShowUserLimitsAnnouncementBar(props)).toBe(false);
});
test('should show when active users count is equal to max users limit', () => {
const props = {
...defaultProps,
activeUserCount: 10,
maxUsersLimit: 10,
};
expect(shouldShowUserLimitsAnnouncementBar(props)).toBe(true);
});
test('should show for non licensed servers with active users count is greater than max users limit', () => {
const props = {
...defaultProps,
isLicensed: false,
...defaultCriticalProps,
activeUserCount: 15,
maxUsersLimit: 10,
maxUsersHardLimit: 20,
};
expect(shouldShowUserLimitsAnnouncementBar(props)).toBe(true);
expect(shouldShowCriticalBanner(props)).toBe(false);
});
test('should show when active users count is equal to max users hard limit', () => {
const props = {
...defaultCriticalProps,
activeUserCount: 20,
maxUsersHardLimit: 20,
};
expect(shouldShowCriticalBanner(props)).toBe(true);
});
test('should show for non licensed servers with active users count is greater than max users hard limit', () => {
const props = {
...defaultCriticalProps,
isLicensed: false,
activeUserCount: 25,
maxUsersHardLimit: 20,
};
expect(shouldShowCriticalBanner(props)).toBe(true);
});
test('should not show for licensed server', () => {
const props = {
...defaultProps,
...defaultCriticalProps,
isLicensed: true,
activeUserCount: 0,
maxUsersLimit: 0,
maxUsersHardLimit: 0,
};
expect(shouldShowUserLimitsAnnouncementBar(props)).toBe(false);
expect(shouldShowCriticalBanner(props)).toBe(false);
});
test('should not show for licensed server even if user count is greater than max users limit', () => {
test('should not show for licensed server even if user count is greater than max users hard limit', () => {
const props = {
...defaultProps,
...defaultCriticalProps,
isLicensed: true,
activeUserCount: 101,
maxUsersLimit: 100,
activeUserCount: 25,
maxUsersHardLimit: 20,
};
expect(shouldShowUserLimitsAnnouncementBar(props)).toBe(false);
expect(shouldShowCriticalBanner(props)).toBe(false);
});
});
describe('shouldShowWarningBanner', () => {
const defaultWarningProps: ShouldShowingUserLimitsAnnouncementBarProps = {
userIsAdmin: true,
isLicensed: false,
maxUsersLimit: 10,
maxUsersHardLimit: 20,
activeUserCount: 12,
isWarningDismissed: false,
};
test('should not show when user is not admin', () => {
const props = {
...defaultWarningProps,
userIsAdmin: false,
};
expect(shouldShowWarningBanner(props)).toBe(false);
});
test('should not show when active users count is 0', () => {
const props = {
...defaultWarningProps,
activeUserCount: 0,
};
expect(shouldShowWarningBanner(props)).toBe(false);
});
test('should not show when max users limit is 0', () => {
const props = {
...defaultWarningProps,
maxUsersLimit: 0,
};
expect(shouldShowWarningBanner(props)).toBe(false);
});
test('should not show when max users hard limit is 0', () => {
const props = {
...defaultWarningProps,
maxUsersHardLimit: 0,
};
expect(shouldShowWarningBanner(props)).toBe(false);
});
test('should not show when warning is dismissed', () => {
const props = {
...defaultWarningProps,
isWarningDismissed: true,
};
expect(shouldShowWarningBanner(props)).toBe(false);
});
test('should not show when active users count is less than max users limit', () => {
const props = {
...defaultWarningProps,
activeUserCount: 8,
maxUsersLimit: 10,
};
expect(shouldShowWarningBanner(props)).toBe(false);
});
test('should not show when active users count is greater than or equal to max users hard limit', () => {
const props = {
...defaultWarningProps,
activeUserCount: 20,
maxUsersHardLimit: 20,
};
expect(shouldShowWarningBanner(props)).toBe(false);
});
test('should show when active users count is between max users limit and max users hard limit', () => {
const props = {
...defaultWarningProps,
activeUserCount: 15,
maxUsersLimit: 10,
maxUsersHardLimit: 20,
};
expect(shouldShowWarningBanner(props)).toBe(true);
});
test('should show when active users count equals max users limit', () => {
const props = {
...defaultWarningProps,
activeUserCount: 10,
maxUsersLimit: 10,
maxUsersHardLimit: 20,
};
expect(shouldShowWarningBanner(props)).toBe(true);
});
test('should not show for licensed server', () => {
const props = {
...defaultWarningProps,
isLicensed: true,
};
expect(shouldShowWarningBanner(props)).toBe(false);
});
});
@@ -3,16 +3,22 @@
import React, {useCallback} from 'react';
import {FormattedMessage} from 'react-intl';
import {useSelector} from 'react-redux';
import {useDispatch, useSelector} from 'react-redux';
import {AlertOutlineIcon} from '@mattermost/compass-icons/components';
import type {ClientLicense} from '@mattermost/types/config';
import type {PreferenceType} from '@mattermost/types/preferences';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {getServerLimits} from 'mattermost-redux/selectors/entities/limits';
import {get as getPreference} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import AnnouncementBar from 'components/announcement_bar/default_announcement_bar';
import {AnnouncementBarTypes} from 'utils/constants';
import {AnnouncementBarTypes, Preferences} from 'utils/constants';
import type {GlobalState} from 'types/store';
type Props = {
license?: ClientLicense;
@@ -22,7 +28,9 @@ type Props = {
const learnMoreExternalLink = 'https://mattermost.com/pl/error-code-error-safety-limits-exceeded';
function UsersLimitsAnnouncementBar(props: Props) {
const dispatch = useDispatch();
const serverLimits = useSelector(getServerLimits);
const currentUser = useSelector(getCurrentUser);
const handleCTAClick = useCallback(() => {
window.open(learnMoreExternalLink, '_blank');
@@ -30,57 +38,128 @@ function UsersLimitsAnnouncementBar(props: Props) {
const isLicensed = props?.license?.IsLicensed === 'true';
const maxUsersLimit = serverLimits?.maxUsersLimit ?? 0;
const maxUsersHardLimit = serverLimits?.maxUsersHardLimit ?? 0;
const activeUserCount = serverLimits?.activeUserCount ?? 0;
if (!shouldShowUserLimitsAnnouncementBar({userIsAdmin: props.userIsAdmin, isLicensed, maxUsersLimit, activeUserCount})) {
return null;
// Check if warning banner has been dismissed
const warningDismissalKey = 'users_limits_warning';
const isWarningDismissed = useSelector((state: GlobalState) => {
return getPreference(state, Preferences.USERS_LIMITS_BANNER, warningDismissalKey, 'false') === 'true';
});
const handleWarningDismiss = useCallback(() => {
if (currentUser?.id) {
const preference: PreferenceType = {
category: Preferences.USERS_LIMITS_BANNER,
name: warningDismissalKey,
user_id: currentUser.id,
value: 'true',
};
dispatch(savePreferences(currentUser.id, [preference]));
}
}, [currentUser?.id, dispatch, warningDismissalKey]);
// Critical state: activeUserCount >= hardLimit
if (shouldShowCriticalBanner({userIsAdmin: props.userIsAdmin, isLicensed, maxUsersHardLimit, activeUserCount})) {
return (
<AnnouncementBar
id='users_limits_announcement_bar_critical'
showCloseButton={false}
message={
<FormattedMessage
id='users_limits_announcement_bar.copyText'
defaultMessage='User 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}
/>
);
}
return (
<AnnouncementBar
id='users_limits_announcement_bar'
showCloseButton={false}
message={
<FormattedMessage
id='users_limits_announcement_bar.copyText'
defaultMessage='User 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}
/>
);
// Warning state: activeUserCount >= maxLimit && < hardLimit
if (shouldShowWarningBanner({userIsAdmin: props.userIsAdmin, isLicensed, maxUsersLimit, maxUsersHardLimit, activeUserCount, isWarningDismissed})) {
return (
<AnnouncementBar
id='users_limits_announcement_bar_warning'
showCloseButton={true}
handleClose={handleWarningDismiss}
message={
<FormattedMessage
id='users_limits_announcement_bar.warning.copyText'
defaultMessage='This workspace is approaching the user limit ({activeUserCount}/{maxUsersHardLimit} users).'
values={{
activeUserCount,
maxUsersHardLimit,
}}
/>
}
type='warning'
icon={<AlertOutlineIcon size={16}/>}
showCTA={true}
showLinkAsButton={true}
ctaText={
<FormattedMessage
id='users_limits_announcement_bar.ctaText'
defaultMessage='Learn More'
/>
}
onButtonClick={handleCTAClick}
/>
);
}
return null;
}
export type ShouldShowingUserLimitsAnnouncementBarProps = {
userIsAdmin: boolean;
isLicensed: boolean;
maxUsersLimit: number;
maxUsersLimit?: number;
maxUsersHardLimit: number;
activeUserCount: number;
isWarningDismissed?: boolean;
};
export function shouldShowUserLimitsAnnouncementBar({userIsAdmin, isLicensed, maxUsersLimit, activeUserCount}: ShouldShowingUserLimitsAnnouncementBarProps) {
export function shouldShowCriticalBanner({userIsAdmin, isLicensed, maxUsersHardLimit, activeUserCount}: ShouldShowingUserLimitsAnnouncementBarProps) {
if (!userIsAdmin) {
return false;
}
if (maxUsersLimit === 0 || activeUserCount === 0) {
if (maxUsersHardLimit === 0 || activeUserCount === 0) {
return false;
}
return !isLicensed && activeUserCount >= maxUsersLimit;
return !isLicensed && activeUserCount >= maxUsersHardLimit;
}
export function shouldShowWarningBanner({userIsAdmin, isLicensed, maxUsersLimit = 0, maxUsersHardLimit, activeUserCount, isWarningDismissed}: ShouldShowingUserLimitsAnnouncementBarProps) {
if (!userIsAdmin) {
return false;
}
if (maxUsersLimit === 0 || maxUsersHardLimit === 0 || activeUserCount === 0) {
return false;
}
if (isWarningDismissed) {
return false;
}
// Show warning when users >= maxUsersLimit but < maxUsersHardLimit
return !isLicensed && activeUserCount >= maxUsersLimit && activeUserCount < maxUsersHardLimit;
}
export default UsersLimitsAnnouncementBar;
+1
View File
@@ -6438,6 +6438,7 @@
"userGuideHelp.trainingResources": "Training resources",
"users_limits_announcement_bar.copyText": "User limits exceeded. Contact administrator with: {ErrorCode}",
"users_limits_announcement_bar.ctaText": "Learn More",
"users_limits_announcement_bar.warning.copyText": "This workspace is approaching the user limit ({activeUserCount}/{maxUsersHardLimit} users).",
"userSettings.adminMode.admin_mode_badge": "Admin Mode",
"userSettings.adminMode.modal_header": "Manage {userDisplayName}'s Settings",
"userSettingsModal.pluginPreferences.header": "PLUGIN PREFERENCES",
@@ -121,6 +121,18 @@
background-color: #3d3c40;
}
.announcement-bar-warning {
background-color: var(--yellow-400);
color: var(--neutral-900);
.announcement-bar__text {
.btn.btn-tertiary.btn-xs.btn-inverted {
border-color: var(--neutral-900);
color: var(--neutral-900);
}
}
}
.announcement-bar__link {
margin-left: 4px;
}
+2
View File
@@ -151,6 +151,7 @@ export const Preferences = {
NOTIFY_ADMIN_REVOKE_DOWNGRADED_WORKSPACE: 'admin_revoke_downgraded_instance',
OVERAGE_USERS_BANNER: ReduxPreferences.CATEGORY_OVERAGE_USERS_BANNER,
POST_HISTORY_LIMIT_BANNER: ReduxPreferences.CATEGORY_POST_HISTORY_LIMIT_BANNER,
USERS_LIMITS_BANNER: 'users_limits_banner',
TO_CLOUD_YEARLY_PLAN_NUDGE: 'to_cloud_yearly_plan_nudge',
TO_PAID_PLAN_NUDGE: 'to_paid_plan_nudge',
CLOUD_ANNUAL_RENEWAL_BANNER: 'cloud_annual_renewal_banner',
@@ -932,6 +933,7 @@ export const AnnouncementBarTypes = {
ADVISOR: 'advisor',
ADVISOR_ACK: 'advisor-ack',
GENERAL: 'general',
WARNING: 'warning',
};
export const AnnouncementBarMessages = {