mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
[MM-56318] Global warning banners of user limit for admins (#25797)
* get data to store * inin * added inside loadme * useCWSAvailabilityCheck hook improve * modal airgap * the bar * Update announcement_bar_controller.tsx * Update true_up_review.tsx * csw * fixes * fixed * more tests * icon changed * changes * Extended tests * Update index.ts * redux harrison changes * Updated copy * Lint fixes --------- Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: harshil Sharma <harshilsharma63@gmail.com>
This commit is contained in:
@@ -7,6 +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 {getFilteredUsersStats} from 'mattermost-redux/actions/users';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getFilteredUsersStats as selectFilteredUserStats} from 'mattermost-redux/selectors/entities/users';
|
||||
@@ -42,6 +43,7 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
||||
requestTrialLicense,
|
||||
openModal,
|
||||
getFilteredUsersStats,
|
||||
getUsersLimits,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,6 +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(),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {FormattedMessage} 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 {GetFilteredUsersStatsOpts, UsersStats} from '@mattermost/types/users';
|
||||
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
@@ -46,7 +47,7 @@ type Props = {
|
||||
actions: {
|
||||
getLicenseConfig: () => void;
|
||||
uploadLicense: (file: File) => Promise<ActionResult>;
|
||||
removeLicense: () => Promise<ActionResult>;
|
||||
removeLicense: () => Promise<ActionResult<boolean, ServerError>>;
|
||||
getPrevTrialLicense: () => void;
|
||||
upgradeToE0: () => Promise<StatusOK>;
|
||||
upgradeToE0Status: () => Promise<{percentage: number; error: string | JSX.Element | null}>;
|
||||
@@ -54,6 +55,7 @@ type Props = {
|
||||
ping: () => Promise<{status: string}>;
|
||||
requestTrialLicense: (users: number, termsAccepted: boolean, receiveEmailsAccepted: boolean, featureName: string) => Promise<ActionResult>;
|
||||
openModal: <P>(modalData: ModalData<P>) => void;
|
||||
getUsersLimits: () => Promise<ActionResult<UsersLimits, ServerError>>;
|
||||
getFilteredUsersStats: (filters: GetFilteredUsersStatsOpts) => Promise<{
|
||||
data?: UsersStats;
|
||||
error?: ServerError;
|
||||
@@ -179,8 +181,13 @@ export default class LicenseSettings extends React.PureComponent<Props, State> {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.actions.getPrevTrialLicense();
|
||||
await this.props.actions.getLicenseConfig();
|
||||
await Promise.all([
|
||||
this.props.actions.getPrevTrialLicense(),
|
||||
this.props.actions.getLicenseConfig(),
|
||||
]);
|
||||
|
||||
await this.props.actions.getUsersLimits();
|
||||
|
||||
this.setState({serverError: null, removing: false});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import accessProblemImage from 'images/access_problem.svg';
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
@@ -12,6 +11,7 @@ import {closeModal} from 'actions/views/modals';
|
||||
|
||||
import ExternalLink from 'components/external_link';
|
||||
|
||||
import accessProblemImage from 'images/air_gapped_contact_us_image.png';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import './style.scss';
|
||||
|
||||
@@ -6,8 +6,13 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.air-gapped-contact-sales-modal-body > .image {
|
||||
align-self: center;
|
||||
& > .image {
|
||||
align-self: center;
|
||||
|
||||
& > img {
|
||||
max-width: 300px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('TrueUpReview', () => {
|
||||
|
||||
};
|
||||
it('regular self hosted license in the true up window sees content', () => {
|
||||
jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => true);
|
||||
jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => useCWSAvailabilityCheckAll.CSWAvailabilityCheckTypes.Available);
|
||||
|
||||
renderWithContext(<TrueUpReview/>, showsTrueUpReviewState);
|
||||
screen.getByText('Share to Mattermost');
|
||||
@@ -65,7 +65,7 @@ describe('TrueUpReview', () => {
|
||||
it('gov sku self-hosted license does not see true up content', () => {
|
||||
const store = JSON.parse(JSON.stringify(showsTrueUpReviewState));
|
||||
store.entities.general.license.IsGovSku = 'true';
|
||||
jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => true);
|
||||
jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => useCWSAvailabilityCheckAll.CSWAvailabilityCheckTypes.Available);
|
||||
|
||||
renderWithContext(<TrueUpReview/>, store);
|
||||
expect(screen.queryByText('Share to Mattermost')).not.toBeInTheDocument();
|
||||
|
||||
@@ -21,7 +21,7 @@ import {isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/user
|
||||
import {submitTrueUpReview, getTrueUpReviewStatus} from 'actions/hosted_customer';
|
||||
import {pageVisited} from 'actions/telemetry_actions';
|
||||
|
||||
import useCWSAvailabilityCheck from 'components/common/hooks/useCWSAvailabilityCheck';
|
||||
import useCWSAvailabilityCheck, {CSWAvailabilityCheckTypes} from 'components/common/hooks/useCWSAvailabilityCheck';
|
||||
import ExternalLink from 'components/external_link';
|
||||
import CheckMarkSvg from 'components/widgets/icons/check_mark_icon';
|
||||
import WarningIcon from 'components/widgets/icons/fa_warning_icon';
|
||||
@@ -34,7 +34,8 @@ import './true_up_review.scss';
|
||||
const TrueUpReview: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const isCloud = useSelector(isCurrentLicenseCloud);
|
||||
const isAirGapped = !useCWSAvailabilityCheck();
|
||||
const cwsAvailability = useCWSAvailabilityCheck();
|
||||
const isAirGapped = cwsAvailability !== CSWAvailabilityCheckTypes.Available;
|
||||
const reviewProfile = useSelector(trueUpReviewProfileSelector);
|
||||
const reviewStatus = useSelector(trueUpReviewStatusSelector);
|
||||
const isSystemAdmin = useSelector(isCurrentUserSystemAdmin);
|
||||
|
||||
@@ -33,9 +33,6 @@ exports[`components/AnnouncementBar should match snapshot, bar not showing 1`] =
|
||||
id="text"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="announcement-bar__link"
|
||||
/>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</_StyledDiv>
|
||||
@@ -74,9 +71,6 @@ exports[`components/AnnouncementBar should match snapshot, bar showing 1`] = `
|
||||
id="text"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="announcement-bar__link"
|
||||
/>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</_StyledDiv>
|
||||
@@ -115,9 +109,6 @@ exports[`components/AnnouncementBar should match snapshot, bar showing, no dismi
|
||||
id="text"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="announcement-bar__link"
|
||||
/>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</_StyledDiv>
|
||||
@@ -156,9 +147,6 @@ exports[`components/AnnouncementBar should match snapshot, dismissal 1`] = `
|
||||
id="text"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="announcement-bar__link"
|
||||
/>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</_StyledDiv>
|
||||
@@ -197,9 +185,6 @@ exports[`components/AnnouncementBar should match snapshot, dismissal 2`] = `
|
||||
id="text"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="announcement-bar__link"
|
||||
/>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</_StyledDiv>
|
||||
@@ -238,9 +223,6 @@ exports[`components/AnnouncementBar should match snapshot, dismissal 3`] = `
|
||||
id="text"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="announcement-bar__link"
|
||||
/>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</_StyledDiv>
|
||||
@@ -279,9 +261,6 @@ exports[`components/AnnouncementBar should match snapshot, props change 1`] = `
|
||||
id="text"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="announcement-bar__link"
|
||||
/>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</_StyledDiv>
|
||||
@@ -320,9 +299,6 @@ exports[`components/AnnouncementBar should match snapshot, props change 2`] = `
|
||||
id="text"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="announcement-bar__link"
|
||||
/>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</_StyledDiv>
|
||||
@@ -361,9 +337,6 @@ exports[`components/AnnouncementBar should match snapshot, props change 3`] = `
|
||||
id="text"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="announcement-bar__link"
|
||||
/>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</_StyledDiv>
|
||||
@@ -402,9 +375,6 @@ exports[`components/AnnouncementBar should match snapshot, props change 4`] = `
|
||||
id="text"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="announcement-bar__link"
|
||||
/>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</_StyledDiv>
|
||||
|
||||
@@ -19,15 +19,14 @@ import PaymentAnnouncementBar from './payment_announcement_bar';
|
||||
import AutoStartTrialModal from './show_start_trial_modal/show_start_trial_modal';
|
||||
import ShowThreeDaysLeftTrialModal from './show_tree_days_left_trial_modal/show_three_days_left_trial_modal';
|
||||
import TextDismissableBar from './text_dismissable_bar';
|
||||
import UsersLimitsAnnouncementBar from './users_limits_announcement_bar';
|
||||
import VersionBar from './version_bar';
|
||||
|
||||
type Props = {
|
||||
license?: ClientLicense;
|
||||
config?: Partial<ClientConfig>;
|
||||
canViewSystemErrors: boolean;
|
||||
isCloud: boolean;
|
||||
userIsAdmin: boolean;
|
||||
subscription?: Subscription;
|
||||
latestError?: {
|
||||
error: any;
|
||||
};
|
||||
@@ -103,6 +102,10 @@ class AnnouncementBarController extends React.PureComponent<Props> {
|
||||
<>
|
||||
{adminConfiguredAnnouncementBar}
|
||||
{errorBar}
|
||||
<UsersLimitsAnnouncementBar
|
||||
license={this.props.license}
|
||||
userIsAdmin={this.props.userIsAdmin}
|
||||
/>
|
||||
{paymentAnnouncementBar}
|
||||
{cloudTrialAnnouncementBar}
|
||||
{cloudTrialEndAnnouncementBar}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ReactNode} from 'react';
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
@@ -23,8 +24,8 @@ type Props = {
|
||||
color: string;
|
||||
textColor: string;
|
||||
type: string;
|
||||
message: React.ReactNode;
|
||||
tooltipMsg?: React.ReactNode;
|
||||
message: ReactNode;
|
||||
tooltipMsg?: ReactNode;
|
||||
handleClose?: (e?: any) => void;
|
||||
showModal?: boolean;
|
||||
announcementBarCount?: number;
|
||||
@@ -32,13 +33,15 @@ type Props = {
|
||||
modalButtonText?: string;
|
||||
modalButtonDefaultText?: string;
|
||||
showLinkAsButton: boolean;
|
||||
icon?: React.ReactNode;
|
||||
icon?: ReactNode;
|
||||
warnMetricStatus?: WarnMetricStatus;
|
||||
actions: {
|
||||
incrementAnnouncementBarCount: () => void;
|
||||
decrementAnnouncementBarCount: () => void;
|
||||
};
|
||||
showCTA?: boolean;
|
||||
ctaText?: ReactNode;
|
||||
ctaDisabled?: boolean;
|
||||
}
|
||||
|
||||
type State = {
|
||||
@@ -189,7 +192,7 @@ export default class AnnouncementBar extends React.PureComponent<Props, State> {
|
||||
{message}
|
||||
</span>
|
||||
{
|
||||
!this.props.showLinkAsButton && this.props.showCTA &&
|
||||
!this.props.showLinkAsButton && this.props.showCTA && this.props.modalButtonText && this.props.modalButtonDefaultText &&
|
||||
<span className='announcement-bar__link'>
|
||||
{this.props.showModal &&
|
||||
<FormattedMessage
|
||||
@@ -216,10 +219,10 @@ export default class AnnouncementBar extends React.PureComponent<Props, State> {
|
||||
</span>
|
||||
}
|
||||
{
|
||||
this.props.showLinkAsButton && this.props.showCTA &&
|
||||
this.props.showLinkAsButton && this.props.showCTA && this.props.modalButtonText && this.props.modalButtonDefaultText &&
|
||||
<button
|
||||
className='upgrade-button'
|
||||
onClick={this.props.onButtonClick}
|
||||
disabled={this.props.ctaDisabled}
|
||||
>
|
||||
<FormattedMessage
|
||||
id={this.props.modalButtonText}
|
||||
@@ -227,6 +230,15 @@ export default class AnnouncementBar extends React.PureComponent<Props, State> {
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
{
|
||||
this.props.showLinkAsButton && this.props.showCTA && this.props.ctaText &&
|
||||
<button
|
||||
onClick={this.props.onButtonClick}
|
||||
disabled={this.props.ctaDisabled}
|
||||
>
|
||||
{this.props.ctaText}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
{closeButton}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shouldShowUserLimitsAnnouncementBar} from './index';
|
||||
import type {ShouldShowingUserLimitsAnnouncementBarProps} from './index';
|
||||
|
||||
describe('shouldShowUserLimitsAnnouncementBar', () => {
|
||||
const defaultProps: ShouldShowingUserLimitsAnnouncementBarProps = {
|
||||
userIsAdmin: true,
|
||||
isLicensed: false,
|
||||
maxUsersLimit: 10,
|
||||
activeUserCount: 5,
|
||||
};
|
||||
|
||||
test('should not show when user is not admin', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
userIsAdmin: false,
|
||||
};
|
||||
expect(shouldShowUserLimitsAnnouncementBar(props)).toBe(false);
|
||||
});
|
||||
|
||||
test('should not show when active users count is 0', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
activeUserCount: 0,
|
||||
};
|
||||
expect(shouldShowUserLimitsAnnouncementBar(props)).toBe(false);
|
||||
});
|
||||
|
||||
test('should not show when max users limit is 0', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
maxUsersLimit: 0,
|
||||
};
|
||||
expect(shouldShowUserLimitsAnnouncementBar(props)).toBe(false);
|
||||
});
|
||||
|
||||
test('should not show when active users count is less than max users 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,
|
||||
activeUserCount: 15,
|
||||
maxUsersLimit: 10,
|
||||
};
|
||||
expect(shouldShowUserLimitsAnnouncementBar(props)).toBe(true);
|
||||
});
|
||||
|
||||
test('should not show for licensed server', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
isLicensed: true,
|
||||
activeUserCount: 0,
|
||||
maxUsersLimit: 0,
|
||||
};
|
||||
|
||||
expect(shouldShowUserLimitsAnnouncementBar(props)).toBe(false);
|
||||
});
|
||||
|
||||
test('should not show for licensed server even if user count is greater than max users limit', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
isLicensed: true,
|
||||
activeUserCount: 101,
|
||||
maxUsersLimit: 100,
|
||||
};
|
||||
|
||||
expect(shouldShowUserLimitsAnnouncementBar(props)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
// 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 {getUsersLimits} 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-user-limits-exceeded';
|
||||
|
||||
function UsersLimitsAnnouncementBar(props: Props) {
|
||||
const usersLimits = useSelector(getUsersLimits);
|
||||
|
||||
const handleCTAClick = useCallback(() => {
|
||||
window.open(learnMoreExternalLink, '_blank');
|
||||
}, []);
|
||||
|
||||
const isLicensed = props?.license?.IsLicensed === 'true';
|
||||
const maxUsersLimit = usersLimits?.maxUsersLimit ?? 0;
|
||||
const activeUserCount = usersLimits?.activeUserCount ?? 0;
|
||||
|
||||
if (!shouldShowUserLimitsAnnouncementBar({userIsAdmin: props.userIsAdmin, isLicensed, maxUsersLimit, activeUserCount})) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AnnouncementBar
|
||||
id='users_limits_announcement_bar'
|
||||
showCloseButton={false}
|
||||
message={
|
||||
<FormattedMessage
|
||||
id='users_limits_announcement_bar.copyText'
|
||||
defaultMessage='User limits exceeded. Contact administrator with: ERROR_USER_LIMITS_EXCEEDED'
|
||||
/>
|
||||
}
|
||||
type={AnnouncementBarTypes.CRITICAL}
|
||||
icon={<AlertOutlineIcon size={16}/>}
|
||||
showCTA={true}
|
||||
showLinkAsButton={true}
|
||||
ctaText={
|
||||
<FormattedMessage
|
||||
id='users_limits_announcement_bar.ctaText'
|
||||
defaultMessage='Learn More'
|
||||
/>
|
||||
}
|
||||
onButtonClick={handleCTAClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export type ShouldShowingUserLimitsAnnouncementBarProps = {
|
||||
userIsAdmin: boolean;
|
||||
isLicensed: boolean;
|
||||
maxUsersLimit: number;
|
||||
activeUserCount: number;
|
||||
};
|
||||
|
||||
export function shouldShowUserLimitsAnnouncementBar({userIsAdmin, isLicensed, maxUsersLimit, activeUserCount}: ShouldShowingUserLimitsAnnouncementBarProps) {
|
||||
if (!userIsAdmin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (maxUsersLimit === 0 || activeUserCount === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !isLicensed && activeUserCount >= maxUsersLimit;
|
||||
}
|
||||
|
||||
export default UsersLimitsAnnouncementBar;
|
||||
@@ -7,18 +7,35 @@ import {useSelector} from 'react-redux';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
export default function useCWSAvailabilityCheck() {
|
||||
const [canReachCWS, setCanReachCWS] = useState<boolean | undefined>(undefined);
|
||||
export enum CSWAvailabilityCheckTypes {
|
||||
Available = 'available',
|
||||
Unavailable = 'unavailable',
|
||||
Pending = 'pending',
|
||||
NotApplicable = 'notApplicable',
|
||||
}
|
||||
|
||||
export default function useCWSAvailabilityCheck(): CSWAvailabilityCheckTypes {
|
||||
const [cswAvailability, setCSWAvailability] = useState<CSWAvailabilityCheckTypes>(CSWAvailabilityCheckTypes.Pending);
|
||||
|
||||
const config = useSelector(getConfig);
|
||||
const isEnterpriseReady = config.BuildEnterpriseReady === 'true';
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEnterpriseReady) {
|
||||
return;
|
||||
async function cwsAvailabilityCheck() {
|
||||
try {
|
||||
await Client4.cwsAvailabilityCheck();
|
||||
setCSWAvailability(CSWAvailabilityCheckTypes.Available);
|
||||
} catch (error) {
|
||||
setCSWAvailability(CSWAvailabilityCheckTypes.Unavailable);
|
||||
}
|
||||
}
|
||||
|
||||
if (isEnterpriseReady) {
|
||||
cwsAvailabilityCheck();
|
||||
} else {
|
||||
setCSWAvailability(CSWAvailabilityCheckTypes.NotApplicable);
|
||||
}
|
||||
Client4.cwsAvailabilityCheck().then(() => {
|
||||
setCanReachCWS(true);
|
||||
}).catch(() => setCanReachCWS(false));
|
||||
}, [isEnterpriseReady]);
|
||||
|
||||
return canReachCWS;
|
||||
return cswAvailability;
|
||||
}
|
||||
|
||||
@@ -290,7 +290,7 @@ describe('components/signup/Signup', () => {
|
||||
});
|
||||
|
||||
it('should show newsletter check box opt-in for self-hosted non airgapped workspaces', async () => {
|
||||
jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => true);
|
||||
jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => useCWSAvailabilityCheckAll.CSWAvailabilityCheckTypes.Available);
|
||||
mockLicense = {IsLicensed: 'true', Cloud: 'false'};
|
||||
|
||||
const {container: signupContainer} = renderWithContext(
|
||||
@@ -305,7 +305,7 @@ describe('components/signup/Signup', () => {
|
||||
});
|
||||
|
||||
it('should NOT show newsletter check box opt-in for self-hosted AND airgapped workspaces', async () => {
|
||||
jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => false);
|
||||
jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => useCWSAvailabilityCheckAll.CSWAvailabilityCheckTypes.Unavailable);
|
||||
mockLicense = {IsLicensed: 'true', Cloud: 'false'};
|
||||
|
||||
const {container: signupContainer} = renderWithContext(
|
||||
@@ -317,7 +317,7 @@ describe('components/signup/Signup', () => {
|
||||
});
|
||||
|
||||
it('should show newsletter related opt-in or text for cloud', async () => {
|
||||
jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => true);
|
||||
jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => useCWSAvailabilityCheckAll.CSWAvailabilityCheckTypes.Available);
|
||||
mockLicense = {IsLicensed: 'true', Cloud: 'true'};
|
||||
|
||||
const {container: signupContainer} = renderWithContext(
|
||||
|
||||
@@ -30,7 +30,7 @@ import {getGlobalItem} from 'selectors/storage';
|
||||
|
||||
import AlertBanner from 'components/alert_banner';
|
||||
import type {ModeType, AlertBannerProps} from 'components/alert_banner';
|
||||
import useCWSAvailabilityCheck from 'components/common/hooks/useCWSAvailabilityCheck';
|
||||
import useCWSAvailabilityCheck, {CSWAvailabilityCheckTypes} from 'components/common/hooks/useCWSAvailabilityCheck';
|
||||
import LaptopAlertSVG from 'components/common/svg_images_components/laptop_alert_svg';
|
||||
import ManWithLaptopSVG from 'components/common/svg_images_components/man_with_laptop_svg';
|
||||
import DesktopAuthToken from 'components/desktop_auth_token';
|
||||
@@ -144,7 +144,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
const [isMobileView, setIsMobileView] = useState(false);
|
||||
const [subscribeToSecurityNewsletter, setSubscribeToSecurityNewsletter] = useState(false);
|
||||
|
||||
const canReachCWS = useCWSAvailabilityCheck();
|
||||
const cwsAvailability = useCWSAvailabilityCheck();
|
||||
|
||||
const enableExternalSignup = enableSignUpWithGitLab || enableSignUpWithOffice365 || enableSignUpWithGoogle || enableSignUpWithOpenId || enableLDAP || enableSAML;
|
||||
const hasError = Boolean(emailError || nameError || passwordError || serverError || alertBanner);
|
||||
@@ -619,7 +619,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
const handleReturnButtonOnClick = () => history.replace('/');
|
||||
|
||||
const getNewsletterCheck = () => {
|
||||
if (canReachCWS) {
|
||||
if (cwsAvailability === CSWAvailabilityCheckTypes.Available) {
|
||||
return (
|
||||
<CheckInput
|
||||
id='signup-body-card-form-check-newsletter'
|
||||
|
||||
@@ -18,7 +18,7 @@ import {closeModal, openModal} from 'actions/views/modals';
|
||||
import {isModalOpen} from 'selectors/views/modals';
|
||||
|
||||
import {makeAsyncComponent} from 'components/async_load';
|
||||
import useCWSAvailabilityCheck from 'components/common/hooks/useCWSAvailabilityCheck';
|
||||
import useCWSAvailabilityCheck, {CSWAvailabilityCheckTypes} from 'components/common/hooks/useCWSAvailabilityCheck';
|
||||
import useGetTotalUsersNoBots from 'components/common/hooks/useGetTotalUsersNoBots';
|
||||
import DropdownInput from 'components/dropdown_input';
|
||||
import ExternalLink from 'components/external_link';
|
||||
@@ -78,7 +78,7 @@ function StartTrialFormModal(props: Props): JSX.Element | null {
|
||||
const [country, setCountry] = useState('');
|
||||
const [businessEmailError, setBusinessEmailError] = useState<CustomMessageInputType | undefined>(undefined);
|
||||
const {formatMessage} = useIntl();
|
||||
const canReachCWS = useCWSAvailabilityCheck();
|
||||
const cwsAvailability = useCWSAvailabilityCheck();
|
||||
const show = useSelector((state: GlobalState) => isModalOpen(state, ModalIdentifiers.START_TRIAL_FORM_MODAL));
|
||||
const totalUsers = useGetTotalUsersNoBots(true) || 0;
|
||||
const [didOnce, setDidOnce] = useState(false);
|
||||
@@ -236,7 +236,7 @@ function StartTrialFormModal(props: Props): JSX.Element | null {
|
||||
status === TrialLoadStatus.Success
|
||||
);
|
||||
|
||||
if (typeof canReachCWS !== 'undefined' && !canReachCWS) {
|
||||
if (cwsAvailability === CSWAvailabilityCheckTypes.Unavailable) {
|
||||
return (
|
||||
<AirGappedModal
|
||||
onClose={handleOnClose}
|
||||
|
||||
@@ -5726,6 +5726,8 @@
|
||||
"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_USER_LIMITS_EXCEEDED",
|
||||
"users_limits_announcement_bar.ctaText": "Learn More",
|
||||
"userSettingsModal.pluginPreferences.header": "PLUGIN PREFERENCES",
|
||||
"version_bar.new": "A new version of Mattermost is available.",
|
||||
"version_bar.refresh": "Refresh the app now",
|
||||
|
||||
BIN
webapp/channels/src/images/air_gapped_contact_us_image.png
Normal file
BIN
webapp/channels/src/images/air_gapped_contact_us_image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
@@ -16,6 +16,7 @@ import GroupTypes from './groups';
|
||||
import HostedCustomerTypes from './hosted_customer';
|
||||
import IntegrationTypes from './integrations';
|
||||
import JobTypes from './jobs';
|
||||
import LimitsTypes from './limits';
|
||||
import PlaybookType from './playbooks';
|
||||
import PluginTypes from './plugins';
|
||||
import PostTypes from './posts';
|
||||
@@ -40,6 +41,7 @@ export {
|
||||
EmojiTypes,
|
||||
AdminTypes,
|
||||
JobTypes,
|
||||
LimitsTypes,
|
||||
SearchTypes,
|
||||
RoleTypes,
|
||||
SchemeTypes,
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import keyMirror from 'mattermost-redux/utils/key_mirror';
|
||||
|
||||
export default keyMirror({
|
||||
RECIEVED_USERS_LIMITS: null,
|
||||
});
|
||||
@@ -65,6 +65,7 @@ export default keyMirror({
|
||||
DISABLED_USER_ACCESS_TOKEN: null,
|
||||
ENABLED_USER_ACCESS_TOKEN: null,
|
||||
RECEIVED_USER_STATS: null,
|
||||
RECIEVED_USERS_LIMITS: null,
|
||||
RECEIVED_FILTERED_USER_STATS: null,
|
||||
PROFILE_NO_LONGER_VISIBLE: null,
|
||||
LOGIN: null,
|
||||
|
||||
@@ -27,6 +27,7 @@ import type {
|
||||
} from '@mattermost/types/teams';
|
||||
|
||||
import {AdminTypes} from 'mattermost-redux/action_types';
|
||||
import {getUsersLimits} from 'mattermost-redux/actions/limits';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import type {ActionFunc, DispatchFunc, GetStateFunc, NewActionFuncAsync} from 'mattermost-redux/types/actions';
|
||||
|
||||
@@ -373,10 +374,20 @@ export function uploadLicense(fileData: File): NewActionFuncAsync<License> {
|
||||
}) as any; // HARRISONTODO Type bindClientFunc
|
||||
}
|
||||
|
||||
export function removeLicense(): NewActionFuncAsync {
|
||||
return bindClientFunc({
|
||||
clientFunc: Client4.removeLicense,
|
||||
}) as any; // HARRISONTODO Type bindClientFunc
|
||||
export function removeLicense(): NewActionFuncAsync<boolean> {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
await Client4.removeLicense();
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error as ServerError, dispatch, getState);
|
||||
dispatch(logError(error as ServerError));
|
||||
return {error: error as ServerError};
|
||||
}
|
||||
|
||||
await dispatch(getUsersLimits());
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function getPrevTrialLicense(): ActionFunc {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import nock from 'nock';
|
||||
|
||||
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';
|
||||
|
||||
const defaultUserLimitsState = {
|
||||
activeUserCount: 0,
|
||||
maxUsersLimit: 0,
|
||||
};
|
||||
|
||||
let store = configureStore();
|
||||
|
||||
beforeAll(() => {
|
||||
TestHelper.initBasic(Client4);
|
||||
Client4.setEnableLogging(true);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
store = configureStore({
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {
|
||||
roles: 'system_admin',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
TestHelper.tearDown();
|
||||
Client4.setEnableLogging(false);
|
||||
});
|
||||
|
||||
test('should return default state for non admin users', async () => {
|
||||
store = configureStore({
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {
|
||||
roles: 'system_user',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const {data} = await store.dispatch(Actions.getUsersLimits());
|
||||
expect(data).toEqual(defaultUserLimitsState);
|
||||
});
|
||||
|
||||
test('should not return default state for non admin users', async () => {
|
||||
const {data} = await store.dispatch(Actions.getUsersLimits());
|
||||
expect(data).not.toEqual(defaultUserLimitsState);
|
||||
});
|
||||
|
||||
test('should return data if user is admin', async () => {
|
||||
const userLimits = {
|
||||
activeUserCount: 600,
|
||||
maxUsersLimit: 10000,
|
||||
};
|
||||
|
||||
nock(Client4.getBaseRoute()).
|
||||
get(URL_USERS_LIMITS).
|
||||
reply(200, userLimits);
|
||||
|
||||
const {data} = await store.dispatch(Actions.getUsersLimits());
|
||||
expect(data).toEqual(userLimits);
|
||||
});
|
||||
|
||||
test('should return error if the request fails', async () => {
|
||||
const errorMessage = 'test error message';
|
||||
nock(Client4.getBaseRoute()).
|
||||
get(URL_USERS_LIMITS).
|
||||
reply(400, {message: errorMessage});
|
||||
|
||||
const {error} = await store.dispatch(Actions.getUsersLimits());
|
||||
console.log(error);
|
||||
expect(error.message).toEqual(errorMessage);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ServerError} from '@mattermost/types/errors';
|
||||
import type {UsersLimits} from '@mattermost/types/limits';
|
||||
|
||||
import {LimitsTypes} from 'mattermost-redux/action_types';
|
||||
import {logError} from 'mattermost-redux/actions/errors';
|
||||
import {forceLogoutIfNecessary} from 'mattermost-redux/actions/helpers';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
|
||||
import type {NewActionFuncAsync} from 'mattermost-redux/types/actions';
|
||||
import {isAdmin} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
export function getUsersLimits(): NewActionFuncAsync<UsersLimits> {
|
||||
return async (dispatch, getState) => {
|
||||
const roles = getCurrentUserRoles(getState());
|
||||
const amIAdmin = isAdmin(roles);
|
||||
if (!amIAdmin) {
|
||||
return {
|
||||
data: {
|
||||
activeUserCount: 0,
|
||||
maxUsersLimit: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await Client4.getUsersLimits();
|
||||
} catch (err) {
|
||||
forceLogoutIfNecessary(err, dispatch, getState);
|
||||
dispatch(logError(err));
|
||||
return {error: err as ServerError};
|
||||
}
|
||||
|
||||
const data: UsersLimits = {
|
||||
activeUserCount: response?.data?.activeUserCount ?? 0,
|
||||
maxUsersLimit: response?.data?.maxUsersLimit ?? 0,
|
||||
};
|
||||
|
||||
dispatch({type: LimitsTypes.RECIEVED_USERS_LIMITS, data});
|
||||
|
||||
return {data};
|
||||
};
|
||||
}
|
||||
@@ -13,6 +13,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 {getMyPreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {loadRolesIfNeeded} from 'mattermost-redux/actions/roles';
|
||||
import {getMyTeams, getMyTeamMembers, getMyTeamUnreads} from 'mattermost-redux/actions/teams';
|
||||
@@ -74,6 +75,8 @@ export function loadMe(): ActionFunc {
|
||||
|
||||
const isCollapsedThreads = isCollapsedThreadsEnabled(getState());
|
||||
await dispatch(getMyTeamUnreads(isCollapsedThreads));
|
||||
|
||||
await dispatch(getUsersLimits());
|
||||
} catch (error) {
|
||||
dispatch(logError(error as ServerError));
|
||||
return {error: error as ServerError};
|
||||
|
||||
@@ -16,6 +16,7 @@ import groups from './groups';
|
||||
import hostedCustomer from './hosted_customer';
|
||||
import integrations from './integrations';
|
||||
import jobs from './jobs';
|
||||
import limits from './limits';
|
||||
import posts from './posts';
|
||||
import preferences from './preferences';
|
||||
import roles from './roles';
|
||||
@@ -30,6 +31,7 @@ import users from './users';
|
||||
export default combineReducers({
|
||||
general,
|
||||
users,
|
||||
limits,
|
||||
teams,
|
||||
channels,
|
||||
posts,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {combineReducers} from 'redux';
|
||||
|
||||
import {LimitsTypes} from 'mattermost-redux/action_types';
|
||||
import type {GenericAction} from 'mattermost-redux/types/actions';
|
||||
|
||||
function usersLimits(state = {}, action: GenericAction) {
|
||||
switch (action.type) {
|
||||
case LimitsTypes.RECIEVED_USERS_LIMITS: {
|
||||
const usersLimits = action.data;
|
||||
return {
|
||||
...state,
|
||||
...usersLimits,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
usersLimits,
|
||||
});
|
||||
@@ -0,0 +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 {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
export function getUsersLimits(state: GlobalState): UsersLimits {
|
||||
return state.entities.limits.usersLimits;
|
||||
}
|
||||
@@ -162,7 +162,7 @@ export const currentUserHasAnAdminRole: (state: GlobalState) => boolean = create
|
||||
},
|
||||
);
|
||||
|
||||
export const getCurrentUserRoles: (a: GlobalState) => UserProfile['roles'] = createSelector(
|
||||
export const getCurrentUserRoles: (_: GlobalState) => UserProfile['roles'] = createSelector(
|
||||
'getCurrentUserRoles',
|
||||
getMyCurrentChannelMembership,
|
||||
(state) => state.entities.teams.myMembers[state.entities.teams.currentTeamId],
|
||||
|
||||
@@ -35,6 +35,12 @@ const state: GlobalState = {
|
||||
myUserAccessTokens: {},
|
||||
lastActivity: {},
|
||||
},
|
||||
limits: {
|
||||
usersLimits: {
|
||||
activeUserCount: 0,
|
||||
maxUsersLimit: 0,
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: '',
|
||||
teams: {},
|
||||
|
||||
@@ -104,7 +104,6 @@
|
||||
height: 24px;
|
||||
box-sizing: border-box;
|
||||
padding: 4px 8px;
|
||||
padding-top: 2px;
|
||||
border: 1px solid #fff;
|
||||
margin-left: 8px;
|
||||
background-color: inherit !important;
|
||||
|
||||
@@ -148,6 +148,7 @@ import {
|
||||
} from '@mattermost/types/data_retention';
|
||||
import {CompleteOnboardingRequest} from '@mattermost/types/setup';
|
||||
import {UserThreadList, UserThread, UserThreadWithPost} from '@mattermost/types/threads';
|
||||
import {UsersLimits} from '@mattermost/types/limits';
|
||||
|
||||
import {cleanUrlForLogging} from './errors';
|
||||
import {buildQueryString} from './helpers';
|
||||
@@ -488,6 +489,10 @@ export default class Client4 {
|
||||
return `${this.getBaseRoute()}/limits`;
|
||||
}
|
||||
|
||||
getUsersLimitsRoute() {
|
||||
return `${this.getLimitsRoute()}/users`;
|
||||
}
|
||||
|
||||
getCSRFFromCookie() {
|
||||
if (typeof document !== 'undefined' && typeof document.cookie !== 'undefined') {
|
||||
const cookies = document.cookie.split(';');
|
||||
@@ -1196,6 +1201,18 @@ export default class Client4 {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Limits Routes
|
||||
|
||||
getUsersLimits = () => {
|
||||
return this.doFetchWithResponse<UsersLimits>(
|
||||
`${this.getUsersLimitsRoute()}`,
|
||||
{
|
||||
method: 'get',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Team Routes
|
||||
|
||||
createTeam = (team: Team) => {
|
||||
|
||||
11
webapp/platform/types/src/limits.ts
Normal file
11
webapp/platform/types/src/limits.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export type LimitsState = {
|
||||
usersLimits: UsersLimits;
|
||||
};
|
||||
|
||||
export type UsersLimits = {
|
||||
activeUserCount: number;
|
||||
maxUsersLimit: number;
|
||||
};
|
||||
@@ -29,11 +29,13 @@ import {ThreadsState} from './threads';
|
||||
import {Typing} from './typing';
|
||||
import {UsersState} from './users';
|
||||
import {AppsState} from './apps';
|
||||
import {LimitsState} from './limits';
|
||||
|
||||
export type GlobalState = {
|
||||
entities: {
|
||||
general: GeneralState;
|
||||
users: UsersState;
|
||||
limits: LimitsState;
|
||||
teams: TeamsState;
|
||||
channels: ChannelsState;
|
||||
posts: PostsState;
|
||||
|
||||
Reference in New Issue
Block a user