User usage alert in License and System Statistics page (#31063)

* Added user over usager alert in License and System Statistics page

* Fixed case below 90

* Fixed constant

* Linting

* Change over user notification threshold to 0

* Fix tests

* Saved dismissed preference

* Text tweek

* Fix tests

* More tests fixed

* Snapshot updated
Этот коммит содержится в:
Maria A Nunez
2025-05-19 14:31:47 -04:00
коммит произвёл GitHub
родитель 2116a6d94a
Коммит 2213cdbaf7
13 изменённых файлов: 447 добавлений и 66 удалений

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

@@ -19,6 +19,22 @@ exports[`components/admin_console/license_settings/LicenseSettings load screen a
<div <div
className="admin-console__banner_section" className="admin-console__banner_section"
> >
<UserSeatAlertBanner
license={
Object {
"Company": "Mattermost Inc.",
"ExpiresAt": "1620335443650",
"IsLicensed": "true",
"IssuedAt": "1517714643650",
"Name": "LicenseName",
"SkuShortName": "E20",
"StartsAt": "1517714643650",
"Users": "100",
}
}
location="license_settings"
totalUsers={10}
/>
<RenewLicenseCard <RenewLicenseCard
isLicenseExpired={false} isLicenseExpired={false}
license={ license={
@@ -123,6 +139,22 @@ exports[`components/admin_console/license_settings/LicenseSettings load screen w
<div <div
className="admin-console__banner_section" className="admin-console__banner_section"
> >
<UserSeatAlertBanner
license={
Object {
"Company": "Mattermost Inc.",
"ExpiresAt": "1620335443650",
"IsLicensed": "true",
"IssuedAt": "1517714643650",
"Name": "LicenseName",
"SkuShortName": "E20",
"StartsAt": "1517714643650",
"Users": "100",
}
}
location="license_settings"
totalUsers={10}
/>
<RenewLicenseCard <RenewLicenseCard
isLicenseExpired={false} isLicenseExpired={false}
license={ license={
@@ -227,6 +259,22 @@ exports[`components/admin_console/license_settings/LicenseSettings should match
<div <div
className="admin-console__banner_section" className="admin-console__banner_section"
> >
<UserSeatAlertBanner
license={
Object {
"Company": "Mattermost Inc.",
"ExpiresAt": "1620335443650",
"IsLicensed": "true",
"IssuedAt": "1517714643650",
"Name": "LicenseName",
"SkuShortName": "E10",
"StartsAt": "1517714643650",
"Users": "100",
}
}
location="license_settings"
totalUsers={10}
/>
<RenewLicenseCard <RenewLicenseCard
isLicenseExpired={false} isLicenseExpired={false}
license={ license={
@@ -362,6 +410,22 @@ exports[`components/admin_console/license_settings/LicenseSettings should match
<div <div
className="admin-console__banner_section" className="admin-console__banner_section"
> >
<UserSeatAlertBanner
license={
Object {
"Company": "Mattermost Inc.",
"ExpiresAt": "1620335443650",
"IsLicensed": "true",
"IssuedAt": "1517714643650",
"Name": "LicenseName",
"SkuShortName": "E20",
"StartsAt": "1517714643650",
"Users": "100",
}
}
location="license_settings"
totalUsers={10}
/>
<RenewLicenseCard <RenewLicenseCard
isLicenseExpired={false} isLicenseExpired={false}
license={ license={
@@ -485,6 +549,22 @@ exports[`components/admin_console/license_settings/LicenseSettings should match
<div <div
className="admin-console__banner_section" className="admin-console__banner_section"
> >
<UserSeatAlertBanner
license={
Object {
"Company": "Mattermost Inc.",
"ExpiresAt": "1620335443650",
"IsLicensed": "true",
"IssuedAt": "1517714643650",
"Name": "LicenseName",
"SkuShortName": "enterprise",
"StartsAt": "1517714643650",
"Users": "100",
}
}
location="license_settings"
totalUsers={10}
/>
<RenewLicenseCard <RenewLicenseCard
isLicenseExpired={false} isLicenseExpired={false}
license={ license={
@@ -608,6 +688,22 @@ exports[`components/admin_console/license_settings/LicenseSettings should match
<div <div
className="admin-console__banner_section" className="admin-console__banner_section"
> >
<UserSeatAlertBanner
license={
Object {
"Company": "Mattermost Inc.",
"ExpiresAt": "1620335443650",
"IsLicensed": "true",
"IssuedAt": "1517714643650",
"Name": "LicenseName",
"SkuShortName": "E20",
"StartsAt": "1517714643650",
"Users": "100",
}
}
location="license_settings"
totalUsers={10}
/>
<RenewLicenseCard <RenewLicenseCard
isLicenseExpired={false} isLicenseExpired={false}
license={ license={
@@ -731,6 +827,22 @@ exports[`components/admin_console/license_settings/LicenseSettings should match
<div <div
className="admin-console__banner_section" className="admin-console__banner_section"
> >
<UserSeatAlertBanner
license={
Object {
"Company": "Mattermost Inc.",
"ExpiresAt": "1620335443650",
"IsLicensed": "true",
"IssuedAt": "1517714643650",
"Name": "LicenseName",
"SkuShortName": "E20",
"StartsAt": "1517714643650",
"Users": "100",
}
}
location="license_settings"
totalUsers={10}
/>
<RenewLicenseCard <RenewLicenseCard
isLicenseExpired={false} isLicenseExpired={false}
license={ license={
@@ -854,6 +966,22 @@ exports[`components/admin_console/license_settings/LicenseSettings should match
<div <div
className="admin-console__banner_section" className="admin-console__banner_section"
> >
<UserSeatAlertBanner
license={
Object {
"Company": "Mattermost Inc.",
"ExpiresAt": "1620335443650",
"IsLicensed": "true",
"IssuedAt": "1517714643650",
"Name": "LicenseName",
"SkuShortName": "E20",
"StartsAt": "1517714643650",
"Users": "100",
}
}
location="license_settings"
totalUsers={10}
/>
<RenewLicenseCard <RenewLicenseCard
isLicenseExpired={false} isLicenseExpired={false}
license={ license={
@@ -977,6 +1105,18 @@ exports[`components/admin_console/license_settings/LicenseSettings should match
<div <div
className="admin-console__banner_section" className="admin-console__banner_section"
> >
<UserSeatAlertBanner
license={
Object {
"ExpiresAt": "1620335443650",
"IsLicensed": "true",
"IssuedAt": "1617714643650",
"StartsAt": "1617714643650",
}
}
location="license_settings"
totalUsers={10}
/>
<TrialLicenseCard <TrialLicenseCard
license={ license={
Object { Object {
@@ -1078,6 +1218,15 @@ exports[`components/admin_console/license_settings/LicenseSettings should match
<div <div
className="admin-console__banner_section" className="admin-console__banner_section"
> >
<UserSeatAlertBanner
license={
Object {
"IsLicensed": "false",
}
}
location="license_settings"
totalUsers={10}
/>
<Component <Component
enterpriseReady={true} enterpriseReady={true}
gettingTrial={false} gettingTrial={false}
@@ -1190,6 +1339,15 @@ exports[`components/admin_console/license_settings/LicenseSettings should match
<div <div
className="admin-console__banner_section" className="admin-console__banner_section"
> >
<UserSeatAlertBanner
license={
Object {
"IsLicensed": "false",
}
}
location="license_settings"
totalUsers={10}
/>
<Component <Component
enterpriseReady={true} enterpriseReady={true}
gettingTrial={false} gettingTrial={false}
@@ -1302,6 +1460,22 @@ exports[`components/admin_console/license_settings/LicenseSettings should match
<div <div
className="admin-console__banner_section" className="admin-console__banner_section"
> >
<UserSeatAlertBanner
license={
Object {
"Company": "Mattermost Inc.",
"ExpiresAt": "1620335443650",
"IsLicensed": "true",
"IssuedAt": "1517714643650",
"Name": "LicenseName",
"SkuShortName": "E20",
"StartsAt": "1517714643650",
"Users": "100",
}
}
location="license_settings"
totalUsers={10}
/>
<RenewLicenseCard <RenewLicenseCard
isLicenseExpired={false} isLicenseExpired={false}
license={ license={
@@ -1406,6 +1580,15 @@ exports[`components/admin_console/license_settings/LicenseSettings should match
<div <div
className="admin-console__banner_section" className="admin-console__banner_section"
> >
<UserSeatAlertBanner
license={
Object {
"IsLicensed": "false",
}
}
location="license_settings"
totalUsers={10}
/>
<Component <Component
enterpriseReady={false} enterpriseReady={false}
gettingTrial={false} gettingTrial={false}
@@ -1519,7 +1702,17 @@ exports[`components/admin_console/license_settings/LicenseSettings should match
> >
<div <div
className="admin-console__banner_section" className="admin-console__banner_section"
>
<UserSeatAlertBanner
license={
Object {
"IsLicensed": "false",
}
}
location="license_settings"
totalUsers={10}
/> />
</div>
<div <div
className="top-wrapper" className="top-wrapper"
> >
@@ -1616,7 +1809,25 @@ exports[`components/admin_console/license_settings/LicenseSettings should match
> >
<div <div
className="admin-console__banner_section" className="admin-console__banner_section"
>
<UserSeatAlertBanner
license={
Object {
"Cloud": "true",
"Company": "Mattermost Inc.",
"ExpiresAt": "1620993600000",
"IsLicensed": "true",
"IssuedAt": "1517714643650",
"Name": "LicenseName",
"SkuShortName": "E20",
"StartsAt": "1517714643650",
"Users": "100",
}
}
location="license_settings"
totalUsers={10}
/> />
</div>
<div <div
className="top-wrapper" className="top-wrapper"
> >
@@ -1725,6 +1936,22 @@ exports[`components/admin_console/license_settings/LicenseSettings should match
<div <div
className="admin-console__banner_section" className="admin-console__banner_section"
> >
<UserSeatAlertBanner
license={
Object {
"Company": "Mattermost Inc.",
"ExpiresAt": "1620993600000",
"IsLicensed": "true",
"IssuedAt": "1517714643650",
"Name": "LicenseName",
"SkuShortName": "E20",
"StartsAt": "1517714643650",
"Users": "100",
}
}
location="license_settings"
totalUsers={10}
/>
<RenewLicenseCard <RenewLicenseCard
isLicenseExpired={false} isLicenseExpired={false}
license={ license={

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

@@ -14,7 +14,7 @@ import mergeObjects from 'packages/mattermost-redux/test/merge_objects';
import {mountWithIntl} from 'tests/helpers/intl-test-helper'; import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {renderWithContext, screen} from 'tests/react_testing_utils'; import {renderWithContext, screen} from 'tests/react_testing_utils';
import mockStore from 'tests/test_store'; import mockStore from 'tests/test_store';
import {OverActiveUserLimits, SelfHostedProducts} from 'utils/constants'; import {SelfHostedProducts} from 'utils/constants';
import {TestHelper} from 'utils/test_helper'; import {TestHelper} from 'utils/test_helper';
import EnterpriseEditionLeftPanel from './enterprise_edition_left_panel'; import EnterpriseEditionLeftPanel from './enterprise_edition_left_panel';
@@ -132,30 +132,9 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris
expect(screen.getByText('ACTIVE USERS:')).not.toHaveClass('legend--over-seats-purchased'); expect(screen.getByText('ACTIVE USERS:')).not.toHaveClass('legend--over-seats-purchased');
}); });
test('should add warning class to active users', () => {
const minWarning = Math.ceil(parseInt(license.Users, 10) * OverActiveUserLimits.MIN) + parseInt(license.Users, 10);
const props = {
...baseProps,
statsActiveUsers: minWarning,
};
renderWithContext(
<EnterpriseEditionLeftPanel
{...props}
/>,
initialState,
);
expect(screen.getByText(Intl.NumberFormat('en').format(minWarning))).toHaveClass('value');
expect(screen.getByText(Intl.NumberFormat('en').format(minWarning))).toHaveClass('value--warning-over-seats-purchased');
expect(screen.getByText(Intl.NumberFormat('en').format(minWarning))).not.toHaveClass('value--over-seats-purchased');
expect(screen.getByText('ACTIVE USERS:')).toHaveClass('legend');
expect(screen.getByText('ACTIVE USERS:')).toHaveClass('legend--warning-over-seats-purchased');
expect(screen.getByText('ACTIVE USERS:')).not.toHaveClass('legend--over-seats-purchased');
});
test('should add over-seats-purchased class to active users', () => { test('should add over-seats-purchased class to active users', () => {
const exceedHighLimitExtraUsersError = Math.ceil(parseInt(license.Users, 10) * OverActiveUserLimits.MAX) + parseInt(license.Users, 10); // Changed to not use the constant OverActiveUserLimits.MAX given that we are currently set to 0. So the active users will be 0
const exceedHighLimitExtraUsersError = Math.ceil(parseInt(license.Users, 10) * 0.2) + parseInt(license.Users, 10);
const props = { const props = {
...baseProps, ...baseProps,
statsActiveUsers: exceedHighLimitExtraUsersError, statsActiveUsers: exceedHighLimitExtraUsersError,

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

@@ -34,6 +34,7 @@ import TeamEditionLeftPanel from './team_edition/team_edition_left_panel';
import TeamEditionRightPanel from './team_edition/team_edition_right_panel'; import TeamEditionRightPanel from './team_edition/team_edition_right_panel';
import TrialBanner from './trial_banner/trial_banner'; import TrialBanner from './trial_banner/trial_banner';
import TrialLicenseCard from './trial_license_card/trial_license_card'; import TrialLicenseCard from './trial_license_card/trial_license_card';
import UserSeatAlertBanner from './user_seat_alert_banner';
import './license_settings.scss'; import './license_settings.scss';
@@ -354,6 +355,11 @@ export default class LicenseSettings extends React.PureComponent<Props, State> {
<div className='admin-console__wrapper'> <div className='admin-console__wrapper'>
<div className='admin-console__content'> <div className='admin-console__content'>
<div className='admin-console__banner_section'> <div className='admin-console__banner_section'>
<UserSeatAlertBanner
license={license}
totalUsers={this.props.totalUsers}
location='license_settings'
/>
{!this.state.clickNormalUpgradeBtn && license.IsLicensed !== 'true' && {!this.state.clickNormalUpgradeBtn && license.IsLicensed !== 'true' &&
this.props.prevTrialLicense?.IsLicensed !== 'true' && this.props.prevTrialLicense?.IsLicensed !== 'true' &&
<TrialBanner <TrialBanner

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

@@ -0,0 +1,156 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useState, useEffect} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import type {ClientLicense} from '@mattermost/types/config';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {get as selectPreference} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import ExternalLink from 'components/external_link';
import {Preferences, LicenseLinks} from 'utils/constants';
import type {GlobalState} from 'types/store';
import AlertBanner from '../../alert_banner';
export interface UserSeatAlertBannerProps {
license: ClientLicense;
totalUsers: number;
location: 'license_settings' | 'system_statistics';
}
const UserSeatAlertBanner: React.FC<UserSeatAlertBannerProps> = ({license, totalUsers, location}) => {
const {formatMessage} = useIntl();
const dispatch = useDispatch();
const currentUser = useSelector(getCurrentUser);
const getPreferenceName = (percentUsed: number) => {
if (percentUsed < 90) {
return '';
}
const locationSuffix = location === 'license_settings' ? 'license_settings' : 'system_statistics';
if (percentUsed >= 100) {
return `100_seat_${locationSuffix}`;
} else if (percentUsed >= 95) {
return `95_seat_${locationSuffix}`;
}
return `90_seat_${locationSuffix}`;
};
const calculatePercentUsed = () => {
if (!license || !license.Users) {
return 0;
}
const licensedUsers = parseInt(license.Users, 10);
if (!licensedUsers || licensedUsers === 0) {
return 0;
}
return (totalUsers / licensedUsers) * 100;
};
const percentUsed = calculatePercentUsed();
const preferenceName = getPreferenceName(percentUsed);
const dismissed = useSelector((state: GlobalState) => selectPreference(state, Preferences.CATEGORY_SYSTEM_NOTICE, preferenceName, 'false'));
const [visible, setVisible] = useState(dismissed === 'false');
useEffect(() => {
setVisible(dismissed === 'false');
}, [dismissed]);
const handleDismiss = () => {
setVisible(false);
dispatch(savePreferences(currentUser.id, [{
user_id: currentUser.id,
category: Preferences.CATEGORY_SYSTEM_NOTICE,
name: preferenceName,
value: 'true',
}]));
};
if (!visible || !license || !license.Users || percentUsed < 90) {
return null;
}
let mode: 'success' | 'info' | 'danger' = 'info';
let title: React.ReactNode = null;
let message: React.ReactNode = null;
if (percentUsed >= 90 && percentUsed < 95) {
mode = 'success';
title = (
<FormattedMessage
id='admin.license.userSeatAlert.successTitle'
defaultMessage='Your workspace has reached 90% of your licensed seats'
/>
);
message = (
<FormattedMessage
id='admin.license.userSeatAlert.successMessage'
defaultMessage='Congratulations! Platform adoption is strong across your organization. To ensure uninterrupted growth, our team can assist in scaling your license to meet operational requirements.'
/>
);
} else if (percentUsed >= 95 && percentUsed < 100) {
mode = 'info';
title = (
<FormattedMessage
id='admin.license.userSeatAlert.infoTitle'
defaultMessage='Your workspace has reached 95% of your licensed seats'
/>
);
message = (
<FormattedMessage
id='admin.license.userSeatAlert.infoMessage'
defaultMessage='Your organization is approaching full license utilization. Now is a good time to assess future needs and align usage with procurement planning. Contact us to explore available options.'
/>
);
} else if (percentUsed >= 100) {
mode = 'danger';
title = (
<FormattedMessage
id='admin.license.userSeatAlert.dangerTitle'
defaultMessage='Your workspace has reached 100% of your licensed seats'
/>
);
message = (
<FormattedMessage
id='admin.license.userSeatAlert.dangerMessage'
defaultMessage='All licensed seats are now in use. Additional users may result in true-up charges at your next renewal. To maintain compliance and uninterrupted access, you may limit new sign-ups, or contact us to extend your license.'
/>
);
}
const actionButtonLeft = (
<ExternalLink
href={LicenseLinks.CONTACT_SALES}
location='license_settings_user_seat_alert'
className='style-button AlertBanner__buttonLeft'
>
<FormattedMessage
id='admin.license.userSeatAlert.contactSales'
defaultMessage='Contact Sales'
/>
</ExternalLink>
);
return (
<AlertBanner
mode={mode}
title={title}
message={message}
actionButtonLeft={actionButtonLeft}
onDismiss={handleDismiss}
closeBtnTooltip={formatMessage({id: 'admin.license.userSeatAlert.closeBtnTooltip', defaultMessage: 'Dismiss'})}
/>
);
};
export default UserSeatAlertBanner;

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

@@ -49,7 +49,8 @@
} }
.AlertBanner__actionButtons { .AlertBanner__actionButtons {
margin-top: 12px; margin-top: 20px;
margin-bottom: 16px;
font-family: 'Open Sans'; font-family: 'Open Sans';
.AlertBanner__buttonLeft { .AlertBanner__buttonLeft {

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

@@ -53,7 +53,7 @@ const ActivatedUserCard = ({activatedUsers, seatsPurchased, isCloud}: ActivatedU
/> />
<FormattedMessage <FormattedMessage
id='analytics.team.overageUsersSeats' id='analytics.team.overageUsersSeats'
defaultMessage='This exceeds total paid seats' defaultMessage='This exceeds total licensed seats'
> >
{(text) => <span>{text}</span>} {(text) => <span>{text}</span>}
</FormattedMessage> </FormattedMessage>

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

@@ -12,6 +12,7 @@ import {getFormattedFileSize} from 'mattermost-redux/utils/file_utils';
import * as AdminActions from 'actions/admin_actions.jsx'; import * as AdminActions from 'actions/admin_actions.jsx';
import UserSeatAlertBanner from 'components/admin_console/license_settings/user_seat_alert_banner';
import ActivatedUserCard from 'components/analytics/activated_users_card'; import ActivatedUserCard from 'components/analytics/activated_users_card';
import ExternalLink from 'components/external_link'; import ExternalLink from 'components/external_link';
import AdminHeader from 'components/widgets/admin_console/admin_header'; import AdminHeader from 'components/widgets/admin_console/admin_header';
@@ -510,6 +511,11 @@ export default class SystemAnalytics extends React.PureComponent<Props, State> {
</AdminHeader> </AdminHeader>
<div className='admin-console__wrapper'> <div className='admin-console__wrapper'>
<div className='admin-console__content'> <div className='admin-console__content'>
<UserSeatAlertBanner
license={this.props.license}
totalUsers={this.getStatValue(stats[StatTypes.TOTAL_USERS]) || 0}
location='system_statistics'
/>
{banner} {banner}
<div className='grid-statistics'> <div className='grid-statistics'>
{systemCards} {systemCards}

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

@@ -27,15 +27,10 @@ import './overage_users_banner.scss';
type AdminHasDismissedItArgs = { type AdminHasDismissedItArgs = {
preferenceName: string; preferenceName: string;
overagePreferences: PreferenceType[]; overagePreferences: PreferenceType[];
isWarningBanner: boolean;
} }
const adminHasDismissed = ({preferenceName, overagePreferences, isWarningBanner}: AdminHasDismissedItArgs): boolean => { const adminHasDismissed = ({preferenceName, overagePreferences}: AdminHasDismissedItArgs): boolean => {
if (isWarningBanner) {
return overagePreferences.find((value) => value.name === preferenceName) !== undefined; return overagePreferences.find((value) => value.name === preferenceName) !== undefined;
}
return false;
}; };
const OverageUsersBanner = () => { const OverageUsersBanner = () => {
@@ -62,7 +57,7 @@ const OverageUsersBanner = () => {
const overageByUsers = activeUsers - seatsPurchased; const overageByUsers = activeUsers - seatsPurchased;
const isOverageState = isBetween5PercerntAnd10PercentPurchasedSeats || isOver10PercerntPurchasedSeats; const isOverageState = overageByUsers > 0 && (isBetween5PercerntAnd10PercentPurchasedSeats || isOver10PercerntPurchasedSeats);
const hasPermission = isAdmin && isOverageState && !isCloud; const hasPermission = isAdmin && isOverageState && !isCloud;
const { const {
cta, cta,
@@ -89,14 +84,14 @@ const OverageUsersBanner = () => {
const handleClick = handleContactSalesClick; const handleClick = handleContactSalesClick;
if (!hasPermission || adminHasDismissed({isWarningBanner: isBetween5PercerntAnd10PercentPurchasedSeats, overagePreferences, preferenceName})) { if (!hasPermission || adminHasDismissed({overagePreferences, preferenceName})) {
return null; return null;
} }
const message = ( const message = (
<FormattedMessage <FormattedMessage
id='licensingPage.overageUsersBanner.text' id='licensingPage.overageUsersBanner.text'
defaultMessage='(Only visible to admins) Your workspace user count has exceeded your paid license seat count by {seats, number} {seats, plural, one {seat} other {seats}}. Purchase additional seats to remain compliant.' defaultMessage='(Only visible to admins) The user count exceeds the number of licensed seats by {seats, number} {seats, plural, one {seat} other {seats}}. Purchase more seats to stay compliant.'
values={{ values={{
seats: overageByUsers, seats: overageByUsers,
}} }}
@@ -105,7 +100,7 @@ const OverageUsersBanner = () => {
return ( return (
<AnnouncementBar <AnnouncementBar
type={isBetween5PercerntAnd10PercentPurchasedSeats ? AnnouncementBarTypes.ADVISOR : AnnouncementBarTypes.CRITICAL} type={isBetween5PercerntAnd10PercentPurchasedSeats ? AnnouncementBarTypes.ADVISOR : AnnouncementBarTypes.CRITICAL}
showCloseButton={isBetween5PercerntAnd10PercentPurchasedSeats} showCloseButton={true}
onButtonClick={handleClick} onButtonClick={handleClick}
modalButtonText={cta} modalButtonText={cta}
message={message} message={message}

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

@@ -39,12 +39,12 @@ jest.mock('actions/telemetry_actions', () => ({
const seatsPurchased = 40; const seatsPurchased = 40;
const email = 'test@mattermost.com'; const email = 'test@mattermost.com';
const seatsMinimumFor5PercentageState = (Math.ceil(seatsPurchased * OverActiveUserLimits.MIN)) + seatsPurchased; const seatsMinimumFor5PercentageState = (Math.ceil(seatsPurchased * OverActiveUserLimits.MIN)) + seatsPurchased + 1;
const seatsMinimumFor10PercentageState = (Math.ceil(seatsPurchased * OverActiveUserLimits.MAX)) + seatsPurchased; const seatsMinimumFor10PercentageState = (Math.ceil(seatsPurchased * OverActiveUserLimits.MAX)) + seatsPurchased + 1;
const text5PercentageState = `(Only visible to admins) Your workspace user count has exceeded your paid license seat count by ${seatsMinimumFor5PercentageState - seatsPurchased} seats. Purchase additional seats to remain compliant.`; const text5PercentageState = `(Only visible to admins) The user count exceeds the number of licensed seats by ${seatsMinimumFor5PercentageState - seatsPurchased} seat. Purchase more seats to stay compliant.`;
const text10PercentageState = `(Only visible to admins) Your workspace user count has exceeded your paid license seat count by ${seatsMinimumFor10PercentageState - seatsPurchased} seats. Purchase additional seats to remain compliant.`; const text10PercentageState = `(Only visible to admins) The user count exceeds the number of licensed seats by ${seatsMinimumFor10PercentageState - seatsPurchased} seat. Purchase more seats to stay compliant.`;
const contactSalesTextLink = 'Contact Sales'; const contactSalesTextLink = 'Contact Sales';
@@ -127,7 +127,7 @@ describe('components/overage_users_banner', () => {
it('should not render the banner because we are not on overage state', () => { it('should not render the banner because we are not on overage state', () => {
renderWithContext(<OverageUsersBanner/>); renderWithContext(<OverageUsersBanner/>);
expect(screen.queryByText('(Only visible to admins) Your workspace user count has exceeded your paid license seat count by', {exact: false})).not.toBeInTheDocument(); expect(screen.queryByText('(Only visible to admins) The user count exceeds the number of licensed seats by', {exact: false})).not.toBeInTheDocument();
}); });
it('should not render the banner because we are not admins', () => { it('should not render the banner because we are not admins', () => {
@@ -146,7 +146,7 @@ describe('components/overage_users_banner', () => {
renderWithContext(<OverageUsersBanner/>, store); renderWithContext(<OverageUsersBanner/>, store);
expect(screen.queryByText('Your workspace user count has exceeded your paid license seat count by', {exact: false})).not.toBeInTheDocument(); expect(screen.queryByText('Your workspace user count has exceeded your licensed seat count by', {exact: false})).not.toBeInTheDocument();
}); });
it('should not render the banner because it\'s cloud licenese', () => { it('should not render the banner because it\'s cloud licenese', () => {
@@ -159,7 +159,7 @@ describe('components/overage_users_banner', () => {
renderWithContext(<OverageUsersBanner/>, store); renderWithContext(<OverageUsersBanner/>, store);
expect(screen.queryByText('Your workspace user count has exceeded your paid license seat count by', {exact: false})).not.toBeInTheDocument(); expect(screen.queryByText('Your workspace user count has exceeded your licensed seat count by', {exact: false})).not.toBeInTheDocument();
}); });
it('should not render the 5% banner because we have dissmised it', () => { it('should not render the 5% banner because we have dissmised it', () => {
@@ -170,7 +170,7 @@ describe('components/overage_users_banner', () => {
{ {
category: Preferences.OVERAGE_USERS_BANNER, category: Preferences.OVERAGE_USERS_BANNER,
value: 'Overage users banner watched', value: 'Overage users banner watched',
name: `warn_overage_seats_${licenseId.substring(0, 8)}`, name: `error_overage_seats_${licenseId.substring(0, 8)}`,
}, },
], ],
); );
@@ -230,7 +230,7 @@ describe('components/overage_users_banner', () => {
const salesLinkWithEncodedParams = 'https://mattermost.com/contact-sales/?qk=&qp=&qw=&qx=dGVzdEBtYXR0ZXJtb3N0LmNvbQ==&utm_source=mattermost&utm_medium=in-product'; const salesLinkWithEncodedParams = 'https://mattermost.com/contact-sales/?qk=&qp=&qw=&qx=dGVzdEBtYXR0ZXJtb3N0LmNvbQ==&utm_source=mattermost&utm_medium=in-product';
expect(windowSpy).toBeCalledWith(salesLinkWithEncodedParams, '_blank'); expect(windowSpy).toBeCalledWith(salesLinkWithEncodedParams, '_blank');
expect(trackEvent).toBeCalledTimes(1); expect(trackEvent).toBeCalledTimes(1);
expect(trackEvent).toBeCalledWith('insights', 'click_true_up_warning', { expect(trackEvent).toBeCalledWith('insights', 'click_true_up_error', {
cta: 'Contact Sales', cta: 'Contact Sales',
banner: 'global banner', banner: 'global banner',
}); });
@@ -248,7 +248,7 @@ describe('components/overage_users_banner', () => {
{ {
category: Preferences.OVERAGE_USERS_BANNER, category: Preferences.OVERAGE_USERS_BANNER,
value: 'Overage users banner watched', value: 'Overage users banner watched',
name: `warn_overage_seats_${10}`, name: `error_overage_seats_${10}`,
}, },
], ],
); );
@@ -283,7 +283,7 @@ describe('components/overage_users_banner', () => {
expect(savePreferences).toBeCalledTimes(1); expect(savePreferences).toBeCalledTimes(1);
expect(savePreferences).toBeCalledWith(store.entities.users.profiles.current_user.id, [{ expect(savePreferences).toBeCalledWith(store.entities.users.profiles.current_user.id, [{
category: Preferences.OVERAGE_USERS_BANNER, category: Preferences.OVERAGE_USERS_BANNER,
name: `warn_overage_seats_${licenseId.substring(0, 8)}`, name: `error_overage_seats_${licenseId.substring(0, 8)}`,
user_id: store.entities.users.profiles.current_user.id, user_id: store.entities.users.profiles.current_user.id,
value: 'Overage users banner watched', value: 'Overage users banner watched',
}]); }]);
@@ -319,7 +319,7 @@ describe('components/overage_users_banner', () => {
store.entities.admin = { store.entities.admin = {
...store.entities.admin, ...store.entities.admin,
analytics: { analytics: {
[StatTypes.TOTAL_USERS]: seatsMinimumFor10PercentageState, [StatTypes.TOTAL_USERS]: seatsMinimumFor10PercentageState + 1,
}, },
}; };

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

@@ -58,7 +58,7 @@ const OverageUsersBannerNotice = () => {
const preferenceName = `${prefixPreferences}_overage_seats_${prefixLicenseId}`; const preferenceName = `${prefixPreferences}_overage_seats_${prefixLicenseId}`;
const overageByUsers = activeUsers - seatsPurchased; const overageByUsers = activeUsers - seatsPurchased;
const isOverageState = isBetween5PercerntAnd10PercentPurchasedSeats || isOver10PercerntPurchasedSeats; const isOverageState = overageByUsers > 0 && (isBetween5PercerntAnd10PercentPurchasedSeats || isOver10PercerntPurchasedSeats);
const hasPermission = isAdmin && isOverageState && !isCloud; const hasPermission = isAdmin && isOverageState && !isCloud;
const { const {
cta, cta,
@@ -120,7 +120,7 @@ const OverageUsersBannerNotice = () => {
title={ title={
<FormattedMessage <FormattedMessage
id='licensingPage.overageUsersBanner.noticeTitle' id='licensingPage.overageUsersBanner.noticeTitle'
defaultMessage='Your workspace user count has exceeded your paid license seat count by {seats, number} {seats, plural, one {seat} other {seats}}' defaultMessage='Your workspace user count has exceeded your licensed seat count by {seats, number} {seats, plural, one {seat} other {seats}}'
values={{ values={{
seats: overageByUsers, seats: overageByUsers,
}} }}

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

@@ -41,12 +41,12 @@ jest.mock('actions/telemetry_actions', () => ({
const seatsPurchased = 40; const seatsPurchased = 40;
const seatsMinimumFor5PercentageState = (Math.ceil(seatsPurchased * OverActiveUserLimits.MIN)) + seatsPurchased; const seatsMinimumFor5PercentageState = (Math.ceil(seatsPurchased * OverActiveUserLimits.MIN)) + seatsPurchased + 1;
const seatsMinimumFor10PercentageState = (Math.ceil(seatsPurchased * OverActiveUserLimits.MAX)) + seatsPurchased; const seatsMinimumFor10PercentageState = (Math.ceil(seatsPurchased * OverActiveUserLimits.MAX)) + seatsPurchased + 1;
const text5PercentageState = `Your workspace user count has exceeded your paid license seat count by ${seatsMinimumFor5PercentageState - seatsPurchased} seats`; const text5PercentageState = `Your workspace user count has exceeded your licensed seat count by ${seatsMinimumFor5PercentageState - seatsPurchased} seat`;
const text10PercentageState = `Your workspace user count has exceeded your paid license seat count by ${seatsMinimumFor10PercentageState - seatsPurchased} seats`; const text10PercentageState = `Your workspace user count has exceeded your licensed seat count by ${seatsMinimumFor10PercentageState - seatsPurchased} seat`;
const notifyText = 'Notify your Customer Success Manager on your next true-up check'; const notifyText = 'Notify your Customer Success Manager on your next true-up check';
const contactSalesTextLink = 'Contact Sales'; const contactSalesTextLink = 'Contact Sales';
@@ -169,7 +169,7 @@ describe('components/invitation_modal/overage_users_banner_notice', () => {
{ {
category: Preferences.OVERAGE_USERS_BANNER, category: Preferences.OVERAGE_USERS_BANNER,
value: 'Overage users banner watched', value: 'Overage users banner watched',
name: `warn_overage_seats_${licenseId.substring(0, 8)}`, name: `error_overage_seats_${licenseId.substring(0, 8)}`,
}, },
], ],
); );
@@ -234,7 +234,7 @@ describe('components/invitation_modal/overage_users_banner_notice', () => {
'?utm_source=mattermost&utm_medium=in-product&utm_content=overage_users_banner&uid=current_user&sid=', '?utm_source=mattermost&utm_medium=in-product&utm_content=overage_users_banner&uid=current_user&sid=',
); );
expect(trackEvent).toBeCalledTimes(2); expect(trackEvent).toBeCalledTimes(2);
expect(trackEvent).toBeCalledWith('insights', 'click_true_up_warning', { expect(trackEvent).toBeCalledWith('insights', 'click_true_up_error', {
cta: 'Contact Sales', cta: 'Contact Sales',
banner: 'invite modal', banner: 'invite modal',
}); });
@@ -248,7 +248,7 @@ describe('components/invitation_modal/overage_users_banner_notice', () => {
{ {
category: Preferences.OVERAGE_USERS_BANNER, category: Preferences.OVERAGE_USERS_BANNER,
value: 'Overage users banner watched', value: 'Overage users banner watched',
name: `warn_overage_seats_${generateId().substring(0, 8)}`, name: `error_overage_seats_${generateId().substring(0, 8)}`,
}, },
], ],
); );
@@ -289,7 +289,7 @@ describe('components/invitation_modal/overage_users_banner_notice', () => {
expect(savePreferences).toBeCalledTimes(1); expect(savePreferences).toBeCalledTimes(1);
expect(savePreferences).toBeCalledWith(store.entities.users.profiles.current_user.id, [{ expect(savePreferences).toBeCalledWith(store.entities.users.profiles.current_user.id, [{
category: Preferences.OVERAGE_USERS_BANNER, category: Preferences.OVERAGE_USERS_BANNER,
name: `warn_overage_seats_${licenseId.substring(0, 8)}`, name: `error_overage_seats_${licenseId.substring(0, 8)}`,
user_id: store.entities.users.profiles.current_user.id, user_id: store.entities.users.profiles.current_user.id,
value: 'Overage users banner watched', value: 'Overage users banner watched',
}]); }]);

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

@@ -1565,6 +1565,14 @@
"admin.license.upload-modal.successfulUpgradeText": "You have upgraded to the {skuName} plan for {licensedUsersNum, number} seats. This is effective from {startsAt} until {expiresAt}. ", "admin.license.upload-modal.successfulUpgradeText": "You have upgraded to the {skuName} plan for {licensedUsersNum, number} seats. This is effective from {startsAt} until {expiresAt}. ",
"admin.license.upload-modal.title": "Upload a License Key", "admin.license.upload-modal.title": "Upload a License Key",
"admin.license.uploadFile": "Upload File", "admin.license.uploadFile": "Upload File",
"admin.license.userSeatAlert.closeBtnTooltip": "Dismiss",
"admin.license.userSeatAlert.contactSales": "Contact Sales",
"admin.license.userSeatAlert.dangerMessage": "All licensed seats are now in use. Additional users may result in true-up charges at your next renewal. To maintain compliance and uninterrupted access, you may limit new sign-ups, or contact us to extend your license.",
"admin.license.userSeatAlert.dangerTitle": "Your workspace has reached 100% of your licensed seats",
"admin.license.userSeatAlert.infoMessage": "Your organization is approaching full license utilization. Now is a good time to assess future needs and align usage with procurement planning. Contact us to explore available options.",
"admin.license.userSeatAlert.infoTitle": "Your workspace has reached 95% of your licensed seats",
"admin.license.userSeatAlert.successMessage": "Congratulations! Platform adoption is strong across your organization. To ensure uninterrupted growth, our team can assist in scaling your license to meet operational requirements.",
"admin.license.userSeatAlert.successTitle": "Your workspace has reached 90% of your licensed seats",
"admin.licenseSettings.teamEdition.teamEditionRightPanel.acceptTermsFinal": ". Upgrading will download the binary and update your team edition.", "admin.licenseSettings.teamEdition.teamEditionRightPanel.acceptTermsFinal": ". Upgrading will download the binary and update your team edition.",
"admin.licenseSettings.teamEdition.teamEditionRightPanel.acceptTermsInitial": "By clicking <b>Upgrade</b>, I agree to the terms of the Mattermost ", "admin.licenseSettings.teamEdition.teamEditionRightPanel.acceptTermsInitial": "By clicking <b>Upgrade</b>, I agree to the terms of the Mattermost ",
"admin.licenseSettings.teamEdition.teamEditionRightPanel.eeLicenseLink": "Enterprise Edition License", "admin.licenseSettings.teamEdition.teamEditionRightPanel.eeLicenseLink": "Enterprise Edition License",
@@ -3105,7 +3113,7 @@
"analytics.team.activeUsers": "Active Users With Posts", "analytics.team.activeUsers": "Active Users With Posts",
"analytics.team.newlyCreated": "Newly Created Users", "analytics.team.newlyCreated": "Newly Created Users",
"analytics.team.noTeams": "This server has no teams for which to view statistics.", "analytics.team.noTeams": "This server has no teams for which to view statistics.",
"analytics.team.overageUsersSeats": "This exceeds total paid seats", "analytics.team.overageUsersSeats": "This exceeds total licensed seats",
"analytics.team.privateGroups": "Private Channels", "analytics.team.privateGroups": "Private Channels",
"analytics.team.publicChannels": "Public Channels", "analytics.team.publicChannels": "Public Channels",
"analytics.team.recentUsers": "Recent Active Users", "analytics.team.recentUsers": "Recent Active Users",
@@ -4462,8 +4470,8 @@
"licensingPage.infoBanner.startTrialTitle": "Start a free 30-day tral of Mattermost Enterprise Advanced", "licensingPage.infoBanner.startTrialTitle": "Start a free 30-day tral of Mattermost Enterprise Advanced",
"licensingPage.overageUsersBanner.cta": "Contact Sales", "licensingPage.overageUsersBanner.cta": "Contact Sales",
"licensingPage.overageUsersBanner.noticeDescription": "Notify your Customer Success Manager on your next true-up check. <a></a>", "licensingPage.overageUsersBanner.noticeDescription": "Notify your Customer Success Manager on your next true-up check. <a></a>",
"licensingPage.overageUsersBanner.noticeTitle": "Your workspace user count has exceeded your paid license seat count by {seats, number} {seats, plural, one {seat} other {seats}}", "licensingPage.overageUsersBanner.noticeTitle": "Your workspace user count has exceeded your licensed seat count by {seats, number} {seats, plural, one {seat} other {seats}}",
"licensingPage.overageUsersBanner.text": "(Only visible to admins) Your workspace user count has exceeded your paid license seat count by {seats, number} {seats, plural, one {seat} other {seats}}. Purchase additional seats to remain compliant.", "licensingPage.overageUsersBanner.text": "(Only visible to admins) The user count exceeds the number of licensed seats by {seats, number} {seats, plural, one {seat} other {seats}}. Purchase more seats to stay compliant.",
"link_preview.image_preview": "Show Image preview", "link_preview.image_preview": "Show Image preview",
"link_preview.remove_link_preview": "Remove link preview", "link_preview.remove_link_preview": "Remove link preview",
"list_modal.paginatorCount": "{startCount, number} - {endCount, number} of {total, number} total", "list_modal.paginatorCount": "{startCount, number} - {endCount, number} of {total, number} total",

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

@@ -2250,9 +2250,12 @@ export const DataSearchTypes = {
MESSAGES_SEARCH_TYPE: 'messages', MESSAGES_SEARCH_TYPE: 'messages',
} as const; } as const;
// Currently, the overage user limits are set to 0, which means that the overage active users warning
// is immediatly shown when over 100% usage.
// Originally it was set to Min 5% and Max 10% (5% would be a blue warning and 10% would be a red warning).
export const OverActiveUserLimits = { export const OverActiveUserLimits = {
MIN: 0.05, MIN: 0,
MAX: 0.1, MAX: 0,
} as const; } as const;
export const PageLoadContext = { export const PageLoadContext = {