From 2676caa52ffb513d119b9f97b14c57b345a3be1d Mon Sep 17 00:00:00 2001 From: M-ZubairAhmed Date: Thu, 11 Jan 2024 14:11:04 +0000 Subject: [PATCH] [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 Co-authored-by: harshil Sharma --- .../admin_console/license_settings/index.ts | 2 + .../license_settings.test.tsx | 1 + .../license_settings/license_settings.tsx | 13 ++- .../air_gapped_contact_sales_modal/index.tsx | 2 +- .../air_gapped_contact_sales_modal/style.scss | 11 +- .../analytics/true_up_review.test.tsx | 4 +- .../components/analytics/true_up_review.tsx | 5 +- .../announcement_bar.test.tsx.snap | 30 ------ .../announcement_bar_controller.tsx | 7 +- .../announcement_bar.tsx | 24 +++-- .../index.test.ts | 88 ++++++++++++++++ .../users_limits_announcement_bar/index.tsx | 83 +++++++++++++++ .../common/hooks/useCWSAvailabilityCheck.ts | 33 ++++-- .../src/components/signup/signup.test.tsx | 6 +- .../channels/src/components/signup/signup.tsx | 6 +- .../start_trial_form_modal/index.tsx | 6 +- webapp/channels/src/i18n/en.json | 2 + .../images/air_gapped_contact_us_image.png | Bin 0 -> 45193 bytes .../src/action_types/index.ts | 2 + .../src/action_types/limits.ts | 8 ++ .../src/action_types/users.ts | 1 + .../mattermost-redux/src/actions/admin.ts | 19 +++- .../src/actions/limits.test.ts | 98 ++++++++++++++++++ .../mattermost-redux/src/actions/limits.ts | 46 ++++++++ .../mattermost-redux/src/actions/users.ts | 3 + .../src/reducers/entities/index.ts | 2 + .../src/reducers/entities/limits.ts | 25 +++++ .../src/selectors/entities/limits.ts | 9 ++ .../src/selectors/entities/users.ts | 2 +- .../src/store/initial_state.ts | 6 ++ .../sass/components/_announcement-bar.scss | 1 - webapp/platform/client/src/client4.ts | 17 +++ webapp/platform/types/src/limits.ts | 11 ++ webapp/platform/types/src/store.ts | 2 + 34 files changed, 503 insertions(+), 72 deletions(-) create mode 100644 webapp/channels/src/components/announcement_bar/users_limits_announcement_bar/index.test.ts create mode 100644 webapp/channels/src/components/announcement_bar/users_limits_announcement_bar/index.tsx create mode 100644 webapp/channels/src/images/air_gapped_contact_us_image.png create mode 100644 webapp/channels/src/packages/mattermost-redux/src/action_types/limits.ts create mode 100644 webapp/channels/src/packages/mattermost-redux/src/actions/limits.test.ts create mode 100644 webapp/channels/src/packages/mattermost-redux/src/actions/limits.ts create mode 100644 webapp/channels/src/packages/mattermost-redux/src/reducers/entities/limits.ts create mode 100644 webapp/channels/src/packages/mattermost-redux/src/selectors/entities/limits.ts create mode 100644 webapp/platform/types/src/limits.ts diff --git a/webapp/channels/src/components/admin_console/license_settings/index.ts b/webapp/channels/src/components/admin_console/license_settings/index.ts index 41c9a80a2f..e47e8dfa02 100644 --- a/webapp/channels/src/components/admin_console/license_settings/index.ts +++ b/webapp/channels/src/components/admin_console/license_settings/index.ts @@ -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), }; } diff --git a/webapp/channels/src/components/admin_console/license_settings/license_settings.test.tsx b/webapp/channels/src/components/admin_console/license_settings/license_settings.test.tsx index 123e349d1e..3345102ea3 100644 --- a/webapp/channels/src/components/admin_console/license_settings/license_settings.test.tsx +++ b/webapp/channels/src/components/admin_console/license_settings/license_settings.test.tsx @@ -54,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(), }, }; diff --git a/webapp/channels/src/components/admin_console/license_settings/license_settings.tsx b/webapp/channels/src/components/admin_console/license_settings/license_settings.tsx index ecab1e29fd..03cd129b5a 100644 --- a/webapp/channels/src/components/admin_console/license_settings/license_settings.tsx +++ b/webapp/channels/src/components/admin_console/license_settings/license_settings.tsx @@ -7,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; - removeLicense: () => Promise; + removeLicense: () => Promise>; getPrevTrialLicense: () => void; upgradeToE0: () => Promise; 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; openModal:

