1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-18 00:19:49 +01:00

Refactor premium feature (#2627)

https://linear.app/unleash/issue/2-491/improve-premiumfeature-component-and-how-its-implemented


![image](https://user-images.githubusercontent.com/14320932/206237837-6032c3c0-1e42-4be2-8b6f-223e0204e1b5.png)

Refactors `PremiumFeature` to be a bit more straightforward and match
new designs, both in standalone and tooltip modes.
This also fixes the fact that the change requests tab did not display
the premium feature info, along with other smaller fixes.

Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai>
This commit is contained in:
Nuno Góis 2022-12-08 11:42:54 +00:00 committed by GitHub
parent fb2c30244c
commit 1be2483e6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 151 additions and 120 deletions

View File

@ -271,7 +271,7 @@ export const ChangeRequestsTabs = ({
} }
elseShow={ elseShow={
<TablePlaceholder> <TablePlaceholder>
None of the changes where submitted yet. None of the changes were submitted yet.
</TablePlaceholder> </TablePlaceholder>
} }
/> />

View File

@ -5,12 +5,17 @@ import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject';
import { ChangeRequestsTabs } from './ChangeRequestsTabs/ChangeRequestsTabs'; import { ChangeRequestsTabs } from './ChangeRequestsTabs/ChangeRequestsTabs';
import { SortingRule } from 'react-table'; import { SortingRule } from 'react-table';
import { useProjectChangeRequests } from 'hooks/api/getters/useProjectChangeRequests/useProjectChangeRequests'; import { useProjectChangeRequests } from 'hooks/api/getters/useProjectChangeRequests/useProjectChangeRequests';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
const defaultSort: SortingRule<string> = { id: 'updatedAt', desc: true }; const defaultSort: SortingRule<string> = { id: 'updatedAt', desc: true };
export const ProjectChangeRequests = () => { export const ProjectChangeRequests = () => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const projectName = useProjectNameOrId(projectId); const projectName = useProjectNameOrId(projectId);
const { isOss, isPro } = useUiConfig();
usePageTitle(`Change requests ${projectName}`); usePageTitle(`Change requests ${projectName}`);
@ -21,6 +26,14 @@ export const ProjectChangeRequests = () => {
defaultSort defaultSort
); );
if (isOss() || isPro()) {
return (
<PageContent sx={{ justifyContent: 'center' }}>
<PremiumFeature feature="Change Requests" />
</PageContent>
);
}
return ( return (
<ChangeRequestsTabs <ChangeRequestsTabs
changeRequests={changeRequests} changeRequests={changeRequests}

View File

@ -1,86 +1,152 @@
import { ReactComponent as ProPlanIcon } from 'assets/icons/pro-enterprise-feature-badge.svg'; import { ReactComponent as ProPlanIcon } from 'assets/icons/pro-enterprise-feature-badge.svg';
import { Box, Link, styled, Typography } from '@mui/material'; import { Box, Button, Link, styled, Typography } from '@mui/material';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
const PremiumFeatureWrapper = styled(Box)(({ theme }) => ({ const PremiumFeatureWrapper = styled(Box, {
shouldForwardProp: prop => prop !== 'tooltip',
})<{ tooltip?: boolean }>(({ theme, tooltip }) => ({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
padding: theme.spacing(1, 0.5), alignItems: tooltip ? 'start' : 'center',
textAlign: tooltip ? 'left' : 'center',
backgroundColor: tooltip ? 'transparent' : theme.palette.secondaryContainer,
borderRadius: tooltip ? 0 : theme.shape.borderRadiusLarge,
padding: tooltip ? theme.spacing(1, 0.5) : theme.spacing(7.5, 1),
})); }));
const StyledTitle = styled(Typography)(({ theme }) => ({ const StyledTitle = styled(Typography)(({ theme }) => ({
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
fontWeight: theme.fontWeight.bold, fontWeight: theme.fontWeight.bold,
fontSize: theme.fontSizes.smallBody,
gap: theme.spacing(1), gap: theme.spacing(1),
})); }));
const StyledBody = styled(Typography)(({ theme }) => ({ const StyledBody = styled('div', {
shouldForwardProp: prop => prop !== 'tooltip',
})<{ tooltip?: boolean }>(({ theme, tooltip }) => ({
margin: tooltip ? theme.spacing(1, 0) : theme.spacing(3, 0, 5, 0),
}));
const StyledTypography = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.smallBody, fontSize: theme.fontSizes.smallBody,
margin: theme.spacing(1, 0), }));
const StyledButtonContainer = styled('div')(() => ({
display: 'flex',
})); }));
const StyledLink = styled(Link)(({ theme }) => ({ const StyledLink = styled(Link)(({ theme }) => ({
fontSize: theme.fontSizes.smallBody, fontSize: theme.fontSizes.smallBody,
width: 'fit-content',
})); }));
enum FeatureLevelTitle { enum FeaturePlan {
PRO = 'Pro & Enterprise feature', PRO = 'Pro & Enterprise',
ENTERPRISE = 'Enterprise feature', ENTERPRISE = 'Enterprise',
} }
export enum PlausibleOrigin { const PremiumFeatures = {
PROJECT = 'Projects', ['Adding new projects']: {
ACCESS = 'Access', plan: FeaturePlan.PRO,
CHANGE_REQUEST = 'Change Request', url: '',
} },
['Access']: {
plan: FeaturePlan.PRO,
url: 'https://docs.getunleash.io/reference/rbac',
},
['Change Requests']: {
plan: FeaturePlan.ENTERPRISE,
url: 'https://docs.getunleash.io/reference/change-requests',
},
};
type PremiumFeature = keyof typeof PremiumFeatures;
const UPGRADE_URL = 'https://www.getunleash.io/plans';
export interface PremiumFeatureProps { export interface PremiumFeatureProps {
children: React.ReactNode; feature: PremiumFeature;
origin?: PlausibleOrigin; tooltip?: boolean;
center?: boolean;
enterpriseOnly?: boolean;
} }
export const PremiumFeature = ({ export const PremiumFeature = ({ feature, tooltip }: PremiumFeatureProps) => {
children, const { url, plan } = PremiumFeatures[feature];
origin,
center,
enterpriseOnly = false,
}: PremiumFeatureProps) => {
const tracker = usePlausibleTracker(); const tracker = usePlausibleTracker();
const handleClick = () => { const handleClick = () => {
if (origin) { tracker.trackEvent('upgrade_plan_clicked', {
tracker.trackEvent('upgrade_plan_clicked', { props: { feature },
props: { origin }, });
});
}
}; };
const featureLabel = Boolean(url) ? (
<StyledLink href={url} target="_blank">
{feature}
</StyledLink>
) : (
feature
);
const featureMessage = (
<>
{featureLabel} is a feature available for the{' '}
<strong>{plan}</strong>{' '}
{plan === FeaturePlan.PRO ? 'plans' : 'plan'}
</>
);
return ( return (
<PremiumFeatureWrapper <PremiumFeatureWrapper tooltip={tooltip}>
sx={{
alignItems: center ? 'center' : 'start',
textAlign: center ? 'center' : 'left',
}}
>
<StyledTitle> <StyledTitle>
<ProPlanIcon /> <ProPlanIcon />
{enterpriseOnly {`${plan} feature`}
? FeatureLevelTitle.ENTERPRISE
: FeatureLevelTitle.PRO}
</StyledTitle> </StyledTitle>
<StyledBody>{children}</StyledBody> <ConditionallyRender
<StyledLink condition={Boolean(tooltip)}
href={'https://www.getunleash.io/plans'} show={
target="_blank" <>
onClick={handleClick} <StyledBody tooltip>
> <StyledTypography>
Upgrade now {featureMessage}. You need to upgrade your plan
</StyledLink> if you want to use it
</StyledTypography>
</StyledBody>
<StyledButtonContainer>
<StyledLink
href={UPGRADE_URL}
target="_blank"
onClick={handleClick}
>
Upgrade now
</StyledLink>
</StyledButtonContainer>
</>
}
elseShow={
<>
<StyledBody>
<StyledTypography>
{featureMessage}
</StyledTypography>
<StyledTypography>
You need to upgrade your plan if you want to use
it
</StyledTypography>
</StyledBody>
<StyledButtonContainer>
<Button
variant="outlined"
href={UPGRADE_URL}
target="_blank"
onClick={handleClick}
>
Upgrade now
</Button>
</StyledButtonContainer>
</>
}
/>
</PremiumFeatureWrapper> </PremiumFeatureWrapper>
); );
}; };

View File

@ -1,7 +1,7 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { PageContent } from 'component/common/PageContent/PageContent'; import { PageContent } from 'component/common/PageContent/PageContent';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { Alert, Link, styled } from '@mui/material'; import { Alert } from '@mui/material';
import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { PageHeader } from 'component/common/PageHeader/PageHeader';
import AccessContext from 'contexts/AccessContext'; import AccessContext from 'contexts/AccessContext';
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions'; import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
@ -9,15 +9,8 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { usePageTitle } from 'hooks/usePageTitle'; import { usePageTitle } from 'hooks/usePageTitle';
import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject'; import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject';
import { ChangeRequestTable } from './ChangeRequestTable'; import { ChangeRequestTable } from './ChangeRequestTable';
import { import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
PlausibleOrigin, import { ChangeRequestProcessHelp } from './ChangeRequestProcessHelp/ChangeRequestProcessHelp';
PremiumFeature,
} from 'component/common/PremiumFeature/PremiumFeature';
const StyledLink = styled(Link)(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
width: 'fit-content',
}));
export const ChangeRequestConfiguration = () => { export const ChangeRequestConfiguration = () => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
@ -25,37 +18,29 @@ export const ChangeRequestConfiguration = () => {
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
const { isOss, isPro } = useUiConfig(); const { isOss, isPro } = useUiConfig();
usePageTitle(`Project change request ${projectName}`); usePageTitle(`Project change request configuration ${projectName}`);
if (isOss() || isPro()) { if (isOss() || isPro()) {
return ( return (
<PageContent <PageContent
header={<PageHeader title="Change request configuration" />} header={
<PageHeader
titleElement="Change request configuration"
actions={<ChangeRequestProcessHelp />}
/>
}
sx={{ justifyContent: 'center' }} sx={{ justifyContent: 'center' }}
> >
<PremiumFeature <PremiumFeature feature="Change Requests" />
origin={PlausibleOrigin.CHANGE_REQUEST}
enterpriseOnly
center
>
<>
If you want to use{' '}
<StyledLink
href={'https://www.getunleash.io/plans'} // TODO: Add link to change request docs when available
target="_blank"
>
"Change Requests"
</StyledLink>{' '}
you will need to upgrade to Enterprise plan
</>
</PremiumFeature>
</PageContent> </PageContent>
); );
} }
if (!hasAccess(UPDATE_PROJECT, projectId)) { if (!hasAccess(UPDATE_PROJECT, projectId)) {
return ( return (
<PageContent header={<PageHeader title="Project access" />}> <PageContent
header={<PageHeader title="Change request configuration" />}
>
<Alert severity="error"> <Alert severity="error">
You need project owner permissions to access this section. You need project owner permissions to access this section.
</Alert> </Alert>

View File

@ -1,7 +1,7 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { PageContent } from 'component/common/PageContent/PageContent'; import { PageContent } from 'component/common/PageContent/PageContent';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { Alert, Box, Link, styled } from '@mui/material'; import { Alert } from '@mui/material';
import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { PageHeader } from 'component/common/PageHeader/PageHeader';
import AccessContext from 'contexts/AccessContext'; import AccessContext from 'contexts/AccessContext';
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions'; import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
@ -9,15 +9,7 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { usePageTitle } from 'hooks/usePageTitle'; import { usePageTitle } from 'hooks/usePageTitle';
import { ProjectAccessTable } from 'component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable'; import { ProjectAccessTable } from 'component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable';
import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject'; import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject';
import { import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
PlausibleOrigin,
PremiumFeature,
} from 'component/common/PremiumFeature/PremiumFeature';
const StyledLink = styled(Link)(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
width: 'fit-content',
}));
export const ProjectAccess = () => { export const ProjectAccess = () => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
@ -28,37 +20,18 @@ export const ProjectAccess = () => {
if (isOss()) { if (isOss()) {
return ( return (
<PageContent header={<PageHeader title="Project access" />}> <PageContent
<Box header={<PageHeader title="Access" />}
sx={{ sx={{ justifyContent: 'center' }}
display: 'inline-flex', >
maxWidth: '50%', <PremiumFeature feature="Access" />
margin: '0 25%',
}}
alignSelf={'center'}
>
<PremiumFeature origin={PlausibleOrigin.ACCESS} center>
<>
Controlling access to projects requires a paid
version of Unleash. Check out{' '}
<StyledLink
href="https://www.getunleash.io"
target="_blank"
rel="noreferrer"
>
getunleash.io
</StyledLink>{' '}
to find out more.
</>
</PremiumFeature>
</Box>
</PageContent> </PageContent>
); );
} }
if (!hasAccess(UPDATE_PROJECT, projectId)) { if (!hasAccess(UPDATE_PROJECT, projectId)) {
return ( return (
<PageContent header={<PageHeader title="Project access" />}> <PageContent header={<PageHeader title="Access" />}>
<Alert severity="error"> <Alert severity="error">
You need project owner permissions to access this section. You need project owner permissions to access this section.
</Alert> </Alert>

View File

@ -20,10 +20,7 @@ import { TablePlaceholder } from 'component/common/Table';
import { useMediaQuery } from '@mui/material'; import { useMediaQuery } from '@mui/material';
import theme from 'themes/theme'; import theme from 'themes/theme';
import { Search } from 'component/common/Search/Search'; import { Search } from 'component/common/Search/Search';
import { import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
PlausibleOrigin,
PremiumFeature,
} from 'component/common/PremiumFeature/PremiumFeature';
import { ITooltipResolverProps } from 'component/common/TooltipResolver/TooltipResolver'; import { ITooltipResolverProps } from 'component/common/TooltipResolver/TooltipResolver';
import { ReactComponent as ProPlanIcon } from 'assets/icons/pro-enterprise-feature-badge.svg'; import { ReactComponent as ProPlanIcon } from 'assets/icons/pro-enterprise-feature-badge.svg';
@ -48,10 +45,7 @@ function resolveCreateButtonData(
disabled: true, disabled: true,
tooltip: { tooltip: {
titleComponent: ( titleComponent: (
<PremiumFeature origin={PlausibleOrigin.PROJECT}> <PremiumFeature feature="Adding new projects" tooltip />
To be able to add more projects you need to upgrade to
Pro or Enterprise plan
</PremiumFeature>
), ),
sx: { maxWidth: '320px' }, sx: { maxWidth: '320px' },
variant: 'custom', variant: 'custom',