Skip to content

Commit

Permalink
feat: New Private apps limitations (#33316)
Browse files Browse the repository at this point in the history
* feat: New empty state for upgrading private Apps

* chore: Change Marketplace info modal text (#33239)

* feat: New tooltips and color behavior for private apps bar (#33243)

* feat: New tooltips and behavior for private apps bar

* Create brown-pants-press.md

* feat: new modal on Private Apps install (#33275)

* feat: new modal on Private Apps install

* add more variations

* Create eleven-rockets-hug.md

* chore: change grandfathered modal text (#33291)

* chore: Use apps provider to check maxPrivateApps

* fix: adds minor fixes to UI and changes requested on review

* Update changeset

* Replace negative boolean

* Refactor `AppsUsageCard`

* Add unit test for `AppsUsageCard`

* Add unit test for `PrivateEmptyState`

* Add unit test for `EnabledAppsCount`

* Move tooltip logic away from `useAppsCountQuery`

---------

Co-authored-by: Lucas Pelegrino <lucas.pelegrino@icloud.com>
Co-authored-by: Tasso <tasso.evangelista@rocket.chat>
  • Loading branch information
3 people authored and ggazzo committed Oct 14, 2024
1 parent 22ffaeb commit 9fa0824
Show file tree
Hide file tree
Showing 30 changed files with 604 additions and 142 deletions.
6 changes: 6 additions & 0 deletions .changeset/brown-pants-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": major
"@rocket.chat/i18n": major
---

Changes some displays to reflect new rules for private apps and adds a new modal before uploading a private app
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Box, ProgressBar } from '@rocket.chat/fuselage';
import { useUniqueId } from '@rocket.chat/fuselage-hooks';
import type { ReactNode } from 'react';
import React from 'react';

Expand All @@ -10,6 +11,7 @@ const GenericResourceUsage = ({
threshold = 80,
variant = percentage < threshold ? 'success' : 'danger',
subTitle,
tooltip,
...props
}: {
title: string;
Expand All @@ -19,17 +21,40 @@ const GenericResourceUsage = ({
percentage: number;
threshold?: number;
variant?: 'warning' | 'danger' | 'success';
tooltip?: string;
}) => {
const labelId = useUniqueId();

return (
<Box w='x180' h='x40' mi={8} fontScale='c1' display='flex' flexDirection='column' justifyContent='space-around' {...props}>
<Box
title={tooltip}
w='x180'
h='x40'
mi={8}
fontScale='c1'
display='flex'
flexDirection='column'
justifyContent='space-around'
{...props}
>
<Box display='flex' justifyContent='space-between'>
<Box color='default'>{title}</Box>
<Box color='default' id={labelId}>
{title}
</Box>
{subTitle && <Box color='hint'>{subTitle}</Box>}
<Box color='hint'>
{value}/{max}
</Box>
</Box>
<ProgressBar percentage={percentage} variant={variant} />
<ProgressBar
percentage={percentage}
variant={variant}
role='progressbar'
aria-labelledby={labelId}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={percentage}
/>
</Box>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { Box, Skeleton } from '@rocket.chat/fuselage';
import type { ComponentProps } from 'react';
import React from 'react';

const GenericResourceUsageSkeleton = ({ title, ...props }: { title?: string }) => {
type GenericResourceUsageSkeletonProps = {
title?: string;
} & ComponentProps<typeof Box>;

const GenericResourceUsageSkeleton = ({ title, ...props }: GenericResourceUsageSkeletonProps) => {
return (
<Box w='x180' h='x40' mi={8} fontScale='c1' display='flex' flexDirection='column' justifyContent='space-around' {...props}>
{title ? <Box color='default'>{title}</Box> : <Skeleton w='full' />}
Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/client/contexts/AppsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type AppsContextValue = {
privateApps: Omit<AsyncState<{ apps: App[] }>, 'error'>;
reload: () => Promise<void>;
orchestrator?: IAppsOrchestrator;
privateAppsEnabled: boolean;
};

export const AppsContext = createContext<AppsContextValue>({
Expand All @@ -49,4 +50,5 @@ export const AppsContext = createContext<AppsContextValue>({
},
reload: () => Promise.resolve(),
orchestrator: undefined,
privateAppsEnabled: false,
});
29 changes: 16 additions & 13 deletions apps/meteor/client/providers/AppsProvider/AppsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import React, { useEffect } from 'react';

import { AppClientOrchestratorInstance } from '../../apps/orchestrator';
import { AppsContext } from '../../contexts/AppsContext';
import { useIsEnterprise } from '../../hooks/useIsEnterprise';
import { useInvalidateLicense } from '../../hooks/useLicense';
import { useInvalidateLicense, useLicense } from '../../hooks/useLicense';
import type { AsyncState } from '../../lib/asyncState';
import { AsyncStatePhase } from '../../lib/asyncState';
import { useInvalidateAppsCountQueryCallback } from '../../views/marketplace/hooks/useAppsCountQuery';
Expand Down Expand Up @@ -36,8 +35,8 @@ const AppsProvider = ({ children }: AppsProviderProps) => {

const queryClient = useQueryClient();

const { data } = useIsEnterprise();
const isEnterprise = !!data?.isEnterprise;
const { isLoading: isLicenseInformationLoading, data: { license, limits } = {} } = useLicense({ loadValues: true });
const isEnterprise = isLicenseInformationLoading ? undefined : !!license;

const invalidateAppsCountQuery = useInvalidateAppsCountQueryCallback();
const invalidateLicenseQuery = useInvalidateLicense();
Expand Down Expand Up @@ -95,25 +94,29 @@ const AppsProvider = ({ children }: AppsProviderProps) => {
},
);

const store = useQuery(['marketplace', 'apps-stored', instance.data, marketplace.data], () => storeQueryFunction(marketplace, instance), {
enabled: marketplace.isFetched && instance.isFetched,
keepPreviousData: true,
});
const { isLoading: isMarketplaceDataLoading, data: marketplaceData } = useQuery(
['marketplace', 'apps-stored', instance.data, marketplace.data],
() => storeQueryFunction(marketplace, instance),
{
enabled: marketplace.isFetched && instance.isFetched,
keepPreviousData: true,
},
);

const [marketplaceAppsData, installedAppsData, privateAppsData] = store.data || [];
const { isLoading } = store;
const [marketplaceAppsData, installedAppsData, privateAppsData] = marketplaceData || [];

return (
<AppsContext.Provider
children={children}
value={{
installedApps: getAppState(isLoading, installedAppsData),
marketplaceApps: getAppState(isLoading, marketplaceAppsData),
privateApps: getAppState(isLoading, privateAppsData),
installedApps: getAppState(isMarketplaceDataLoading, installedAppsData),
marketplaceApps: getAppState(isMarketplaceDataLoading, marketplaceAppsData),
privateApps: getAppState(isMarketplaceDataLoading, privateAppsData),
reload: async () => {
await Promise.all([queryClient.invalidateQueries(['marketplace'])]);
},
orchestrator: AppClientOrchestratorInstance,
privateAppsEnabled: (limits?.privateApps?.max ?? 0) > 0,
}}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type FeatureUsageCardProps = {

export type CardProps = {
title: string;
infoText?: string;
infoText?: ReactNode;
upgradeButton?: ReactNode;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { IconButton } from '@rocket.chat/fuselage';
import { useSetModal } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import type { ReactElement, ReactNode } from 'react';
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';

import GenericModal from '../../../../components/GenericModal';

export type InfoTextIconModalProps = {
title: string;
infoText: string;
infoText: ReactNode;
};

const InfoTextIconModal = ({ title, infoText }: InfoTextIconModalProps): ReactElement => {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';

import AppsUsageCard from './AppsUsageCard';

const appRoot = mockAppRoot().withTranslations('en', 'core', {
Apps_InfoText_limited:
'Community workspaces can enable up to {{marketplaceAppsMaxCount}} marketplace apps. Private apps can only be enabled in <1>premium plans</1>.',
Apps_InfoText:
'Community allows up to {{privateAppsMaxCount}} private apps and {{marketplaceAppsMaxCount}} marketplace apps to be enabled',
});

it('should render a skeleton if no data', () => {
render(<AppsUsageCard />, { wrapper: appRoot.build(), legacyRoot: true });

expect(screen.getByRole('heading', { name: 'Apps' })).toBeInTheDocument();
expect(screen.getByRole('presentation')).toBeInTheDocument();
});

it('should render data as progress bars', async () => {
render(<AppsUsageCard privateAppsLimit={{ value: 1, max: 3 }} marketplaceAppsLimit={{ value: 2, max: 5 }} />, {
wrapper: appRoot.build(),
legacyRoot: true,
});

expect(screen.getByRole('heading', { name: 'Apps' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Click_here_for_more_info' })).toBeInTheDocument();

expect(screen.getByRole('progressbar', { name: 'Marketplace_apps' })).toBeInTheDocument();
expect(screen.getByRole('progressbar', { name: 'Marketplace_apps' })).toHaveAttribute('aria-valuenow', '40');
expect(screen.getByText('2 / 5')).toBeInTheDocument();

expect(screen.getByRole('progressbar', { name: 'Private_apps' })).toBeInTheDocument();
expect(screen.getByRole('progressbar', { name: 'Private_apps' })).toHaveAttribute('aria-valuenow', '33');
expect(screen.getByText('1 / 3')).toBeInTheDocument();

await userEvent.click(screen.getByRole('button', { name: 'Click_here_for_more_info' }));

expect(
screen.getByText('Community workspaces can enable up to 5 marketplace apps. Private apps can only be enabled in premium plans.'),
).toBeInTheDocument();
});

it('should render an upgrade button if marketplace apps reached 80% of the limit', async () => {
render(<AppsUsageCard privateAppsLimit={{ value: 1, max: 3 }} marketplaceAppsLimit={{ value: 4, max: 5 }} />, {
wrapper: appRoot.build(),
legacyRoot: true,
});

expect(screen.getByRole('heading', { name: 'Apps' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Click_here_for_more_info' })).toBeInTheDocument();

expect(screen.getByRole('button', { name: 'Upgrade' })).toBeInTheDocument();

await userEvent.click(screen.getByRole('button', { name: 'Click_here_for_more_info' }));

expect(
screen.getByText('Community workspaces can enable up to 5 marketplace apps. Private apps can only be enabled in premium plans.'),
).toBeInTheDocument();
});

it('should render a full progress bar with private apps disabled', async () => {
render(<AppsUsageCard privateAppsLimit={{ value: 0, max: 0 }} marketplaceAppsLimit={{ value: 2, max: 5 }} />, {
wrapper: appRoot.build(),
legacyRoot: true,
});

expect(screen.getByRole('heading', { name: 'Apps' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Click_here_for_more_info' })).toBeInTheDocument();

expect(screen.getByRole('progressbar', { name: 'Marketplace_apps' })).toBeInTheDocument();
expect(screen.getByRole('progressbar', { name: 'Marketplace_apps' })).toHaveAttribute('aria-valuenow', '40');
expect(screen.getByText('2 / 5')).toBeInTheDocument();

expect(screen.getByRole('progressbar', { name: 'Private_apps' })).toBeInTheDocument();
expect(screen.getByRole('progressbar', { name: 'Private_apps' })).toHaveAttribute('aria-valuenow', '100');
expect(screen.getByText('0 / 0')).toBeInTheDocument();

await userEvent.click(screen.getByRole('button', { name: 'Click_here_for_more_info' }));

expect(screen.getByText('Community allows up to 0 private apps and 5 marketplace apps to be enabled')).toBeInTheDocument();
});
Loading

0 comments on commit 9fa0824

Please sign in to comment.