(modalData: ModalData

) => void; + getUsersLimits: () => Promise>; getFilteredUsersStats: (filters: GetFilteredUsersStatsOpts) => Promise<{ data?: UsersStats; error?: ServerError; @@ -179,8 +181,13 @@ export default class LicenseSettings extends React.PureComponent { 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}); }; diff --git a/webapp/channels/src/components/air_gapped_contact_sales_modal/index.tsx b/webapp/channels/src/components/air_gapped_contact_sales_modal/index.tsx index 69a33aa08b..7594a189b9 100644 --- a/webapp/channels/src/components/air_gapped_contact_sales_modal/index.tsx +++ b/webapp/channels/src/components/air_gapped_contact_sales_modal/index.tsx @@ -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'; diff --git a/webapp/channels/src/components/air_gapped_contact_sales_modal/style.scss b/webapp/channels/src/components/air_gapped_contact_sales_modal/style.scss index 78663355dc..b0243124dd 100644 --- a/webapp/channels/src/components/air_gapped_contact_sales_modal/style.scss +++ b/webapp/channels/src/components/air_gapped_contact_sales_modal/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; + } + } } diff --git a/webapp/channels/src/components/analytics/true_up_review.test.tsx b/webapp/channels/src/components/analytics/true_up_review.test.tsx index 5e703b2ae4..60ef3b1d07 100644 --- a/webapp/channels/src/components/analytics/true_up_review.test.tsx +++ b/webapp/channels/src/components/analytics/true_up_review.test.tsx @@ -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(, 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(, store); expect(screen.queryByText('Share to Mattermost')).not.toBeInTheDocument(); diff --git a/webapp/channels/src/components/analytics/true_up_review.tsx b/webapp/channels/src/components/analytics/true_up_review.tsx index f950d0a747..416b238587 100644 --- a/webapp/channels/src/components/analytics/true_up_review.tsx +++ b/webapp/channels/src/components/analytics/true_up_review.tsx @@ -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); diff --git a/webapp/channels/src/components/announcement_bar/__snapshots__/announcement_bar.test.tsx.snap b/webapp/channels/src/components/announcement_bar/__snapshots__/announcement_bar.test.tsx.snap index ac61e705c1..9949f2093d 100644 --- a/webapp/channels/src/components/announcement_bar/__snapshots__/announcement_bar.test.tsx.snap +++ b/webapp/channels/src/components/announcement_bar/__snapshots__/announcement_bar.test.tsx.snap @@ -33,9 +33,6 @@ exports[`components/AnnouncementBar should match snapshot, bar not showing 1`] = id="text" /> - @@ -74,9 +71,6 @@ exports[`components/AnnouncementBar should match snapshot, bar showing 1`] = ` id="text" /> - @@ -115,9 +109,6 @@ exports[`components/AnnouncementBar should match snapshot, bar showing, no dismi id="text" /> - @@ -156,9 +147,6 @@ exports[`components/AnnouncementBar should match snapshot, dismissal 1`] = ` id="text" /> - @@ -197,9 +185,6 @@ exports[`components/AnnouncementBar should match snapshot, dismissal 2`] = ` id="text" /> - @@ -238,9 +223,6 @@ exports[`components/AnnouncementBar should match snapshot, dismissal 3`] = ` id="text" /> - @@ -279,9 +261,6 @@ exports[`components/AnnouncementBar should match snapshot, props change 1`] = ` id="text" /> - @@ -320,9 +299,6 @@ exports[`components/AnnouncementBar should match snapshot, props change 2`] = ` id="text" /> - @@ -361,9 +337,6 @@ exports[`components/AnnouncementBar should match snapshot, props change 3`] = ` id="text" /> - @@ -402,9 +375,6 @@ exports[`components/AnnouncementBar should match snapshot, props change 4`] = ` id="text" /> - diff --git a/webapp/channels/src/components/announcement_bar/announcement_bar_controller.tsx b/webapp/channels/src/components/announcement_bar/announcement_bar_controller.tsx index cf8995ca2f..2df53ce9fd 100644 --- a/webapp/channels/src/components/announcement_bar/announcement_bar_controller.tsx +++ b/webapp/channels/src/components/announcement_bar/announcement_bar_controller.tsx @@ -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; canViewSystemErrors: boolean; - isCloud: boolean; userIsAdmin: boolean; - subscription?: Subscription; latestError?: { error: any; }; @@ -103,6 +102,10 @@ class AnnouncementBarController extends React.PureComponent { <> {adminConfiguredAnnouncementBar} {errorBar} + {paymentAnnouncementBar} {cloudTrialAnnouncementBar} {cloudTrialEndAnnouncementBar} diff --git a/webapp/channels/src/components/announcement_bar/default_announcement_bar/announcement_bar.tsx b/webapp/channels/src/components/announcement_bar/default_announcement_bar/announcement_bar.tsx index 12aaef1a41..92cfdc5b5c 100644 --- a/webapp/channels/src/components/announcement_bar/default_announcement_bar/announcement_bar.tsx +++ b/webapp/channels/src/components/announcement_bar/default_announcement_bar/announcement_bar.tsx @@ -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 { {message} { - !this.props.showLinkAsButton && this.props.showCTA && + !this.props.showLinkAsButton && this.props.showCTA && this.props.modalButtonText && this.props.modalButtonDefaultText && {this.props.showModal && { } { - this.props.showLinkAsButton && this.props.showCTA && + this.props.showLinkAsButton && this.props.showCTA && this.props.modalButtonText && this.props.modalButtonDefaultText && } + { + this.props.showLinkAsButton && this.props.showCTA && this.props.ctaText && + + } {closeButton} diff --git a/webapp/channels/src/components/announcement_bar/users_limits_announcement_bar/index.test.ts b/webapp/channels/src/components/announcement_bar/users_limits_announcement_bar/index.test.ts new file mode 100644 index 0000000000..e2110860c5 --- /dev/null +++ b/webapp/channels/src/components/announcement_bar/users_limits_announcement_bar/index.test.ts @@ -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); + }); +}); diff --git a/webapp/channels/src/components/announcement_bar/users_limits_announcement_bar/index.tsx b/webapp/channels/src/components/announcement_bar/users_limits_announcement_bar/index.tsx new file mode 100644 index 0000000000..b29519ee58 --- /dev/null +++ b/webapp/channels/src/components/announcement_bar/users_limits_announcement_bar/index.tsx @@ -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 ( + + } + type={AnnouncementBarTypes.CRITICAL} + icon={} + showCTA={true} + showLinkAsButton={true} + ctaText={ + + } + 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; diff --git a/webapp/channels/src/components/common/hooks/useCWSAvailabilityCheck.ts b/webapp/channels/src/components/common/hooks/useCWSAvailabilityCheck.ts index 63e3b20e04..4dfc2e8b72 100644 --- a/webapp/channels/src/components/common/hooks/useCWSAvailabilityCheck.ts +++ b/webapp/channels/src/components/common/hooks/useCWSAvailabilityCheck.ts @@ -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(undefined); +export enum CSWAvailabilityCheckTypes { + Available = 'available', + Unavailable = 'unavailable', + Pending = 'pending', + NotApplicable = 'notApplicable', +} + +export default function useCWSAvailabilityCheck(): CSWAvailabilityCheckTypes { + const [cswAvailability, setCSWAvailability] = useState(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; } diff --git a/webapp/channels/src/components/signup/signup.test.tsx b/webapp/channels/src/components/signup/signup.test.tsx index 0e389dd93e..b73d144461 100644 --- a/webapp/channels/src/components/signup/signup.test.tsx +++ b/webapp/channels/src/components/signup/signup.test.tsx @@ -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( diff --git a/webapp/channels/src/components/signup/signup.tsx b/webapp/channels/src/components/signup/signup.tsx index 87d255b43b..02fca747a1 100644 --- a/webapp/channels/src/components/signup/signup.tsx +++ b/webapp/channels/src/components/signup/signup.tsx @@ -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 ( (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 ( GB&LEO=mMl4E1(hU8GD}=wmnBNhaTgX? za@Zw@CBr?wzq-3W@2~sk>R#2Wt(}@T)6?D4pPufXC^c0DqK7mO@7%dVr1)O;)15o_ z5VtQrF3=)B9`*qEA#i@L=XU1~A^Gil_s;K(r$FOfw@(T(cPfVHHi19)t)x|?@7$?M zAiOjKzNlnSl$F-}dUxxJAmhu49;1)cdd7^8iKPku?<9G!e0A(|0Zr~47pb_O_et>#f3&q3nOlw%A--R&auhguK!(@O# zY6W?I-kTeqzxly4&okkYrPh7bgLDdLIop#noBSOa899YVy@YdtHIh7RKl~V=P?Yzy zQtZ*WfZ_GFuXXdV6F^Guw>gk)r7>W7BThTfco zaqjV)6R+6}1C4+&BY)eyF@(#}w||>s7HZlyVkG6Zeg>>p8-GQB&F(T;O@Y_c>-|Dc zcNoyONbaYAXoLiUo-}^N+!vp$+!hJmej_08JS9aVx%bN#7Z+*OEb*7?!`b94xs$^2 zO+WH3W_Z{p0)m=8L`I^2{?1CZs|d)n7q|=kt-=1W$E*3`q?%WXM^;u~dcdVc&7evy zA7{WoQ0f`bvISlFw%{U>K+IMuDzLJsEjOnF=6V}n=c(UI=pkZby`TWIQ zbN;le{=ImX^~Qmd-*UQzt*w4!WXVHewH$JQMImB>kJYt`3_+6u@FvyBNL$mjjXSto zg3!fquWbsJT#ga!>o;#W?7}b(rQ@nHvLSKPc~?&>qaq>ycIK#heYR&>J)$Cu8voZc zvGTnP-m^EuiISC_pm~IlmGx&ziUj!Ufro3)lFMCHS;>U?(fbA97X$}KkW4*I*aIC} z22aZ4`M*24tn~CJ4}SSlUYWj}#L7ePe|??wdhr}D;ri_qdbB&BvcgnVR<^wDn~Lm& zTvh-&QT%*Eir??7d%z+)aHo=#O#LMC_R}BT?ce+-wWKr;-xTSxMoJw|kOTTO7j`#2 zvWRfH@2IBwB3Gi?7w$++UFTLY>g0bgq3Dh7Wq+1#VSrDjF7&yTjRpOQknoW0J>wWT z8QC#0))e?5BNHG47I3RMW{lHtaOwznGw>q4*79Jq(~MyiF0|LN08 zJp6muVgJ4VG<};qrgP=yq@U5Mm4V5g>{feOY*_&I0Z%U5Ejj=1H$(%R8@;2%Goa+- zQnBaZ`c*lZby-HXR{d6_6KSM>KEh7(GXfR~eo1&JTMp9+Fedy~lPG1hqeekKbk%Pq zb5SuSa{2NI72;G+`aNT@eR9jImRDR*AOIL#c7b zd8}vSQh55bs0=^T?OrJ|KNr!}kwN0?8_$2m@bn1?!Av5H0s&R3`-K_qN1TaSd|jkH z_h9q>HIQ&+u1I3SB^#3K6jHbRo+DK}gz4<2^&%odwntn8pDasxuu;+*F&F;p0r z;3ko8LqGE~4UVf3FV>v`cJt2nRoJY_)o)B&*=$2rmf9eTqRINAFEk3Jp0C$m?#fK? zR5A~k+*~W*&Z!QClp`3i{xWUdQ*lC)@Ii?lK zYopHZQeyoVQFG!?7kzN{cHQBDel7V_7xj>j{O;^A9Mx{mi5CU32$e-s!|4aNtj~T7)DQ(`OQ$e5N zVZH<<(6Gbi{|at*pbYW-b0YA{+evdQhVY;ObuP#4^9=nOV2jFB8QNnX8($NxVr0YH zUPrdl*~=3ZAs3}}UeM2NTQY~=QxMe1=GOR@bGgIfhfCqeo23QZx z<70v&!L98lRBt6)i&8r!yD}j&h#q2aY+!NG#%Nf;T&p9XEoKtEYIn}SN z{MdyKnZ5m-5Y^st(GhsuEh9UZ0DP#ZuLa|tzjmP(Yq@TmksS<{^V0lKUge8F@ba*P zHm%FCbMo6Zw72?t%L#RidJYrjk)4>kr8ED>B&H|7kp#XT!|6c`mRgs8gZUO+oAxHB zvFcknA^m7}TqXnIz21RrD~j|YJYG|B0qN3GI9aG2pdA{#0xOFxmrWz0_k*Iky$Q56 z>s7vcE5|t1(rv?H03Ng78pbkXkO7mi zhLbdGC-$PE`OgOwJ)zr-6!XsCp zyU@$qLj`Nu=k9cVQ>m5SE;Clp?fmQMdwK1v3v-Nmn{gRD)Z|H%A%3su^KuITnFl+r zeJ+u;*Rg0jhhQDDhQwd1h=5LLtG>dETCM4 z+3FvWQa85I^>on*Achej-2?sQf5de3C-9kavOgLTXs?-D5(gP%LoE(VW7hNFGyNG; zT$7~^??W=_jRMtYXOjuW8K({T>LBq~sc5>+9J9yo;bVK19k5T385BQ4 zmpMl0)_EsRJC%u4N-`+$F4T1VQhC%nYE+RL>00%OsNhR~C+PcrxtK-&Jf{U_zW=$1 zIEU<2B)~{|NRAJ^G=;AtnthN^z@(^1Kv*Kb2Y0cn(0?LCn+$$mO`%#~X8`MKj&x_o zuk|r%r@DX9>ZWemAY~a}kj=}cG=`_uU?&zQuasyfcC6h1i3^DrO9xd6xtGHHrmQjb?i9eTx!xVL)~9|M z5s`jU7J{aFi>hn+vQ$$1Q-SN*Pu`_6zfF0y=C!{k{kG1Pq5aL5|5#m*s+@kz*5^$g zRd?s+Jf*PFkUp`~oPJ)ozHUx_ez>tbDS7bVaF70q`aYo;jLg^I%))^=sX8#i5MjX3 zbCArL;J+;CeMxH~h#KzsA$6eIcyzp*cm)cCk{&+>)K}o`v1pyU8iWWSk(0E-so6$0 z{8Rkw$)p^$eH(fmliRzxmm5@UXi~W@40ezm=9NC(JGri~Y@JfEVQ5gW`o$PUS(X}V zBX&-5wY-@;0y?qWvC_TAQ$b<-YR~=P1NeYDH-S)M9f3A7R94t`CZbKf@-vfv$Y8g? zHYgIL4bA+M+f4`CZkAYhsH{bYM+O)jA5_}AHPeL=9Fycxhl`cpftSY9H#3Y$QRQM` zMMwY(%cX_>2MG1ZKt)4GKV)S9gZ~yZaT@`SZ;jG_Ya3xr^!I6}6-OOd#xPGSe?^e= zo4=Exl@04GnE8?QD zQf4>ehD}l8E55ULtXOP-qj>6`r}|{TQM$JNa;>J~*Ww5Hmwerj@S}I$83%8LM`;1F6LFHTdoW)KjJ7E$3ORhN^4e`h{AU6vl>L0%D%aGS@uM)d=MKEID>fHBz zkdAKPZgy7+V7utB`v=};4+_XkNWjR8OAl%H`SUpLFt|K(V*h#CSgCJxVMKIAJs|hy z&}s$g<*(1+yU>3yb#i5wDK0ZX^xs1L*EGgB0o=BPZuG(X0hqPuJORPjrq#YVP8@yb zl)B1NCqm-SL}U+R6)^ae9G@4nmnWJc6Q=MriseZ?a>4lB;G|O;`+ZjjBxecMBk!tR z6YB97jCJRUU=|ojrI@V%2Q%%YqrvkDP9b0iA!=ftw9!_>DUynyfKg2py}=px&EnQ_ zT<5VTtd$3|3{DWZJPUbSIu(@0TVJE5o4={_wD|+an!wFVvc16;Ip)fU69Hee#o3tC zO|qf))eW0d(`v`o@Zb1l?M>Ozr@vr2pQo3{ICD7!)oTo0t_Ei!jBMcXQxn#?f38Yz zEc^WE#syR6b;!gynKWmjJ@(&#bK|ll*gU~UKUEK2%;q{Gvz|14he8%y+Hi*}KbI|E zxxY&zn|uA35I@t8jqVp!+UF?b=tF^$sfL@kjLJ3xMC`Fl@9}<>NTugV%sC2Afxa5e za-R>p?nfpN+5A8b4#t$LA}=1cUyvBqJGadER>Zu8RRazo$9um2+?l_DbwLUn z=et^Aa&gCtu^=8eN$Pj5G!83)q$$Kw#h7IWzd6Sx8a;HN7p|izS*iE8ZV%nIAwohu zv3;~4GwWA`)gIh&inRJ3U?|O`K?G)DZI{QJASZLUfRCz$h|>s)sN1Nw5TkIYY)cO)Axp^d1U(31ZJ zGRbeheO-6qDTD3Cs!Pl0lA5oE>ggfdLqdkUZV-iEh84rfz%<{%S;BSg<<$-P+JkSR z1;HYtsQ}Ufu@-&)m~j=GF^7H4Y#VMDaOrQcJU>J4u0Gl9$x$41G3#xDRA3NqcomV| zuT(EXDk~Pc}h%`AG1He4kt^0GgVJvHz_3~}F0x-leuE5Ex*o5S{feEalfZv{n3(xiFt15#( zOJ2^!MpZ_w%2QnjgYLd4Dw_Kt)~}57+OhF}4r~`?+QyJ1RQxkM;%;a|Czgk7MH_D^k>{U<@~f8bH7Ck8&FtL5D^ zAPAFB$N(+S!g1gz7*`@~AgdLyPhXhpdsyG4c|_3v>9{ixKifOBTR^;?aB@X77feu& zACZk#-u@0Yx2CFf;_T2>j_j*o+_{Y8a8nyAg)RMk?Wx4yfhC{!_q>Q`btbxyvsD zeZ4E+s{-n?C^%Q2mR3fjnm+L;{o&f_EOK@qTQdNJQDH5rg?e9^URN+F+GOsP!E8e^}8ZVCYM%Estp^FgJUJDF=1kXr9MD8`Gg#01vuX_!uQQ#R`8P zY6}6Lp|bAY={%}Qfh`+|jidE8A3#07`n*YK-h&m@-8Z|KRW`nWkU?HkI@>7r-4oXT zWNO_rc}Ge@Z_AuGqbWmb_b0|%h5qGJF16Yp8_`pw_s4kW(71wZhR{Y42WX;ozlQ{h zOFLq)-=D$!=)mohQ?vVX*v+0BkIw4cow+GPM#WC^fv^}h&UOz3qXU%~<-3~9xlH)5 zUu~H6s;^=<&J5 zoT$Rp4Jfr-O$oevsCy-(rA5&|hNd~bajX31#PaMNc{LBJalgJ^uk)|=d*XyVzFaFU za4P@1+SA8;O+pOdDw_{uLw|?TZs?ue_`-&_#W#`#pAcmjH+;xN$VK8;%?N!$q>N(U zf7raIsH0@SOA|~zSM(%;>lvrMnfqOH?;NJz@xkSD{lj~M!*xYYgtCOM3CkI^ke!G6 zZSejuWUt-FdPEe5qa+BbhaPYupV+<e2}3cm5Fi6#MvCY#lm{^0f(h5NF4QYAZUugN~6F-XS3V0 zhRVI7Rzv$a_-gp^cEtjUWm{wkwy$$>II+GshDzk2OxwTy-B+x0}#8R0%`TA{@A0U&+NXY zrCw8@jS`jaGHaCb^}5^P?&c-EN(J@ z+esFuY4=$>0ZSe@LNvSTYGwxaF}y$Onf3jypu}J5b45i$q*&v(={rb(c&X852?P}THYivw0u)y! zZ{Dr^ma1d-lc+WD>5OxtC7?QgV8-Jd$6FY4FKx?nJ>crWP=Ni+{oeiR#JE&aT8_Lp zi;M%NGXLTJwb!pgwa|TUTvwZRnz(socdKa~S}M3WsokW^wJI2`Ck?6e(JcK9s%Or% zER0WlT0QPFgAGCFPNWFT=F(8@*J-yk_qOYFn?w@!+%Lm1X<~)z<`|=dC}Uo)4m)G%nTqvRLn;s8S|)gH1@aKiY*_5iNK^4<`Tm zZG4GisyR3prRFI+igu_P0Y@g=^2szie&8-W3dswOlM60-0qu;2Fk;`}Judb2C#n-U zy9z+Jdl_8ruWEe$ne!pRxu&V zx&z3wQ7hEGOEkWE(e2JrmGUTS{8$c>O116t+bW9AWPs>LPCZ2C zXZ!3cieNY3D@%4tLvNYp17gaBLy8~u%`ztuqR8|_S?q#GVwF5N9u2P#I%Hhak}1bV z$_Nf2U1F{-7X!iA3+tLqo1|sJF@8=q=g3eo>Qv3}(a($Fi~5C$3oF)PPjist*E>s7 z>zt+&M)Qszj59tzw)RyZ<(8?14R$P@6FxV9C{PLHXxEETIUMedGML{BOf=vany$E{ z=D+OP#&3ykgnQgbT#67)j3(|-+)&c=@#*gy@0%}~cEt6c+?=asa=3OL?>^481OmXx zYucb}(W#~?lkx@?&8m+(*G9~?_hpWkNoHAxC6Ba3uG&-1kB`1v$?ScHwaPKpYOU(0 zFlSxRC#?r6(0DG?$1Fk)Al`Bz`Y=mt~#X_umG_$CH(2fUChun$OsLa_BmevFELY57~%{ zXS$QO6k7>f59#D>I#!Si*yGU#@hec+em&!x3^}WAjC*&}74Q#;_mK9vT*S$A&T)z` zy>JkaE)ai+Ih+1_|D>Q`(x*hhAqI~FbnF@MPm|b)*ZQ7gbC6eUt!O5XGEtR5()u># zn(jtXde8UWCwg1zp6vlUZ{OJo(74uEDOuXU{dqmDIw_=59IUFO>P?zJ`ah2MT+Spd zv!38pVFv6oUpx+#!jq>U#=v|U8~4b*>Q7U>ycYLt%JDZmmAF|7ysAHF{&-#W-1S2!4B^P-{vP%uSEo7JZv}xGoM%6imeds%}tYI$uLU@dDTv?B=@s^?~>ng*dR{K;%+J)#;w0NMj&N|9djUpPi6`?_c#OH%*D6=B&_=6XfK7CXSG?=2nb zcwavhWzF&)=9yF!K`ZUu7}RV#du`BcBc&>Iv=hDHBN4X&s6$9eog_DwvVG zIMZyZ`Lm$9USy=ot>Bj$6&=eLRuYVRy#5ap)DAlGcSVhT$yy2T`E8uA|*@7Fm zg?7?1u0VjJ*OR+$ZlsGS)k7og_^nt(I$<;+B_ZJ(yOVHpgVR1YhYLJ?+1&Cu93;(S z#BfzqTsqIrH=B^FL?Y~>C#Koks^c{tBXA7zyp75&dZKHYA0hZhejCBUt8#F4*2-1| zH3nN$0K+fb~SuO=1v54qmi=MQs4bvoqlhryh-Df?@HCp*MS7#vC)&5OFXC14= z9LgH2K*;!k*?zsfGqr(khwq$0HSSfc?4%g9N-znJQB}yf_|MZ9Nw2GbmRYUO>^do?qjNmqB`U3?jE6yoHYnVFvnR(B29oLZZlj~O?mS79f(2mMGy`L6FA zk@`d@q{-1fM3MEpfvTVyV!R>M9Un(gPkx-A<}_%0?i9Ak&T6K0-K45M8px{m29I_F zg>6?1qb?6X-86peDQCQCA}t3pqeNx8SlJIH@U-25_chxY#wH#`4BAx$mLKVVv|L{$ z^Z_Yo`%}>i(AnY=6+YsaX&~3=S;<#(^_B4(n_~?BLyG-ksk&_Q8GMG1PfCPrL+V|2 zns^cz^f9QAKR%TecO9O^%PymH-%?`@;hS;T$_A2g(J4)DPx~9N&}T>L$s>!*nC<&S zv*F8>GJ!T)^9maRy;B{-IadhLa{XMA)sxH-j1KIq-2oSz3(C9iPc*M8yiDuxMjGL2 ze$hRQw0R*d!-+WG`6hxod3-A6OMj zXRHiztHPsgKQ?N?HVcH=Av~r_V@$qdjo`rn=okEX2lOTdVdh+13=KCMU>AwdEz3|0 z+tx5)@N+5N3}PNn>}x8aS#1iCy%2sHjH2OaUkJ3QU=h>ye)Mf z$RuQ!_{Y_jwR#mH?cq#GaGcp}@*HIyk0DZu&Vi`7ZsCnB&!{uKWA;vD`4|ue_Y_hJ z-PmVe$Doa8sD+Fm%O$JisC(mFA*tks*^tI{uTbW~@FFT-oEl*Vep)UzU z<={Ol{|4ytLUjAF?bX!emXF-i%5_@%)yNXWH_}*RyV?*Y`&~kzWN9(^C*Mo$nL?U( z^Sa$=^cnqgb25d>XuFN(L!5Snd6%s*E_bTX%k&f z2J&%@Yna91)6Q%)6?~nOFF|v8qTrN+%M~tv;*8B>|Jv~zg1~#A9gk5cUH$8tDauu? zongma1}wiFCcIQ)q(b#v_d?88H3y_Wb>y}|xxVItM{%Pm_{uzJwTWA~7LQ`_#ap5! zYG0`eyTXX~v*5z}jqU%)qy#B6HhNpw$^21=1X%Ej{M_9z_uKt4EY^tOv`z%&s_TfD4inX*A zu$Kt7{ynCha^Vu9TeSj}j4Nwv4&%%f;o3ckoQkHfrx7l%Fn-6!IW*`fR2;6g)%LA% zik1j|{Ko?)=3K2&!i$+lsM$fb0`~XWlh6CiGs2`ruZ{BgpXRCDCuDL7n2>**CH38P zB`wgU8cVOhECDgb%bAHsdr0UD{HUv>_+sz2)6)kgckp;?5K6GaNyRmjwOKbY?(T19 ztrTgXjj_&0g}Z;)?_xR z+TaLT-A;x_CxJUDT!ip%rcr%dmQt+N71l|S9P{`cKOE?EGa^UmwQ4@B+Y7NkwsYbJ z%jfZrqEOcne02uzrUdGE_Wu z0eYNV#Y8&(cB==rG14|_M83U35~#90gGD`)@fy2YqTCeDba)JSg9T`y~QJ zP?yf-!IgmxT@DJ1I)ct98ga5Wd4f>~ToV{6h;@^(jgI$(P!{MK;jYs;6T|Ca#>${E zxxJj(-Y%K}og#?Ze-z&G z{5g=J(5J4*N!s`2NsEp5>jZfYY9>ws8NN97i*z zl;E^%!VbqHFOqT^8-K%9v#Ig!`0deAQI@*j7PT{5nkM+R_v+IK6)tQjXzH_TPQtWF zh*lP-Ho~&@TQ#RuRt*en4n9FFARvkL(?(czx4D4@og2MI&Pn*{#Yx@zamYDPr$-bU z<+h!9)|99CV_l~4mDef)BdxoPNNjR0&|k6z)Rjm);*~C@_o77{m1H z=UDLJrN>6_{Bhn-cKYIx&c9DSsbmz;cYU_Xl)LM2w+$s0*m>eHPB+&*Ht zhCxtIdt$NXs8hS4&cETe`0@6bCWaQdd0<3~8YMdfbsO;GKxqJ_8D2g?jUP=Vx^ zW}EsR$a4YaWny}XW#LOJLinZR_vac!)npAOs-p%ow8h87yn}J zM-!xSQ)P=NzVGxncE3R#1SWSBP2rl&Hdg5HHnh)+q~A@`YcS3F7|1Leck7Fe?4Sks z^SmHhV!~53XDjTX=iOdA5ToDQ8{-L-EkFIt&2@frG|tX)Fh9MTxKXQK*iSYoVXXcz zY`6T#{?*E53Uu9qwKlHZq-4&-C@f8kQClmWs15p~ffM3w;l|Nqk z7vXScu_8mEDjyib44RCV&`Jp;!~W}4R2qzQsHWnoepEoqoyT{6sI&`KZ-11?cAHZ*1sIGUfXwd zpdpo*mi<*XQX7bM`Gd$O?X0_snsvZcH;@S+jq~uhSz8mdxYvOiNs+PesY{IOmt{UZ zo$JIVOfFQTZmc@{mp$h%Rg#2e?Yu&ouJJn20^A|}V1n}s1HRY!MDB(1ULg{*aoQju zCfiA8CA_R(IE0}`e&r7ee(G1#sd5*)kUr*eb>G6GMK|-090ioE+$+G2@ciKKnPp;g3?y>>am>2k+XznTpXO`oz>JNd8{ zWVI3na{G;69`9CJ4Du)e@MiaIdQa719FK9yLVlJ+CY`hqYfPK0-tiyeENuWopRswazf&j~j`MU+VrnmObTTe~Ua;!G5%8{@uZSDE z$Rd}i8oW?Ou`we@ycN6I-99H>MIDzu%3z1oUa~R3=U(01)jl(Cm1Wr3dS7bR`nP$q z7wz%oQBCD=KV#lKfcmI&ZFV4uAB*-yBhIrdw;4uth)NJ2@}xc+>neo#bOWWZ z(Y5`f>n4d;D9Dg@sf)~q_xvfMwy3Y0d=7Rf>pHDcc>GXt;$PKM-IzPH74}UMDpdE@ z^1N{Gi9Pst-#&C&w>;W&+-&RpnC5QXt+=)-3rKWdiJD?;{p4g0uy=xJ((dajZr7H_XIYcy znpNFHsF!@{G#O8%jTZMWT3&1imyd!14-<*Tgr#j-E6i;~5>b$=;70{?XRAoI0xG(I z<^V04ah`K|o5-#r(fd11$Swl5Aa32GH>X0N>l0Fmu`CnFLe*Chv3Q;WsI-spixDz^ z0&+yRhf?ISLfT>NL3s>q)$}8Mm4OpK9y9LcndxbYct#0!psGueIDtT2z;NZge=%(d zyfR7MKU04}mjDPhUw2tveY*G~u>zDXzlYLE7(YK{(iK8b#VOJ?5QT0#Mqm@OGU>+d z$L|av=5YeIe{^-dc61$tdsGrYL>8o->X=N;eIBkEqvx-QEc;hSWwAW z&nNM$5i=-S$ow~=cki&~Jerlr{-F}??7I?QM|8@OB~<62ec#}g(u%pTD%)nyBNSKb zneIq&3_lLhq&1O?N6PP|-Sp+3q9PoJ0h<$#H5;ioJqKdjuPlxB&VX|){(qC|K#G0n z^_Rpcs1GS5Olrk~H=d$oU+Q8$-+kU5VE|fwDq8D01c>tEH z34W-yoPPfw?{W-od8h6__a+wvxNuPT*5=Ry`>zYTaJU)}hs4C%gdRy@ZqqsL?_(U}C(o;2U(lq*P)6ehon8D8D72t0 z8qF&=zevR#c~DUa;?8g+b7b(joj3e9y97_&*Gjum_&3U={hT^28;A{Hwt#q9YR7Q- zc4peu#S+aY_1^1`E&i&GHzCyCM^UljK1EO*L8dR}C%wic%~aFhw#8tych&smDfHXp z%qm@CV;sgg1=~ljZP<&sm`1&Fu}`4s=K3Y|-%ay={UkaFa}qJ*>`j~(@(dh!HQ;gC znhGHYtU{9`P|oC8R;98+VKl??*o*=y=5ZdzK(SFb@UJVkjTR+oh=pCH{aamQ+>N7r zR2y@NZuzCZ&*v^RkZN38lkHpAVsO&*lHi}Tn)yuhcFvSrJ8%3Pv%{`O^I9mSy`;H; zVt9=C<3S6LjBJ8caB|!OP;)+&a7ltT%@zT6;!q8y%0>U((d(=tR@|eks*rZgB*AO+ z(Uxs42X^2u$wT#*nt#-PIZhlqsrJ^vpVi6zi*LigQp0Fbf@LxMH!C}5?V8^!C=uW0 z`nPa-KLv*7Xm28W`xZSVVunsPH(Q*a!*-ZV|2TUDYe#VGYy{PiTp z+VQpq_4Y;yRqm%UQ;m<9FhJ=nIC?5J=b!d=a(JFlqpClMZny37fV`_%WsWQ8jX#EC z@b%>#FB%DQw4LosaecZ7gM{n*SQPKCd_~x0RZ?Igse~x3X`B4F28y^C7d4YA9pu-I z@>cKH)auO2{i$aN4HAb5bISgBT3Kv$taMB>^@idLOiT()>swV!6N)(=__u>6{C)!n z2G4(c?dP3&KDt4>EBHN6;vdl%c!_=pG==?XsOZufC4cfmm3iO6#0ybc{*tzw#q6V? zrM&{V!WiKz{t+d>?AG`IX4h_du*yK;NUMXx(@QZfR#E8b_ww=5#_Pd%FfpV2(g%yh z@`S`3!>YnuvKP{R4tHJL{z78+eZmHP1{{7a(AA}GL^jW6eEqAxy}8CeNk}KSkbo(( zhe0!pn4%sEgO%A!{`A3w=MJ}dx)_S(o}JMa;^WBL8;QZR zskMjv13uG)ODm=HO(Nh)R!K9e=b}W~jRM~zphCG6`L80=TAlw_|Vni4!W+lf5KBA?=OXY%fpWRxwpr(PLl-WF$@dK2P0- zEndgPqcK=A{~v%MWtgk%dHtqGYs-zw$29r9iHkdw+MY6RTs?TUBn@6tpYrBLiY@Fd zy^IA7R0hEMHE~8Uks(`7MF5Rn#nzI`L%dM;&~$)jEt0Y>$qB>HJJ%j3WIZpr-C)lY zx_~-roG~uks^~bEkoBRHE}7NlpndOYLNE#vR7v}jH!jPT$9KqZHvx@S4K4dv!hine%jurd$3AKb zPdr0fX^9oiD5&Uh9s5EzRoFBWB2arOUyVl|q>y!Y!`C}|tvcwL{gGHQt&Tc8F-M`S zmq8-a*7Po+Tr9P>f`~N+;$^IP=ROv=o?L**p9ij389@};OArk$x89m|3X*zH54Q7) zv7BCXH18RHx-CB?i)g@+*i!`2m&;_FoL277k1{x7G(7GW5N!s0R&vLo<$2!t&(dwi zQZ>ErygrKMviwihXg_cc`=V*W6Sq-n`2-K(j@Og(N-vXX2?v&uio_UG{|m-}4Fdm> zagD6m%VpLvViL&=%VS8#mNJG~!(WxEIbzUM`#$=iv^^q2zGEH}ifj9l`Z&TCj7d+7 z_aueMkj9_zq2NmOq~)zcerN)iF%0*WschIe!W*<^;bW10Ern-NZI|hCkKDMS^G)%@1b+Y7 z>h9X6v#*Ujm42FqQyvU`PCL0lobbzSP4LjnO31g;<~g7kw{yq;QOP%M-8p_J_$OMT zW-}x?#(@eQ%URK)*X$23d=BH~oV$1TsD$&e3PIC`hvcPSSB5LZZe`5%^}<%mvx+vK zn`W%Tbx;VND)Htc<@@`~DSz8D1c5Box%_L@+0b?MTbtn)=9$DK_*}9%Eqj#1>yHFH z2u8$iR>8+iGZ+&4^=u5Mh}SP72bSJt{^oD+iD3;6c`sMs)uJTZ|rO~ zM?r$-sQ%jK=norJy?;#_>H^o!39+X2gsR8w{$5~k6;Nq+Em3Je+1Oht=*y(Q19 z*dq_JXC(^|_d&x48Vb28RzUR57T8{;#~Q~p^VpK-XM-(IK|b>zn<`pLOmA1)hgHga8jZxn&o)nDZNA0bJ3Gp^Yi5Ll#3+h5b0k7VIrkp3 z-C_68kGwxg$u}d3rf7K`w4H;6nN^L;Rbzc;eB&N^?6>yDZ@P6jSS^VP(L&{O<(`YY zc`5rtSS}QzFQo;#8$%~`ndipYd~^PR?_daiqSJZ{&iNwh>rI=f$b2Lduhd?7K{$7e zT+(d;umfk?=8WAZnkMLT!;U|Wcqo+-w%|cCC7gZz`;TjuuasN6?#UOAT`!u}_xS18 ztCJ|pA;Voq`>>}|F`-3|IS~_e=jFiVkyiPi*X=^`7LL6?>Wv$}a3HTl=Iwx6f`A?V z<5>O~`5>V;Twe}C4S36B_7;w|ouX%XLB>|G3pM@|7)(spJ%v%=)>VIz*6_=LPVm)~ zF2Z`ge9wFhv$Q4`Pk`)Q#Ks2a&$mAm^e7J~HBOUxeu29F7khawC4s8{&uwmEya@ZK zRNrPTQ5$^($zyK_G2#3}!Rnl1A@*jfMMiQfkGP|X+=!8|kJFsk(kKm1seUyzV)%HZ z?$b7LSpDJ#Tv8I=zuKXPcwKJqGC$dBFXjg6B$Z2Y+)(ko6aP6<43sl=XiIv>gX^`FD@2=+l6 z!$^+TrW!ivb@cHc07DMv-GSSkr&BxW?#{z7LQx-(PA0S!o2We!U^P&O<*Vj~h$#Ot zLuj{UMk7rm8Zm5d7Tw8a<9F76nmiH0_uauU!8t)-{be+Xa0ET*wLuporHIV1MuZf0 zamS7#Na}J~#DRKK?!$*!R_M!GxQM9Sy_cM3=60^HWZj!4orv#BJZf*owXb(hf7-Ub zme%J~zZI~(JhJZ}KgVXIqmE|jeok~1-(p>_Dukj%4v_h4(gPD^27ru*xu8I);im(j zya+>(4HU3krJL811G7hT#?K)*g5`owE?0w(a*ZW;HzdtkYC`5WY{@^{Q;0Cq8Wiw| zJ=~Oj8cG)FM$sr?_0h)#4o;TUavT_&n%`zliMRJ9@ToZ#RBDja4>5FcBdm5H;Y4WC z7e=b-Y_6jP+K z$8k)(5 zX2`gRgg@t90?k%34MMLRu^A%H{xHDf2UIT($Q7GtgAy_mdLse?Yc3pcq_Ai7B3eubggefYEgYop$r zSrvL(b#o<2D>sf?Ki1|VBF}+wde0HZpsLHfw{ZK!C0rxGR~Wz5`mI}GwZ|@JSkY~i zk0#Gw7bcJC2I?GBA#ISy!C1V&z?92tRIFl)8IR|>S;>FNZLEb?x>TdDs%rM?1~kZ= zqi1F;r<~Sf#<9IKX3~12ljYx~t8g`FXz-_VCP}?SeVRfdR)ALwGOav=>;3Axy9W4; zaT7S)RY=T^So6d=7=QA1H{6}X++K|h2<|&>ftc&BrB3Or%CyP{dP?2;MDBJy@*fWw z{T9pUq(oR@rq+=ZTSG;Lq%Y0hld$^4xX<`@iqE`84O8nd|IWYp=EA#r|F5 z;0=8t9JXs1Y0dUN8f&D`DeH-K@u5r>5)ep#1EofZ+QP=$Arx< zcHdBflw=g8VTav7`}?UwZh5o*>)DoTRRaAZ??r9@d;;KStpl!WB(e@Llln{-!jF>P z(9Qv%cc=q|*^TyjJ;!8nz4xNvG_NB+-qeG+jKzKJ=34*t->8e2%V`W=9^XA|HXGT( zS8P}xlTi$~_>$zPZ-J?kSPu_njYRP5uN<0t`!i=!JszkyH>BqrcLe@>CnxIDip{M1 zmmtekJ=FiZ0WIk&k9HIY&_$|ZS1#Wr>m3w2N6B?ZF{D+lBen?QxXiP&9!^irbn)RX z&~3`nsU7&XmXftvjc|Dgo-ZSYp}fQ#m#SGxUh~9K`OQwO4##K;#iS+G7%0Y3^BPVv z$h@;)b#Ae4*4L9~;&{h&p-Y*i1*Ea;+X`4%;h7cz7s%dS@oF{&xP?XxpAL8Lv$WDA zR{K+xn*n8dz^%f}lnWwkzhhDY0cvF$LeqFo$GoZ{w(!3u1o2h&m-jyPCA@B{y_(Ib z7osoSKXk>9HKf!nsx3gjkG1>hbMwo)BDH_bk6a}{fYodt8fmM7zKUL9^pAqBLL2W5@bjdw~j zzZ@zWWvl<4lVpvYCRH+pDmB zcEQ8`F{74mP709(+<|H#&SXJ6^t5@^J}i8~7F zmM%PkjY88M6U~4qc5hs{#FmUB6Slu~;uL6wXf11XF<308Ea#`XJxsDAH~(^3Y1Ro3 zpVLtun|7PVRcr2=+N)0Mkf~8ysVFk?4=Hs`#hwB%BQg%VaL+p5@Mgp!I=_%}X8`9m zE9Vp4>r9xvhsNd65_1`BleONw=QreU>0t`(xaU1FM->?qW2`GYct|U3ngADbeAQbj zHS;ipY$)*fa!Wh~3O0{{{>K z;iHyQt(!~T+JVfw<0u6UjYv$fjI>WbVLbTpV`IN>5KR|2wo~|DqlD85Evb8^(x9P> zRr~^_+sAt4YanPHImh-!hktYzbhhFXZu26%htH@wukQk zNfH=Wc~aY9axwj*C!hZKn{@e1GxNwd4o9Vut)T0OBo|X)$TK2_z1hI zg)qpVr8cKgdQM;Wbom>@FXnk=s$zb=HJhh>FL8kOA;7V0^eP2+4(~QKJ&tb$91OTz z9EZ42KZJ&!pB#$`f8ip*pqyV!4wDEEBPRK3(TF-H1h(q~9hO)-)rB9l`gtPc|HSYU z)WCG7)0B9G(WH(_4B&8h=tgHBH4$FQS#DBlb#lK-b85zDM9ECoy%jer>XI|lyjVUk zlQrq=JLo~3e(+X&> zAeKcUiKL%F8v|ue6Bw0BXfqE$M^yf6QCr5>xrjd8c+XMtaEbR13sq{qhI)1gnS1K1 zz68v&;chr+M##d?sDz;Gsx`{yE=v}cePQrYB-TFyp-SF(=W?^!*6 zu;iB!CIi|sTfU_bE=%XOns6>}`W)DuESWLm{>-6-*-@w)PIkGFc?WJ?zn<`qy(Y0&zu7I?4Rv^(bXlGh{sa|I=%8 zo?SICA3&x3yA#VxRX7bWXR(MQo2PGN)j`e;*14MlB%LL6yP9Y<4f&&b>h!FwB~6%B z#5yx>MtmYUrw#ZMLYBHylUQTZIDV@L^sNCc8ER)4X;(IwJ`Uo-9aq zHa6gJc0y{icO9#LfF81bHo+$Fao2w~X-1+EopD{8RY-%9-r0Lr`q7rha2=nOLDd*+z6(+aAYa*l)Nq487>)nxJ&pEM%t_E}2 zK10_Z#9rZ<9nEE1q?yS845WSHAFw%JJE}nof+VEmwLaZ~xVH zTZJ-TO66BFy9#t(J(G|#M#q}$yhG|zwNfS*>+=Ic`IPM$;gxnH2|E5ragD>buZ8iE zu9B^9UQ%f>vD)Xvk(}>wl`LNS^K+TwNx-d%W5C#|6{ulHb?-0bX7tD{vg**Az}Bn> zPH$WHyE_tR!b()zedI#-J4-`-3MfsWl;LXdvxE>JTqcC*@)C8)&n6-OLMVDQ_w$Xd z`}taXJN``Tgi4vq>oF~$Uy`kW$?xN+Rs`$2e#p!NpG+L@hWY;7I(4cHEHZG3g-3ko zjnfr;1FtEZnd^@FfMQ$iaBcm7Oa#3`BTYpz%nN>p3|Bx64ok!qqWAKUE80uo^a-Zx zN}Qv%X}ADUX<-4s>PNLQi)M{3dfWANLFMLf{dJ=1UvJVFONg0N%51Wy#0c@#0vuAs zpbS16L&rYHv$`BOTg?al#2D|9b@O*jhC{4G9jFpeyY3`6wYLq7Xer2J(66nXS(}=m zY=F0qOVw4+EoUF%0cxz`2(YNAfb8**eoDS zIchAR=Ch-zWrlAWVv=-Gwo!jDqKQYEu^u_fVPqUtl9-zzqZ)wAvNeA#v1K?wiLR86 zsr5w6RyiAf?ds;;X@tj!<#-tUsUOi6esrUndwz}h)XEAHu}h^)ukSeX4GTW*I#Ixm zUG*-E_|ZypPxn!VW)Ioh=($R@amsb^eAW$On}(-w%GqydYRz3tn1#7hHQCK@ks`-a^(wr*tlV+EfzdHAakP zx(r9B7RlK~M^tJhEGbelHfiIM$0C0I9dx3n{TIO00jqI$a?+BvQ16dxM%|FFeayn< zLQX%NbnHA^ewSq`lar1^ELVT+yo(A*`0URjB6{^J)-RCCyeI_c`0uZCURgqTkQ2Th zZf!e~>dl@rCF);GBK{Y>UcrM@4e(@u}Pv%N(lQD?@td!Cz*j-!?9GD zL43#$gmY=2T-JD*WQ@fObL9MIsG^(Q$T;ouZ9n;6p+i2?kg5XR>RJdTi9mXf)G}UQyO?+`k8M1#iG9E!k}ie@VQaz$#g_HmtWE^ zz+?@Egtd@Qp+B~symV{13of-LJ2F~bc}3%sje#k@GR5e4F0Xic@-g)uXQ%)`p}4t~ zaYDOTg(f4z)uN2U+)uh(IwV58w3wFA=1d#|yG+(sB%xN24IgM1vJK>d-7{bGj@N+^dh+F6 z_o14HBGa@gf&8n>Q-v@Pj#SU%P{1~71ACalJM?YGb$yY+cAt>0P$p|(z=*>=835Px zfJ*>_47mgJR~SkhELgfMoC)?PkNo8wzyH)e3`#jF>0Y5%k6RTrb4mW=wNq9i|4hYS z({I?JzX{E-PthKg2p(^Hhg;%fBpQ@n;^p;ju=8bf1J8Hv+~9pb9O1andJF@w%EK2O zL7~t^8sW>3bnGlO;El;mPOs~`7ula68_cI6=zglezQ8DENEr51wtl$YZs0VY30yK~ z7M;|^_vIRFR~y@D+FK=VUgksp>t}q2Dct0-`=GM?yE_ffd{Y+-SB{Iks(j4HXH*w4 z*zI>bb{OBU=ym@V7=XY~Vme=XteU*QE;R#re#=$xmI(sAp4z`TN%l+XxzMqb#~-mH zI}vD%9CCm*OSDp`(Pf{V-)&WfSHY9y^gE)C<3qt5RDD>46`P&#p8^Dli38U^9}-5k z2fAx6m_pO!6drN{4jR10tn&!#ES1FX|hmTtagc2n}uyj$N$%2=yU3)tLoza|x_4~vkVW=g-}g7i3ujYoW! zMWwS)_-yYK5pXaiwfz$IWs9Jbqb2YksdQ$mwu>F-vY5uS^YdL98vNwD*Okb3#)Dgj z$(vM1@Q}#i!z473^`*OSO|5U6j4hFZ+fPQ?i zLwc;nWgRwGze|s?^`4sGH={-+rFU^gCkbjPI2r3(4k~eiWvzUV0wa%dN4?~`>Oee* ztab~e=l#rj{4nIcQRaP|ZNKyAp0m%ZU{G{27tem|Rp(3`oUCYtHVqwH#K|!}sdQ$H z^z|)oTgrvb4#&%pRZtOK`QJ>V4U^LYLR`M$v}~=gw0{~hoi!o9xI1d_tZPb-0}@uO z3uo4~U4U4Ttmr^WQS%b0h2JgB^`P4XzD*>=gS_vM7t1GRHXWe7v|4$8F65<>wO zYA}It0{bxO)8f(|M-4Fne^x5%t?pBW-gS;#!oRD-#s>FJyv4KyGI%C$F{yg}Z$vQd zCe^~no2OgNA17{g<|=kzUU?Kdux}NZ4XswF_JtOO*a{*I+6EZnGU&(=nb);k ztf!mR<-&o+5w1acV<3p5gu^(VFWFv?^&vucM-V`;o*M3#Vkv(UU2+zF5b4Xs8&gLOe4>wBzQ1DUDW#sdv1~CkN zEL}~4l24-f?PuP!#`rU4e;6D8xAb&I4gdq`alThx3Jqy^YAMM4Lq?m$`%SL1Zn_)h z^CAm|itwYR9V=D7*W2Njw%LDZtr#ci)^FvU<~kGF#$Pv}ukzLWU2w5<756$Id<@aK zy#)rP^8h~@oD!vX`O3k@ncwBB!Rz%sbV4V}(ZDoc^PFdVB`QyREMVs;mDm%ePhk`*y_B9Y6T2VsbA57hK*XD@Z92^bD zK49ME|2pfiGG)KsdXJxos8kN(3kFh@Iesj@DVr-Yg@6r!2c@i$Bm}w=!-?A&36p>5dv@8wI!m)mOxxBjR;d!9o|0;783b zM1C*#qBE?AJCUR)M*1KP{|+~L{hD?Kh1trQPY9^?UIA`X;;Q;8V8+DX#u|T22c*nU z`d%d5M<(ODtfHSt71|_+FAF2QPP}diThHij9zCCwTkLym`fu>qzVh!jS=dZ&tVrPb zS)uHBp<5oba)mW~9GX*_+8&$9K<7|<#H~UoQ)3>qt2}?zTk_O%Xurtv^j+wQ=~!Exi^S+) zw6Pf9Lb!JC4;g0nK&98JVRy#Tc|=NqDO-|^>zv<-F;6a=5i2?1eK-|n#M*FQbDFDd z6SJre0CkJc)yAvWTEYR}#SZ=);T+AD^-|grr2W1?R6H@>xOl^Cb)hsco?uJ6AN~I< zU%0cRL|b@{`AFz-`<`H@?St{vtMQ%4Kt2*t8uvR5^xs<5mh8_)Dh=fY0p)nmEqDRN zg83V%%pb(*XOO6L_nbQ?HuF=$p5iQ%zvb`lUc)Vpw|Y zZ?|Ev!vtIJl<=d_rG~QN^N-Q;_uC@Hf`eY{&q%A`>*f#UM&rHQ$B)PMJIPlvMK2pB zsQZWDd2=pCz^9h&%~9b+S0{lsKWy%g?qIjrrW-+F25q*ML6m(Zsy!~?Fimo2U1A07 zJ^r}FW|$drNKYQn6k-%93`nrW1xp+=soe|j1~RGxt8od$TGo-D)D-hA z0erfSmx~F~NdE=(vGVI25ngHMnCDJyX+xhJFC?qVZCSPQ$%(r+_RZ9!8F0?>w<%R< zknJa+ay;c5kg+q6&I-1kdQRJpq6}hB@|1! z$GTpX!rFu8?}RM47fgT$?jF%jQFn?Zv&Wir^CQT7YG2Y!pBsa5zO30U;?fEk3FLEG z(HW66VN@v~tbAQp_)8*Gd7dp=!*bGZXTT}s>V&0na2xfUW-J2;by=kWmKXp2mFGkU zcZ54nJ~dS#SJtsd??JA`SGsq9?OSWvEdj4wylNdzLwv_d*#2+tCTZcVGq*nDs>QMvO#`;-a?EFL1{_^TK$6dyo==*d29BnF2srGuMwCz4@g{?*UWh~m% zud;lp?0MDLm`KkLL#$K-b2SfQp+p|kbv4Y$4U~rH+@Cxy#$u0{pg;ql#I(`Gl_D4D>O+L6 zee*dXQiAg#A+pf}Tn_WjS;NoI3^yb}E9urHO?mC?wcm)E3)5A!7y)%$-gjgbSenru zmkB8*=aT?NIZE2%P|7~z??HS0Lw%H^dlzZ*e*IJs#n5?qv(ch30^Cb0<+A6~9Jj`a zt;NLPsq#u>OVyiJMelRssPd#}<)_xRLeK>Y@Sv6r`K`oI%6v(jk}gU)Y>{OyZ82i? zIZ>a&%Z_nsLdoyyfMHTru7Nk#*xqUKQu}ly=7$o?Y;}@D8WC(XCGO{|U_Sd&{#9h_ zpMSh*H7w5!+dbQ>xzs&=NEq#wXr8;d|1>X^zYnC5u>e{^tbT5xvttvB*{1T%iPLbb zdwwddT14G>QQJLar_t0(+5vzR^hIcWK~8|*Y|!+JcG!GWMDJxbMb!TC=fdT?ezMk& z%T0H!7&s-ro9`#y%&-JRHoKkC6LH5L@(`*iqgMFHi(5IY*%8+}Y)|NR;g zq4@V_tGuy79dB?qFb};>c=S1aBv>vsz15A8=J@d>hndX`;5sMlXKWVepYA9GD1kGL zo5gk3eOpLKf+u#WZ9`xw9EzH%Aotl^dA>rzFUV4~JdNj$X!hd1JDP6vn zpt4jcQ~go#NtrupH~VFh?<)}!D2H;_g?M&)hG2amHenm* z_87u&%)W|=uP4ZPSeSkE@UYQWz{$b< zVb0ha9`EJ6*xrzN$NtmZQh=$k_75F7JmUAIkr7^!Q6L3g)>Ot~);zcAnAiS*msn-I zhg^QEIFu#@B)Z?VOq?YIpQ~%G;y~whX8edDo5tDM`wa}Ykr_A_&n@;uc|hD<%|b(t zO^eXDJ~WoO`!aQSZw0pWc7=Mrosg_Ui=KYA-^Jo;5E<;7`093YUCzu5f*0%;RDG@g zM8X!jGq6qOwz?B3@UT`s$of}qieZ!-S{?Dbe^d8)OeT?#Nu3Enx+oR)McBc*=k8B( zhGi;8yWU;NEbeD)jiKC&6~Ik{LUVsOguj+$z{H)v*uLrUvi)b@z<`Ci*T2DpCPx7i zq$)hG<*1uK2^;oXieF=YQqQK)FpSV{X7Z}UwToP(riwl?+a#x2bz`8m$|jchhtM{1`q2(Th~MitLZc>El*`@T z-e(F@7}826E@*9h37);^DEaKPrHqLRIP%d^GxIAv46J%od+NZ=l;Ae~350PO|2&y3 zNhyZ#3AnaAr3m5$67_;CR7tjV?Sc!6lOwQee%ThHamgSbsd`UdMv}1@%C#O@>9(#! z&oL!R_vXq^d=?O37M}vb;YI$&n#ip422CX(B?I0v*25o+PJ=3q`!7#7P1M6v@+Tq zHAi2D#j=oNgrps{pz*_v9ZAl$J|)?h3grsU(9p|ej}T8;4vr6RR&JGyr-Vjq{EC-S zgx5cvadZiD7v!*&HF46hH77)`_ryCvmRqk=+0e(d#6{-&hJrcHbWh|)oe9!ENri6h zFBJ3aX2a_9=ME#tvA-VR3Y&0Otirb@Axd2CUIt181O$Gso=xBh#rS*A6BWM#%w=(M z!6&xFAMU3R(aQskX$wk)F1coprKy!rhtr!|Zf>iNi z@M)g)1z`64fj2Dosl!!Zn=&V^g!_t4l|&~b>DV)z4&}b4H+gEutxE!lu}o~K98>#r zcnGKoed>uZuT{~Ctmgb%A1IPmi{fyi(#y8PxK9V>I!c7kAVHib=M4_{%!1LW40qO`_7~Sd8=xB@aQ^>a;&@^eY!P+MOcVf@CXPBf&o~D z5^APpSmN~ltbc`x2mb_Ls^-l$RlI`jse^Pg;|{k2JBClTYEQ8q7BQN+7{ zGk$Qusw^Rji8VDI`hMSxaWk7HT<3|P@n*30=}1Y)Qixl4st`hN7b;2yDV@B?kJ7ukvG@V_|@0NAHUJS&594J z&;uG)U|WR}o8imk{IG@!%Vq+1XXZWq7Y>Ziq{iSHrWmv+1;N59C zsfVEsp>Wo*J9}8%J1MVg`7SVIunT}b^4m!Jw(Z>nacVG_5aLk42tYgBvpTP6`o|}M z#+AOfzGBzd^?Nxk_nztMw7t*qjrLC|YV8&O;VMg(=i@q5jj7omTE#pYN7psd;Bfid zAUX5J;P=?k5}f4_c~E#a#tkhO*VPXtEP<3hssY~q%t=;@`>VvU+dXMSF~@?SkAz!B zaCzg@a*U0wg3!>n+m7l|5bJe+@37M5IacRTXBS%PIkb_k-qAW*+@Ye6)ZZFQ%@kNW zxl{rB$4Tlj>rm|o&ti9azhb1!o8I8x_YXA6_U9^v_i|^%l{Qz=I(EANONSt%V&^7# zm?#Q67c1i;5Kc?hoaOu6y3;qPBWGqxV{WsE8`JEL3tP&xdHjQo0}ag9ZaqYWfVzzPZPa!(woKwfM{u;9=(W?e}tc|WBJ(sJE5rX`Tgolf{+n!G? z!m*n-L{!vs@AGzJP2Js#!OBht?evzPRZ84hG^GB)*tya;@b_L8Tx$8Hk}c-@kBO`d z_uKY!XL@W(w}cj0Qt&4Jj#qrMyZyc=aCR>CCB*{$5#APoKqBeTKyvBHraRVLl;?8e zv3I{y?R^tAaQI#W|3?z-9A~b2r_)~tW%Lnq5l3a%j+m%_Ae?2O(uq+S;kq@-eT)yRYyo!w{|riY z)z#(sb#p+%w};-C_0vTu2IG&=0R)1WEJpa7f2rRY>s$+MnO9c|+*>ZfWb?Y?7o78OW_5t21quT02@RF!L1gKhmT6#h7TfJcIBU2L=&0b+Q8_!&%*% zy~89R5F&hK%g9!WJ`$4~Eyxwc)$U#o8Kfv&)L^c!)*;R0`fYFgBt-JJ9uXC!MU_01 z=rU1Q#%u9T@^aIjH^7k*m_Vvl2tHp;)9Ss(QVbh7nJBo3nBh=wbR5ZQGCcO7HDuyW zATmkfon#=z=>j@-WuV@!XU`Fflq_F~^u)_suL|Z7><3aUd-| z2?}_rcQQpqy?L_!9#GA8{XS})b+*-uu%R< z&W*ooiGN^%7j1W!D0e8#^u9~|@K)m+QN#_rkSx_i2;5+!fos=@h=NiBmpDmaI&mGm zm)Bx3M&&}T-gaAk1xooKDv?4BFzC%qH0w*cvcwRkP8Kl77NOCcRPzy1sGDx$`yq?v zi{8lPQPorWoyLZ`u_ltpeIlpyMV;TjAb%i|d z4|qd!9cPV;oLEf5Ficy6O2W^tR+Zws*|#kvXAaN4+8R0(a52^hPgUqYJ?$43b0u}v zG1sdjG_+$EZ5uiJCIzRwVF1iC!7^p{9DZR|*tZv@ zgW6?{&WQO}F_ z`PyeV(r_J@`L$k3{x5I}?zqg(M9WrNXn}y48iXut3v1tF2!RQty6K5HUR=7{ z4<~d7@EItuUCKvCBm@iouC_@vm7^nxkxLW&TpUj!a|?41kuB9Z6NA@m+y}>>i;MeW z;&0Z}S{0LrBa<1cw5*V%Z=boPCLW?*l%)@3BY}x3Pk4xGUt#72VaEo6!rO;KC+k!? z0*qCPD`NRTu_D+~Y!|tG)uH=-QPJe$XE0y4xj&|a0*Shuss(jr$`Wf^Ah>*9O?2uo zt;xie0k6C1m`cTQ?7sDl8x^%BO5w;DwM2E?WFd?{-0D$bcBlkcoMB-?(kNJpJ9=TS z2`Myre6^>7llh`sj4|sQv%9Fa==)MDZG~j)ChW471a{l%JmN!22Mo9w9~xu@tmUlE z9A0cX;i|^ER6(09=9X$+X*FM|z!Ti%K}1%g3So7m&FY@DO&vBK`k1Y{1B3imM%o$Z zaw6cc5@DBn@f7aHUR}#eIx7Vnu^+&?p`p0Q+wfGG{`Ekq!dH-LhBSfX$WAuQfg7U6 zWEz6u^m^p?Wt!>!wMYxnLQv(u`;)( zm$l6M+wW&w4*q3}ia#KGGknvPEOX#sIG)|5U?-BHA8Is2UvwI?hK9>r!j5_acE^8& z&uf8)5uj*BETO`T34H@Bz`YE-6kDNud89M0%!Oz(ozInE{_}K$V6B8OmJ=KB^wRI!Yd5c@W^u>qoNe^t$h0A7 zGbbJv>I$i{z4aLA^~NJvp%I9dKB)E@ad`xVmmILcDq}q54YF0yF)Ear25c$jIsV7r za=uoVU%^1hggWZobKKyx9kmg3x^)4**95zGWazI`QqV`{z79hjf_TByDzd~BQuZhB zpBQAp=3>(%r8secf!}rhN*5Ud<|sa1`#=kZV~GW<0pf|iO6_Q}YKsFBGA|L3do}S2 z^?3MllBY84J-tAz@yFVc$nm8D|G}mC1AqH=qTZ}uN8xmBkumQ0Mg?DiAo}Ny&h~6`LmAtC(@PA_ElIa&F zglzpJU?-7osVcs(jc1?=sNt9vmg*C!h=}1fG{OUnARPbCiN$Fi!mZutR2Qpo`Y3u?otf5OND!07&%2IxDFqdqBG@(WZ@Y*rdaYBjb()P>0$2y3oWQp?7ejGG;nZ%&*<-SdG_N&bw)J zvsD8zd1-z2Qc~26|2ipC1{(Rw__o}}-;9MveIVgXnjeb^6S9ei(jo)h)GaX~N9o%a z&n07kcIZs!D>kbr?ipcj67CnX+#zX~9^Ocr&qQwSvod1sDTcCKmS2lS{q%{ms*SgvUXs^kJMvtey+?z^~ww z(Yymd4Ft{a!@%sHj0n4TurTLDhr!b35nM+_)BD7tb=IHoULDu_C!`1@PaZQwcRpWS z<#qG0(l=Jj!7+l}1hFt81UT!xhtm-ped+7LsD=q1QEGhZfQ5dK$=*k6&^n@@D`W;4 z*a&rtOUMCY#lfmx6EG5m@c9xZ&XG_LvUP7P_$NTvWA}0j6aWE(z98>3g8p@n!XHN^ zno@_AaksCrem5N3c%QAvk*%#@S+4HR44|=p;kx8`SA3@>HW=FCL9f0w7dn(2@BVZX zXfeca^wam-?*HX-b2I!)^v9*i_UT$|^-uj$P~nE1gRsV8zyMDyw~7jMHzMNG&B2%IOkEl|8d^VD{nf%Iwqrj3KChADGh@B%sGwT(#A%)!YueK) zT@YrA5E7(k7@^x74~gUCT<5|Hp%iNsast@gKD$^a(qRj&A z92(Ar@FEU7^a+&nJ**az(GV6^&M3wT3K=^NpsKG^ZNwXfJ6B)id}rOZk`OQ%cIoJ^ z4;&#=?DPO7*S=VAaJ(WFKXe|v&OHL^Ur*%KXMkNA042VyW7~3vnVqkf!=aPlK3{k^ zkU|p&HZNWdj7Zt^%|$j5tK_lOn=28-84|}~%zV142T<_Nf~L@L#N#2Qtz6ea%~ZX&R{M_fqugW9trVg4a4>C>-W&$R+)*iyFSG9>Bv0mZ?jxa9EQ z-_70=s##7GLVihlh8TK%whDPa^B^(+r&-O`-iOWWqC%+uL& zprA7wW-#&Uo9K5inGXtID{@U0`_lS}FouR9fOEy@EePD(Ew{5=!9^B+%p~*YAY}d} zkP3fRPLH|Naps0xmsVMRV#6Fl>tbzjJ}9BoF{^UUG%lviml9k>LwE^|_H1`8RC?Kx z3@28i0wv}}^)r?TyEj_6Yzm=dW=pX3Gyvs7DPk*m)rZp}teDWWL?Jdg(yTTv9sqdR zo7v z6V2T2=E4y2kwpFbb6FZ^nxr~5bna1I(!8FkN|zZ!4IHeE$s0cfZV@!8Yp<<81|lN6 zd&nH8oi=hiADdm+=1GEY{h$NAv!6(o5vJ_$t2tIs=)P!I&7zu}R`8>@rx|99G$R{c z*sUy;evcv3aL>c~y=4epLF-8?&jsyp-4PgV!2GbSfVc@97O_PeI~b4e*FAXki3No< z!SZ4(=Ig96gQuzF`8rXs9llx001|3v#~gM4Yk(&I|c8w z#6BTmW6-eYVlg!38d!tiLQCFbK24TS_rKPr5^fftt2} zArp;)%4~P{=eiWk@s{vOrqsZaDI14hJt-YXR*ffeLMa7g3V7`4>h4X~jqxL^U)s@` z=HDQE1Ki!dD;m@t38D=lWFGnmB~46AZ`lPqfM@;acCr&Wyw=V0CG@P*Tu7zlq#B>4hfl+NA zMi5)+AAvj6&nnakb()*Hn?6Kz&W~bNb|{{D?LsF24mUTg-F8v%TU2+Jn1>D~cz=t) z$o}5(`|yYw76P$AzdawbI7m+=o4e-O_2e39Y!_G5Jh`c2F?6|!R7*9%Tvt8639{#&A>OjL+%je@Clz{E?k^0X+ zd{bki3TlEcY8;^Gn1@_L&y{F5C{26ZZq*qmblu6qVm2l_FJNOM@pN%MMaM-x0vK6F z#tF1RpU@B4gL{1w<~5VA8#ewtch-dSKAkYc&FL=n8SfP5u^d6sJE`&Sbo$k1EsbZN5t-sk?A@0H0Zg1k z@gPapM{ZsEt8z;_PB^7a0%ErvX3ORi2E!w(6M)J{nc~nK~ypDRyQ<|~k1^MF~ zNt{QuqDjo3Kxzm30|__RYhwn@I#>2QEI-V{3@d7Cy{O7_Cy1aU(r@YL)M)gjSja&^ z*}61&6cxQV2E2)(1O1%_cLLM8!$T$&<#xQO(=WrujO72|`p=`PRIJlSHqOPcG!)Ss z)dDQt4?$aC0?~?H=jyFWB~EySwfToxR~ov)Nr0hZTHfglH?GXLJ^8~drg;$YmStD8$CWu!mQ8C?Inl$VCJ0-}JtLa*r6tN1jD^~K@f2xBEph$v7pB4K z%@;yV*lMw7p5Y7H(rx~!<<#*pPffkM2HBBXd|Q&WY!Jb7AwTI9K=2rz-23J~IZ6?4 zP~mc{c47AG7Ki<4$l~u(*kZ8)`-!pRT#0?7oAQg1y`U0tYSw)NJ ztm#T9`R0i1q9zUo?vT`An{(&kbaONrRSf$4+|Sq7_IyUb3Roy!yLH6n79)WL|L`v| zQdznS8@Y%2#Trw4W$$%wyFJE=1N2#swHVQd<0yt6V*_6LvV}&S!1$Bw;s6V4 zUX45o3#B>9*e~|NF{e^MlxDoPvwIj9wu?a<)3?`?Qy`~~F*5d3pl}t=mW*%72y83S zeXexIB7*j-%HSj@RtcO}<0n+mGeiq-a~84gz}9Xk?bbhMXhjW*CjvlpHUVVf06_kd z8LH+Y9wsqp_zI8bidc69AfXqHw{4+v3dK!j<|;FiJ8z-;#a?)0-IRzl;gjfv3zFJC zz%tn5eqdy(*#ozI4#l#`3LopqbgJ;fopYJPSGt(3UzhG<3$WA(vR6=MwZ8$M-?P8p zC}RHWc)+h)p^-5&V$pW>TGatqExkt!5E_Jdl@S-oUbQ%$FVChs+F6}7nPNoIBD%IEsS9Qow zGHVGAZ*6pmp}M?;kC8sG3fkYgZ@b6r zH$Ee-X1Z&t$hR|gm3~rSWo4N!8PjcCggLT@Z;?QN^M?P4ofg=En9tq3wkA>U+7qqj zAY&lYdmqj4T!V|OryX{wMfO8SUI@bZ%&}Ebo)%hKV(w= zVH}_)$B7W;)P;Ur?k|+jI|gOmV-ho@P(9sawUp=`Bc2KaN~>;74m_;va7`L{L_1XS zJV%DBOPl0Tu7K5NMN{Cen=TwrwBu`H!4)1LCs01JTkHcxbkjs!erUsnx86MV6?elez(+nRugOPME@x^+)dakDm8K zUMQ|gk}t8I2s9$QBIO_Qmqcg@NT2mw4^7oi#4{qJQ<@W2USJ;oA>*vONr@<6soo`E z$a zrdfy6?Ec2=+(b1>aNVMAYm0ewa3on^V~O&zOkUMm!gi#>v8|fI5zpg+(e2IfgL-Ug z;A^|V`w^&qs)WS{cxJ;rum7VZgxYtJaNWR4YBCGgFGkvs-h)9oxbWwaT}W+k9t33O z5*q4e-8URry7-PE8Sj7LMk&E%sAYeN6L4Fevx=twa(BAYZc@L^MraJ{j{&^6?5FV2 z1-%_V*f0)PMNhJf4lwcbC^FPwqTF$&af*2K`W--%KWinbsdmtbaxDeR(;oc(aXp%U zPQv|$i~tXG!BFzHaf)fI2TPcK;EV&f_i_mo?~k8Az>tX}GNGoRzL5w`$LGRP0DbO5 z-ww?PLk6sl!y~dHKBCnM&me>IJUY+{ClQ%SzSZc<;P=(Ow(BHP?j5p#&0Kz+av7i- z2p6&HMGWeSHbY_kv4Axev#bpq|L_jjJNRrKLR<}O?)yWbY{W!}1g^AFa?ukr*xo6i zN+cfXI~P|YAXv9I5TcJU(tIEubr`nfLX@57DM#rdyhi{_V91cG$X@x=853&2t#q;~V~Qv;2LH6(l(rAAm_Z=i}%4J)GNGN`J%gx?6ebfNAk9lqGJmN6P{i_x}R@4V@ zJbIa%NDlcVygy2=1l*6JZ1#Ez4hiwu*x+{2f**EWBK9{T8Rm5-#4_8g$+U-B*0)Egog8ebjJ|JEU!Ar1omZa~l^q+T-`K$AEmET%&MxukKWNA=f~g1Mef7o%iF=(4Xd}d%Vh=Y z1FMfO@5CQ^!z)9$?>&k)5eD<#WGuF!1mrLSC%3ML28BA)%&d5A2tJ=BTe|~=>Q-CV zWJEEmZD{dVUmJ%U=C2LEY#q zwE5=Q{wM|{4Chv@H**GVL$TlR2Cohpz(jc~7`K=fZcDzoY%uIK~$ydiZ zOR*4-z`rLQ^D7~4lgmw~5)%RXcRugKxZ6W2Jztch{M3HUHhu}+*SJR7-=~eNJ91r} zJ<;EK3Bu9Swq@AE@A8#HXE24xy(^a`|<8-)`JFic0gc_VuXn{2O53T_V!?v^MEn48|g__FwRr2uiEY@*GvE zP9Nxxe}WCC{8Bwkpsv>@$C3f@-V~hwks~W zcXjJRlCtge_nZ0+x7rwVAjm{qEJSZNR8b2^HG-x+71nHi9NlZILv$xr39XrK|6hAo z{TI~}u0aq`q@_VpkX%5zOF$`+ly2!}X%OiK5owU_l%>X5NW2pP6^w=Xr#&%3w=DHr0S5YcXbYiG{Z*7x}0*cha>AB4%9>gs(Qf(KZlb z$21fwg}?mTaj<(_4(zPEgAp0*>~sehgz0nhQ;h~}K^}GCL(X>NGpwJJ_H$T6wKsO$ zDL@U=EB%&Dh5j&k4yx|{>^SHXVE+3&$EM1Amnij7bnsn#Ux|wvoi6W3ce%mC<0n|PuDgQw!!`a`yAiVbHq>wD_^2C38A(*=y3HnJ0*PCNYX5iYFXBJ~WWvFz zHs`9HU+RFUFF!yvG&|v@Qwa31b4}oNTK~+?d2$BdRaY1JubH8jhc6*%RDBa?HQB%_ z1+*V!2&1@mDF*#jDL?!tdgY|p zBo*U#4?7Xs+D8S#UhVmKd_c@$A5({s1kqO_9^xR6`@V8t7(x_ht=O}7=+xP33uZD8 z!sZe4{k4A6mD>w*)B56^N3R*D)#cl|!&6DA>}Kgq*uzI+xZ&P>{MBB@9UYtP`1im@ zeVNm!dKxX+7G}g@@Eb~aST)0OtoUiMzh+C28J`NT{34s`e9- zO8}}e@QTGyb2~xD(%H|>VPGw#pBxo)S{Q9kut$D$vuDeD{Ve*-G+O;1+anv-LFT(k z%f$Lc>rXS(BqRFs*N;iJ&_;CUhMVnutlcPNf0+$_qs3~hA;}G${|cOb+;#z$#)DH$ zH(OVb+TGf$3Uy7kJY>Ize}A2cs~{@#_xe~27z50Upiuobkq|(igc%El%T=Hq{i=4x zad>MSHTQU^ZRpDFs3=G={{pu{$68pW>exco4({*D?Dw~*4ZPMP*}Br(3BZm;yXaG@V$lX(*}-%nQ{=v^KRkpqWtXZKfmMl zuh{ft*{Nhrx7^w7`of=hyl1X&2wJ}&RTs|0NHgw*`6Xv>57YGZhQsS@f{?gz*1_*70FooknBzpHZDb=v!yX-P|I zO6uCjTfTU-7Myf^JsU+5^3JhYo*H(2N$IPk_8M}(M3pI7)*JZRA5lGoomI2dP9k)- zl+$x1Ql?Q||J=TEJ8WGW@1t50IG?LP-lxo!-+2FI)9E@65HmQA+%E8IWFkSvZ_=u* z-a#qPhODB zKB5gF@fhpBtn5vWlX85GO~}^gcK)!?TSv1J25?3Sbz`@3#+R!3;SS4PH-A@RdVZHq zS)eCeb-YIF{dZx3AoOK4d-TYH=c}@rsiv;B$zX7yZ~rW{%CW=95)h?kSb@aH8QOC3 zTI^-?A~K##jcy2dz2T5U;n1jDb>chWNSqx2eTh%~J8UEjGCJW-iVNTpr%Q_s0YR+6 znn?y+&gB$6l0oM+{3r)de=%S9rBUX>H#ptYi=@RGBhNjD1TN#C)?9PSAMCce%@M)= z9@GV!FgH1EIwtj@+GBrc9bwy>fqZZC{wSUX+y2F+2+lcS10R*ll!~q2sz1_--O$p8 zzo0cx#$6}pZLihzf&({)k?|)%6FUZk8ZkBaLY1eD@GGnlmeUMrwg^F}hz!yo4F-$; zGkV9=y8^HC;=C5R!TrA9_u3*0(xA-x#?CGXO|^B=%mO={#v$*}nzqV!Z>{+U`{$3; z9Qpa(0ASd@JCm|x{=huJG2&qkqTs(jx^;F)ocydOhWl zX(OVQ?FnxWt6@H!zOJT=!x?p(=l0pY&YXv?SY$F%E{gXQB}W4`1CfoX)-?6i+P?t3 ziJp*=tdO8wx#yI)7;HQ<+eIf>l(QgZ&=!x&jKsGEIjZ(8Q)eDE6I#7q82i&e$dvKr zDV0k%xe&Y(M+?R|WGym8ZTX=Uwo5apPqNZ2vtz5A&Ci>?1 zi#Y2LqFuR7{h{nYv`v8%RB&#;_Mrvvy87wKn>+*kWwDd2p-&NEV{)8(|D;a@D%5OJ zTtQtK6-842T?@u-+CB5VB>$!ZBnuC1$o1Cz+)7kh@uUj*GD`aRlL_X)caDkC^W+KZ z&peP#7_7V;LlgE|y`;ZYsbxo~1@BaXN-4R)n>H9P6>UT39ak)WL-#YWl}jO7-KvjG zN;=JBsc7eI4TV^0r~qx9OT?MUSV!Sc*9mz(s{z9ngWCw(*B(&$KgtK=h6)JM36cExrDnpo<@Y^u=!tGjWqs{A zbfPnn1SUHTV6+mF@w~ls75a#(`t-D7@wU?-6v}op^@{pIuWyCB==*l*@FaGjiP78g?Oa)Xn}4s zpdQXbw}1J4@pdhK{+y&}ac7oyJkL(V0PX|o^=%R+Ws3dq>)O>XcAix*WIbDuvYRcP zjarG6d(!~=A=M%a^?;66`7y0A@|aGHQ#$|8Vpzrc|GO$+EeY zl=8yNN23hWH#jow0prtBs!B>#$Kl6431hzX1ra49ZkRHi8%FZTmQG z4Mh+4Rl=cyVr}PkXs%ZZoFaU(w)Pf*413e%!sE31zQh1Q=s`(;Sc>+IsvA=n`HB$E zsqoWo_hAPS^06+%m`wSD#ep$r(<$1;SJ1gPABsIbi^{7=JOul9|_t7I&2^A4Eyk_+iz^b8w2rOn(zx2@I z%?9m@NPwS5Y`^Z!lwl)5y^(4fin7;~YW1x9$W4)6#Gz40pE+c44$dQ^y1U3CI_I4YT4lUCC#15N|L(s+Sp*wS|8MS zC|~bwom><-oZb4fCJdXik`(@2Tk%y?$w{9fqS(NK0kl$l|I`HL*WMUpsM&UYxU*TZ zLaGvkDy4bz6T4F}rou#!1ZkA_ts?4Kjnz1)!82c<@{<9x&5+YDZa5oa5ohtYlLqT`*w{EKE}DJqrjtM z)Q076C(|B(f#1S{chJ2Fd!yTLS5iULO4T6D1dPYq=ie2Y+y9V@GV)TGYC- zR-B#bmKMYY*#c(au>}hvnWw!K5cnJMI;C}P*$V@4@_bLDCUz3`4TGYTSH%m~w8ahk zxbfVr^1|>-M-X$y3#%#x0M-XeEIbQEUN1 zBN#$(R8Jx$hgo~s-hCDibCO+M(e8XNAqQ#QKxlf|VjMEj{5un(@Ke3XT%KNqcH8F; z2yY2lKtv4c$QxH>{5k)Y#m%E+LGoa)^>o5avh${KSu+c%ZXeYnnCQQo-e3urEGr|v z!%70^uXY9Yu>D?QoR|;~Ach?BDtIFyM4(+!%V68CLgt4Jj}FE<;Z~oV zyLLVJg^UO9=#4PW_H6{|xz8e~{S%%7T!Z?VW6?F@4en8{M=Cx44}niC;2w|E;ZD7_ zj^+Mk2PZRs(D6+rW=t)wozm&GitwR>XE*g=`A+AOh zpJP_SUAO(b0DVBOHAJ$7HM!%(s?oWNh=*$G!13~ifaxZor&-}fyPj2noX5NAAt4j8 z*aexw1~_!pwrZ@E4(w2`fm5%Ohu^+WINHTo8TDqxa-EvKZ9h21)Gg3q_b?7AL?+`d z8vp%g`N=sk8wuT}hoI;jXMq3QFO?u5^g(x603<8hKK!00@!xNnNeK5AB-=j>X_Fj_ zV1W^b!}=czU6(n(^Gb`D6felRtY$H9D3dA)D2%E+f&N? zmZqOLvA?*Fsf)Av7_M3NS7_qE0pGsrnDn*th9Y6B@ssnOM2=_E1r86rx2{o|Bzn z-Mj08S$%B}GYU~qbr8!K_46-gH|ruC!gT1i%;O9Bc~|U&yf7SdUapRMXZ^0++!oqDcNUZAsJ(7_{G&KBD_MUq&Vtz z?}rq#F?pSf53Hup7i&GRUYiW$2cL$%o{Cyjg}OUed|q*cFa-{oLF0@?Znka3Bw<~$ z_j){|VMjYhPRo?0HsimYi@LRe8^m4StwC?}foo`>C|2P=wu7f5I$IPGq{Mkctx9gfnv`TsdUT7NKOP zfctzO`>2gaULh^|I0L}b;cBs!PN`WeTmZErOnEs%XmPD(Ijzm)X+&UZ84)m6$X$WRW z1b5#e7v_(JmX~@p7y{SS(4SFr!-Ii_7gm9rTTys(EU(sE-+JQybCavz5s|y3?#-23 zVq%?l@LVfGybOWhsp^$QLfVUy&{4@dM=swV{2&k!=XyQ{u3@a)+?TT2m9`LNTC^c7 zJ9Z=!$I~ylMub<9l8{*VXQBGUKkiMa#f2EF=HG&3@8o)?jv>1&T+?;|OBxN0%j~A& zARG!Q{Pw0rYLa@1I`d<5jk_3W!9uWXUdP!}krs&}9jPeUM7#qI-m7iwFV{1RxRldE zRsG0gK|p0qbpU&7d$eK|NhM3k`4CYxKE2%DZ;9xp_sNnL#4Y`bBSX20Ofmr$bILELmaa6m2z*WLVC% ztW4C~kIR!$SbSVxewHyMn|T3eT3mh<-WM4+^NP$O3U#`Y`UxgP&FP zQKw-25(6maF`le1@4-Tm|KBo%UB6CE90-sB*jHW##47aT=+_p989QbNW} zQkTuxjN6{q3q4teSf5nD8%ieLS&rw!R}huNrq{@tDe9$bRsA?-VkaBLgYMG2T)D%@ z{lEM0!=d13^LZzuMa<`$w=NHHXB5gX*w5K)o}x*fG%ie%%-tu}luC?&dCV>@hZ&%4 z%ahu=mpk;s1+cD+vDB&uO~H!%hU8D$!_9LlyVXXr^jLaRcm6RX9X}P_Rgz# z{MPTHtj6hl;4OskshK;^0k7x1k=u{B4rAy;DxX3F)EMZXQAYwGz2yG>Tl5KrQe%& zrS*MOPtxGxSzuuGd{1|u0ijIKlcwPlT`_xL0qrKWijKotY?sY4$oXii`W*Ba%FM&0 zQ>3j?NRZ1-Ly4*@bLPgaRHfW;iQrT7GSsCA?DE@?roZ(g-yiXi`41meAA*CEr+=WJ zA#brJ7ccF^ildfUX^3>C9E1mOsb7jN_->VsBwjl|bTqkPqr0`~eVuf#s*V#97HqxA zsd%v{Yw#6tw_x}c0g=lwGV!eTEU4%#6%N@HGjY(_ir|lTX=1Kms3}cVLDMn3SGT#J zi5F{Xf8MuH{R7?@ejUQcEph20$eJ#9Y$vnxmET4v($QebX>{Apu_xfd=z0^Nk!s9N8yOnuxIsK@ zU`yKrfP{J+!?k1N>lH($+^t9wvPvYUT77j$kWuK)eNa$7-tEz|s~8Z1fZhg({8M$53-Qu=I3iNJIc^rv5p)hNemm20n`+hTzd1q*K)>oqNCsApN8!@4@ByHBYUW6M z0^-b+_ar{B$(2a2Sv5V?dSsR^heX#NpV~Gs&*Xna-eJ;sfA`Cl<;BO(RV?aQGjp@y z$FR*Ed%Pa&ih3@M4Dg4zO3O~E;$~BE{^gMHwJBa_;gp?{0QSRX)vXMLkxpUq^m`W2 zvGmLIP{~m)Wm({ASgkR@BKcsoVbK3SmWagu{?Bo3f~>2)mfr%?!XGKLVsDZpI%V(I zf>d36$q3vzg7D)rK$~X6rTJOsU(Cx+Ib2c(hs<7x36B~p%TjJ& z)i0kr258f2Oy1ioKBn_y&3O2lg#oSgKkL%&9BiAlTw-m1%ZW7|?5tgEzG<&Z58!)N zQx-dG_3q~p218EDchRYNXk^tQ-3g?@dy&#$Hn0#wZg;40OeEj8^JP4Hf?R*g53_Mt z096qfQ!r4JwvH9B-iBZ4=~c5toBrYD78vz3A-a6Wo2huKbp}faAwh-q z6z+*)KwCB@oL47BNcfvFg4*RyWX1jC6R(($z2Z*G&#t=gXh`XE>m{eJ1v=Woy3;VA z*m%C@Q8&R0vBU?s&c0WU(zf3k-JoxWNZ-@jyeymH9n6sQS1tyWPY-F;`|evxOjl{^ znDR>6w0=8!-LcTomtq70W%+V9@)b$-@3{j^3d>R|D*jr6;$KFqxpt9N2NO2@B+`Tg zNs%N9Z}lfpTG#%;Dx|4U!~6s| z4Ua>5KbediZVqX}$6=NKS9JJrP1?yY#rBZR^`7xeGEzXy} zNC`mRvImFHthBj)n3Ig4`pZ)8-2!KZguuiERbRQB5EFD$02QnK8 zWFHbQ#Lph2CId)A6^FLHa(t+TVirLqVpkYNv9B-%h)58a^<-4feih` z-(-BH8cvidJ?B4~B}A8dAN-zfDIo!4?&(0n1C!7dHBn`AWtk$7g-(4m>!tRJ#r=NU zTYP)!)RznKZ|saNBFTaE*p(;Eypvz8s#o}h((`pW4c@Q#r8ERa_dOYh#?^=7FXjyP zFvGI0sUB~MVo3d9NcN$S({8@@D&$BnuI*Yg`7R)&1s4rWdfmOL8o-a0frnKeL;BZtP{jf- zy~}Ud|8F)|QKGc$GVJeQE9~FS4I{$o1x*6O7 z9wF8W)*9B5Gf`8!vD8L0Q3%58{+qvwY(kzsuP)5zD^k9e#Gi zyM#a?Pu3R<22CQd^rXCBmM&*WRGH|hZn&}^+Xf_~}TbXR-u21s%FqrvsyverR$jHX$da+d|kO@hVfe^Ts+^qjxE7KRQ)Nf<~oz-+U^`KUjkPUG>_2x_bK98X;f; zPeeNT-QQ+pbaJuDj?P$d%~pNpF5;6c&N>Fo*qkd>%XczS$P#Vl)mp2+9Z=yh_aFoo z1up%`U*QXy!%v!mP&zaSXGpct-F$?!AUDL)XUDg;u;_;mLWi9{bJE__S$ltCVv@`E zBC8b%L2Vt?C8(TCEvFLm{t$kxRuS(YK|*>;;ytGL&FkgNXEg+SozJf#@BY<1vFTzB zA~kmru-(rbjZbM|bge{W)r-Z%>uFIG5i?BB4?a@#=gpAbiI!K(4|8f0%;fo>>u%!_ zqVL*q=G95zD1%wN8BZQU(l&z&E~U>#0ZFxct$mB(IEU(X%d&(`e}8 zr8r`%BGufr&Fmz=>i=A`?g?G}bg8Slb=qf~c(sJFx8oyQ=d7H~o=aA{7=hY~gDZZ2 zX1mnYow5JkI+utUGQ6=-i%(Edn6NfFL@m2)=H{l;;E?*AsdY`HTd%6)gcw*#U!sAd z>RI(&)ffO^aX@++RdPuQw>3}b0l(Fj51vMj=DN3~W!8FHcz7{nz24uUa@SUvDM2}2 zEm$`cwe{WodQC6F@m*7YZvXFX%iwSTK~7_M%FR(e#B0t_hE(zuBf!FJF%jUim6hfF1>j6FgceIl)PfI~1Kt-~lU1kxqj?puwYfEZ;*tN9i`(21 zYzSClBoJuE_(Ebj}9j zmtVY?+n*YC!qMUQ`z{73RYRHgbG{~7tw1QM54A3JWzAQ*$=jS0AZ1GaXY!=5MLCc- z8%H@>4#j&FaQ$0q%~w(v-SL9G!Rq)40RYg=USI#wwpg!n2T!pZS(FbVQy3#vFIUH z%!F|uj1P!cF3pY>RS^I$&`W#!{-KF&Poit($2g!MtBe4eY-2oF8D(}|DtP(l)CZW1 zkB_ka@g15T?hh(SvsHgHkm_|8ckFSm2FQefNrC&{H|u>K=m)|g<}yw;ZerT=D^kh) z9{0d{@otBhfq}tg(Y|qL#m^@s+?yDXl*Bl|2SN}7E{`AqIx_s{v3@tmA;$-!P7c|r z5wcpv8caRFlIy`C-jB2My}-{KDWd2H*=+f19&j((hRtJzUSiK%KYA z(s!s8^myWzzwp%{>9R8vQ}JR>|J4@oYa=2gX3EInVR`W?7F{iXY%X5xBkB#!E0J4& zprU){?aT~{iMab!s0SV2x)tbWS?FJDJ(j@YEJXMotYmch2?b@GMfneQfsAnp`7uPVk$yBHEdgg zKm`03{bMo{(>6n@Tj1S-aotImCv9~Op?&=F!Ww)m3^$l+# z6{5hj7Z{G%Es#UNOMj*nHVKY0pLhx`W2t#5$rt?j0=`f$^7WecZjmLO!@@Y*_x`BV zz^-`UP|Q`TQH*O-k#MRoVoSdP*uf?9RO^9ja$dP2JOy_enw$MlHS$^ZKYLW^S)(m- z@=uvT+%OlZz#j0qnzzVy*8mhg>y9M%ixyxI^^^n73U*J!+=5QKBz!IyFoPVtzKnb#GO6KA_{O1Mjhn}$cO`HeWGDuswJXWZr8EQW{iITXSGE?PL zdJx5zv~l&h==0=_Oc!uOUo@heLHIn#KPgrqQY`wJrRZ^ZE-~zpxM@(AM{mJ%srDlU zhqnIW)vdAU<@n*{XjK4tBmX_2orKSdI4L4c+x$3QjP4EO4GU= zsD;3mf_HHSdAKM?OeGwZdb?k$8@*vLQf-N=UFIUyH>PwbC{3MdwT9lC%qn=K0VKpF ztkrt-lqhS;PRx}w4_i*hpEL?vx*Q_xP`=(2QDa+sRN6-GYb6CPFGL>__$N|nq@7zO1L!fPSc-(E1e>S_5Je@+juddpS0iamLP3dns}60x;3b`S4ccXAe+ zW2{DO>}Yr1^-cV7&cQ)(@_>&xfc^4ZSE>fgUp Kk*$ { }) as any; // HARRISONTODO Type bindClientFunc } -export function removeLicense(): NewActionFuncAsync { - return bindClientFunc({ - clientFunc: Client4.removeLicense, - }) as any; // HARRISONTODO Type bindClientFunc +export function removeLicense(): NewActionFuncAsync { + 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 { diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/limits.test.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/limits.test.ts new file mode 100644 index 0000000000..cc8243e25e --- /dev/null +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/limits.test.ts @@ -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); + }); +}); diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/limits.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/limits.ts new file mode 100644 index 0000000000..3c25e51c10 --- /dev/null +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/limits.ts @@ -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 { + 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}; + }; +} diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/users.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/users.ts index 1a808dc4dd..2cffb37acf 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/users.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/users.ts @@ -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}; diff --git a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/index.ts b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/index.ts index 59eb3f6ff3..2715e91631 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/index.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/index.ts @@ -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, diff --git a/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/limits.ts b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/limits.ts new file mode 100644 index 0000000000..573f5d9bae --- /dev/null +++ b/webapp/channels/src/packages/mattermost-redux/src/reducers/entities/limits.ts @@ -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, +}); diff --git a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/limits.ts b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/limits.ts new file mode 100644 index 0000000000..d8db2cddf3 --- /dev/null +++ b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/limits.ts @@ -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; +} diff --git a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/users.ts b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/users.ts index ea360c73e2..03ff4a85ba 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/users.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/users.ts @@ -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], diff --git a/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts b/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts index 7fb299611a..33388b13a1 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/store/initial_state.ts @@ -35,6 +35,12 @@ const state: GlobalState = { myUserAccessTokens: {}, lastActivity: {}, }, + limits: { + usersLimits: { + activeUserCount: 0, + maxUsersLimit: 0, + }, + }, teams: { currentTeamId: '', teams: {}, diff --git a/webapp/channels/src/sass/components/_announcement-bar.scss b/webapp/channels/src/sass/components/_announcement-bar.scss index 3869282ffb..d06163241f 100644 --- a/webapp/channels/src/sass/components/_announcement-bar.scss +++ b/webapp/channels/src/sass/components/_announcement-bar.scss @@ -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; diff --git a/webapp/platform/client/src/client4.ts b/webapp/platform/client/src/client4.ts index 13ce25917d..1f183d60b3 100644 --- a/webapp/platform/client/src/client4.ts +++ b/webapp/platform/client/src/client4.ts @@ -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( + `${this.getUsersLimitsRoute()}`, + { + method: 'get', + }, + ); + } + // Team Routes createTeam = (team: Team) => { diff --git a/webapp/platform/types/src/limits.ts b/webapp/platform/types/src/limits.ts new file mode 100644 index 0000000000..1d8d5b60e3 --- /dev/null +++ b/webapp/platform/types/src/limits.ts @@ -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; +}; diff --git a/webapp/platform/types/src/store.ts b/webapp/platform/types/src/store.ts index c2f6a0c14d..8fcbe01a4a 100644 --- a/webapp/platform/types/src/store.ts +++ b/webapp/platform/types/src/store.ts @@ -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;