From 42f6568d0927d11d7830e8c51ea75ebf00f8a2c1 Mon Sep 17 00:00:00 2001 From: Conor Macpherson Date: Fri, 14 Apr 2023 16:50:53 -0400 Subject: [PATCH] update overage banner/message in invite modal to direct to the self hosted expansion modal if self hosted expansion is available. --- .../enterprise_edition_left_panel.tsx | 5 +- .../overage_users_banner/index.tsx | 16 ++++- .../overage_users_banner.test.tsx | 17 ++++- .../common/hooks/useCanSelfHostedExpand.ts | 1 - .../useControlSelfHostedExpansionModal.ts | 4 -- .../invitation_modal.test.tsx | 15 +++++ .../invitation_modal/invite_view.test.tsx | 64 +++++++++++++------ .../overage_users_banner_notice/index.tsx | 33 +++++++++- .../overage_users_banner_notice.test.tsx | 25 ++++++-- .../self_hosted_expansion_modal/index.tsx | 2 +- 10 files changed, 142 insertions(+), 40 deletions(-) diff --git a/webapp/channels/src/components/admin_console/license_settings/enterprise_edition/enterprise_edition_left_panel.tsx b/webapp/channels/src/components/admin_console/license_settings/enterprise_edition/enterprise_edition_left_panel.tsx index b2616d9453..db8fba9cf8 100644 --- a/webapp/channels/src/components/admin_console/license_settings/enterprise_edition/enterprise_edition_left_panel.tsx +++ b/webapp/channels/src/components/admin_console/license_settings/enterprise_edition/enterprise_edition_left_panel.tsx @@ -65,9 +65,8 @@ const EnterpriseEditionLeftPanel = ({ const actionQueryParam = query.get('action'); useEffect(() => { - if (actionQueryParam === 'show_expansion_modal' && canExpand && isSelfHostedExpansionEnabled) { + if (actionQueryParam === 'show_expansion_modal') { selfHostedExpansionModal.open(); - query.set('action', ''); } }, []); @@ -100,7 +99,7 @@ const EnterpriseEditionLeftPanel = ({ ); const handleClickAddSeats = () => { - if (!isSelfHostedExpansionEnabled && !canExpand) { + if (!isSelfHostedExpansionEnabled || !canExpand) { window.open(expandableLink(unsanitizedLicense.Id), '_blank'); } else { selfHostedExpansionModal.open(); diff --git a/webapp/channels/src/components/announcement_bar/overage_users_banner/index.tsx b/webapp/channels/src/components/announcement_bar/overage_users_banner/index.tsx index d22fe6389f..7012548f7a 100644 --- a/webapp/channels/src/components/announcement_bar/overage_users_banner/index.tsx +++ b/webapp/channels/src/components/announcement_bar/overage_users_banner/index.tsx @@ -16,9 +16,11 @@ import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences'; import {PreferenceType} from '@mattermost/types/preferences'; import {useExpandOverageUsersCheck} from 'components/common/hooks/useExpandOverageUsersCheck'; import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink'; -import {StatTypes, Preferences, AnnouncementBarTypes} from 'utils/constants'; +import {StatTypes, Preferences, AnnouncementBarTypes, ConsolePages} from 'utils/constants'; import './overage_users_banner.scss'; +import {getSiteURL} from 'utils/url'; +import useCanSelfHostedExpand from 'components/common/hooks/useCanSelfHostedExpand'; type AdminHasDismissedItArgs = { preferenceName: string; @@ -53,10 +55,13 @@ const OverageUsersBanner = () => { activeUsers, seatsPurchased, }); + const canSelfHostedExpand = useCanSelfHostedExpand(); + const siteURL = getSiteURL(); const prefixPreferences = isOver10PercerntPurchasedSeats ? 'error' : 'warn'; const prefixLicenseId = (license.Id || '').substring(0, 8); const preferenceName = `${prefixPreferences}_overage_seats_${prefixLicenseId}`; + const overageByUsers = activeUsers - seatsPurchased; const isOverageState = isBetween5PercerntAnd10PercentPurchasedSeats || isOver10PercerntPurchasedSeats; @@ -86,6 +91,12 @@ const OverageUsersBanner = () => { const handleUpdateSeatsSelfServeClick = (e: React.MouseEvent) => { e.preventDefault(); trackEventFn('Self Serve'); + + if (canSelfHostedExpand) { + window.open(`${siteURL}/${ConsolePages.LICENSE}?action=show_expansion_modal`); + return; + } + window.open(expandableLink(license.Id), '_blank'); }; @@ -101,7 +112,8 @@ const OverageUsersBanner = () => { return null; } - const message = ( + + let message = ( { getRequestState: 'IDLE', }, }, + hostedCustomer: { + products: { + productsLoaded: true, + products: { + prod_professional: TestHelper.getProductMock({ + id: 'prod_professional', + name: 'Professional', + sku: SelfHostedProducts.PROFESSIONAL, + price_per_seat: 7.5, + + }), + + }, + }, + }, }, }; diff --git a/webapp/channels/src/components/common/hooks/useCanSelfHostedExpand.ts b/webapp/channels/src/components/common/hooks/useCanSelfHostedExpand.ts index 93f2d2092e..1488713947 100644 --- a/webapp/channels/src/components/common/hooks/useCanSelfHostedExpand.ts +++ b/webapp/channels/src/components/common/hooks/useCanSelfHostedExpand.ts @@ -16,7 +16,6 @@ import {findSelfHostedProductBySku} from 'utils/hosted_customer'; import useGetSelfHostedProducts from './useGetSelfHostedProducts'; export default function useCanSelfHostedExpand() { - // NOTE: This is a basic implementation to get things up and running, more details to come later. const [expansionAvailable, setExpansionAvailable] = useState(false); const config = useSelector(getConfig); const isEnterpriseReady = config.BuildEnterpriseReady === 'true'; diff --git a/webapp/channels/src/components/common/hooks/useControlSelfHostedExpansionModal.ts b/webapp/channels/src/components/common/hooks/useControlSelfHostedExpansionModal.ts index a310c4538e..fee2a34351 100644 --- a/webapp/channels/src/components/common/hooks/useControlSelfHostedExpansionModal.ts +++ b/webapp/channels/src/components/common/hooks/useControlSelfHostedExpansionModal.ts @@ -25,7 +25,6 @@ interface HookOptions{ export default function useControlSelfHostedExpansionModal(options: HookOptions): ControlModal { const dispatch = useDispatch(); const currentUser = useSelector(getCurrentUser); - const canExpand = useCanSelfHostedExpand(); const controlModal = useControlModal({ modalId: ModalIdentifiers.SELF_HOSTED_EXPANSION, dialogType: SelfHostedExpansionModal, @@ -35,9 +34,6 @@ export default function useControlSelfHostedExpansionModal(options: HookOptions) return { ...controlModal, open: async () => { - if (!canExpand) { - return; - } const purchaseInProgress = localStorage.getItem(STORAGE_KEY_EXPANSION_IN_PROGRESS) === 'true'; diff --git a/webapp/channels/src/components/invitation_modal/invitation_modal.test.tsx b/webapp/channels/src/components/invitation_modal/invitation_modal.test.tsx index 0db5a75825..80c5226eec 100644 --- a/webapp/channels/src/components/invitation_modal/invitation_modal.test.tsx +++ b/webapp/channels/src/components/invitation_modal/invitation_modal.test.tsx @@ -17,6 +17,8 @@ import ResultView from './result_view'; import InviteView from './invite_view'; import NoPermissionsView from './no_permissions_view'; import InvitationModal, {Props, View, InvitationModal as BaseInvitationModal} from './invitation_modal'; +import {SelfHostedProducts} from 'utils/constants'; +import {TestHelper} from 'utils/test_helper'; const defaultProps: Props = deepFreeze({ actions: { @@ -87,6 +89,19 @@ describe('InvitationModal', () => { preferences: { myPreferences: {}, }, + hostedCustomer: { + products: { + productsLoaded: true, + products: { + prod_professional: TestHelper.getProductMock({ + id: 'prod_professional', + name: 'Professional', + sku: SelfHostedProducts.PROFESSIONAL, + price_per_seat: 7.5, + }), + }, + }, + }, }, }; diff --git a/webapp/channels/src/components/invitation_modal/invite_view.test.tsx b/webapp/channels/src/components/invitation_modal/invite_view.test.tsx index b90af49861..16e49e8f88 100644 --- a/webapp/channels/src/components/invitation_modal/invite_view.test.tsx +++ b/webapp/channels/src/components/invitation_modal/invite_view.test.tsx @@ -11,9 +11,12 @@ import {mountWithThemedIntl} from 'tests/helpers/themed-intl-test-helper'; import deepFreeze from 'mattermost-redux/utils/deep_freeze'; import {Team} from '@mattermost/types/teams'; import {generateId} from 'utils/utils'; +import {TestHelper as TH} from 'utils/test_helper'; import InviteAs, {InviteType} from './invite_as'; import InviteView, {Props} from './invite_view'; +import {SelfHostedProducts} from 'utils/constants'; +import {act} from 'react-dom/test-utils'; const defaultProps: Props = deepFreeze({ setInviteAs: jest.fn(), @@ -99,6 +102,19 @@ describe('InviteView', () => { preferences: { myPreferences: {}, }, + hostedCustomer: { + products: { + productsLoaded: true, + products: { + prod_professional: TH.getProductMock({ + id: 'prod_professional', + name: 'Professional', + sku: SelfHostedProducts.PROFESSIONAL, + price_per_seat: 7.5, + }), + }, + }, + }, }, }; @@ -108,40 +124,46 @@ describe('InviteView', () => { props = defaultProps; }); - it('shows InviteAs component when user can choose to invite guests or users', () => { - const wrapper = mountWithThemedIntl( - - - , - ); - expect(wrapper.find(InviteAs).length).toBe(1); + it('shows InviteAs component when user can choose to invite guests or users', async () => { + await act(async () => { + const wrapper = mountWithThemedIntl( + + + , + ); + expect(wrapper.find(InviteAs).length).toBe(1); + }); }); - it('hides InviteAs component when user can not choose members option', () => { + it('hides InviteAs component when user can not choose members option', async () => { props = { ...defaultProps, canAddUsers: false, }; - const wrapper = mountWithThemedIntl( - - - , - ); - expect(wrapper.find(InviteAs).length).toBe(0); + await act(async () => { + const wrapper = mountWithThemedIntl( + + + , + ); + expect(wrapper.find(InviteAs).length).toBe(0); + }); }); - it('hides InviteAs component when user can not choose guests option', () => { + it('hides InviteAs component when user can not choose guests option', async () => { props = { ...defaultProps, canInviteGuests: false, }; - const wrapper = mountWithThemedIntl( - - - , - ); - expect(wrapper.find(InviteAs).length).toBe(0); + await act(async () => { + const wrapper = mountWithThemedIntl( + + + , + ); + expect(wrapper.find(InviteAs).length).toBe(0); + }); }); }); diff --git a/webapp/channels/src/components/invitation_modal/overage_users_banner_notice/index.tsx b/webapp/channels/src/components/invitation_modal/overage_users_banner_notice/index.tsx index ce1cd6a1c3..f05638c2ec 100644 --- a/webapp/channels/src/components/invitation_modal/overage_users_banner_notice/index.tsx +++ b/webapp/channels/src/components/invitation_modal/overage_users_banner_notice/index.tsx @@ -4,6 +4,7 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; import {useDispatch, useSelector} from 'react-redux'; +import {useHistory} from 'react-router-dom' import {getCurrentUser, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users'; import {GlobalState} from 'types/store'; @@ -16,10 +17,12 @@ import {savePreferences} from 'mattermost-redux/actions/preferences'; import {makeGetCategory} from 'mattermost-redux/selectors/entities/preferences'; import {PreferenceType} from '@mattermost/types/preferences'; import {useExpandOverageUsersCheck} from 'components/common/hooks/useExpandOverageUsersCheck'; -import {LicenseLinks, StatTypes, Preferences} from 'utils/constants'; +import {LicenseLinks, StatTypes, Preferences, ConsolePages} from 'utils/constants'; import './overage_users_banner_notice.scss'; import ExternalLink from 'components/external_link'; +import useCanSelfHostedExpand from 'components/common/hooks/useCanSelfHostedExpand'; +import {getSiteURL} from 'utils/url'; type AdminHasDismissedArgs = { preferenceName: string; @@ -42,6 +45,9 @@ const OverageUsersBannerNotice = () => { const currentUser = useSelector((state: GlobalState) => getCurrentUser(state)); const overagePreferences = useSelector((state: GlobalState) => getPreferencesCategory(state, Preferences.OVERAGE_USERS_BANNER)); const activeUsers = ((stats || {})[StatTypes.TOTAL_USERS]) as number || 0; + const canSelfHostedExpand = useCanSelfHostedExpand(); + const siteURL = getSiteURL(); + const { isBetween5PercerntAnd10PercentPurchasedSeats, isOver10PercerntPurchasedSeats, @@ -83,7 +89,30 @@ const OverageUsersBannerNotice = () => { }; let message; - if (!isGovSku) { + + if (canSelfHostedExpand) { + message = ( + Purchase additional seats to remain compliant.'} + values={{ + a: (chunks: React.ReactNode) => { + return ( + + {chunks} + + ) + } + }} + /> + ) + } else if (!isGovSku) { message = ( { getRequestState: 'IDLE', }, }, + hostedCustomer: { + products: { + productsLoaded: true, + products: { + prod_professional: TestHelper.getProductMock({ + id: 'prod_professional', + name: 'Professional', + sku: SelfHostedProducts.PROFESSIONAL, + price_per_seat: 7.5, + }), + }, + }, + }, }, }; @@ -483,7 +496,7 @@ describe('components/invitation_modal/overage_users_banner_notice', () => { }); }); - it('gov sku sees overage notice but not a call to do true up', () => { + it('gov sku sees overage notice but not a call to do true up', async () => { const store: GlobalState = JSON.parse(JSON.stringify(initialState)); store.entities.admin = { @@ -502,8 +515,10 @@ describe('components/invitation_modal/overage_users_banner_notice', () => { }; store.entities.general.license.IsGovSku = 'true'; - renderComponent({ - store, + await act( async () => { + renderComponent({ + store, + }); }); screen.getByText(text10PercentageState); diff --git a/webapp/channels/src/components/self_hosted_purchases/self_hosted_expansion_modal/index.tsx b/webapp/channels/src/components/self_hosted_purchases/self_hosted_expansion_modal/index.tsx index 5fbd4d6ebc..88efd97956 100644 --- a/webapp/channels/src/components/self_hosted_purchases/self_hosted_expansion_modal/index.tsx +++ b/webapp/channels/src/components/self_hosted_purchases/self_hosted_expansion_modal/index.tsx @@ -509,7 +509,7 @@ export default function SelfHostedExpansionModal() { { setFormState({...formState, submitting: false, error: '', succeeded: false}); - closeModal(ModalIdentifiers.SELF_HOSTED_EXPANSION); + dispatch(closeModal(ModalIdentifiers.SELF_HOSTED_EXPANSION)); }} /> )}