mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(console): add new usage display for pro subscription plan (#6413)
This commit is contained in:
parent
1e4aa5ad9a
commit
2f40aaa773
7 changed files with 306 additions and 2 deletions
|
@ -0,0 +1,35 @@
|
||||||
|
@use '@/scss/underscore' as _;
|
||||||
|
|
||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: left;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--color-divider);
|
||||||
|
background: var(--color-layer-1);
|
||||||
|
padding: _.unit(5.5) _.unit(6);
|
||||||
|
gap: _.unit(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font: var(--font-title-3);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font: var(--font-title-3);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.usageTip {
|
||||||
|
font: var(--font-body-2);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
padding-top: 1px;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { type AdminConsoleKey } from '@logto/phrases';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import Tip from '@/assets/icons/tip.svg?react';
|
||||||
|
import DynamicT from '@/ds-components/DynamicT';
|
||||||
|
import IconButton from '@/ds-components/IconButton';
|
||||||
|
import Tag from '@/ds-components/Tag';
|
||||||
|
import TextLink from '@/ds-components/TextLink';
|
||||||
|
import { ToggleTip } from '@/ds-components/Tip';
|
||||||
|
|
||||||
|
import { formatNumber } from '../utils';
|
||||||
|
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
readonly usage: number | boolean;
|
||||||
|
readonly quota?: number;
|
||||||
|
readonly usageKey: AdminConsoleKey;
|
||||||
|
readonly titleKey: AdminConsoleKey;
|
||||||
|
readonly tooltipKey: AdminConsoleKey;
|
||||||
|
readonly className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ProPlanUsageCard({ usage, quota, usageKey, titleKey, tooltipKey, className }: Props) {
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(styles.card, className)}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
<span>
|
||||||
|
<DynamicT forKey={titleKey} />
|
||||||
|
</span>
|
||||||
|
<ToggleTip
|
||||||
|
content={
|
||||||
|
<Trans
|
||||||
|
components={{
|
||||||
|
a: <TextLink to="https://blog.logto.io/pricing-add-ons/" />,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t(tooltipKey)}
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconButton size="small">
|
||||||
|
<Tip />
|
||||||
|
</IconButton>
|
||||||
|
</ToggleTip>
|
||||||
|
</div>
|
||||||
|
{typeof usage === 'number' ? (
|
||||||
|
<div className={styles.description}>
|
||||||
|
<Trans
|
||||||
|
components={{
|
||||||
|
span: <span className={styles.usageTip} />,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t(usageKey, {
|
||||||
|
usage:
|
||||||
|
quota && typeof quota === 'number'
|
||||||
|
? `${formatNumber(usage)} / ${formatNumber(quota)}`
|
||||||
|
: formatNumber(usage),
|
||||||
|
})}
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<Tag className={styles.tag} type="state" status={usage ? 'success' : 'info'}>
|
||||||
|
<DynamicT
|
||||||
|
forKey={`subscription.usage.${usage ? 'status_active' : 'status_inactive'}`}
|
||||||
|
/>
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProPlanUsageCard;
|
|
@ -18,10 +18,27 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.newPricingModelUsage {
|
||||||
|
margin-top: _.unit(1);
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: _.unit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardItem {
|
||||||
|
flex: 0 0 calc(33.333% - _.unit(2) * 2);
|
||||||
|
max-width: calc(33.333% - _.unit(2) * 2);
|
||||||
|
max-height: 112px;
|
||||||
|
}
|
||||||
|
|
||||||
.planCycle {
|
.planCycle {
|
||||||
font: var(--font-body-2);
|
font: var(--font-body-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.planCycleNewPricingModel {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.usageBar {
|
.usageBar {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: var(--color-layer-2);
|
background-color: var(--color-layer-2);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { conditional } from '@silverhand/essentials';
|
import { ReservedPlanId } from '@logto/schemas';
|
||||||
|
import { cond, conditional } from '@silverhand/essentials';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
@ -10,7 +11,9 @@ import DynamicT from '@/ds-components/DynamicT';
|
||||||
import { type SubscriptionPlan } from '@/types/subscriptions';
|
import { type SubscriptionPlan } from '@/types/subscriptions';
|
||||||
import { formatPeriod } from '@/utils/subscription';
|
import { formatPeriod } from '@/utils/subscription';
|
||||||
|
|
||||||
|
import ProPlanUsageCard, { type Props as ProPlanUsageCardProps } from './ProPlanUsageCard';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
import { usageKeys, usageKeyMap, titleKeyMap, tooltipKeyMap } from './utils';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
|
@ -38,7 +41,40 @@ function PlanUsage({ subscriptionUsage, currentSubscription, currentPlan }: Prop
|
||||||
|
|
||||||
const usagePercent = conditional(mauLimit && activeUsers / mauLimit);
|
const usagePercent = conditional(mauLimit && activeUsers / mauLimit);
|
||||||
|
|
||||||
return (
|
const usages: ProPlanUsageCardProps[] = usageKeys.map((key) => ({
|
||||||
|
usage: currentSubscriptionUsage[key],
|
||||||
|
usageKey: `subscription.usage.${usageKeyMap[key]}`,
|
||||||
|
titleKey: `subscription.usage.${titleKeyMap[key]}`,
|
||||||
|
tooltipKey: `subscription.usage.${tooltipKeyMap[key]}`,
|
||||||
|
...cond(
|
||||||
|
key === 'tokenLimit' &&
|
||||||
|
currentSubscriptionQuota.tokenLimit && { quota: currentSubscriptionQuota.tokenLimit }
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return isDevFeaturesEnabled &&
|
||||||
|
currentSubscriptionFromNewPricingModel.planId === ReservedPlanId.Pro ? (
|
||||||
|
<div>
|
||||||
|
<div className={classNames(styles.planCycle, styles.planCycleNewPricingModel)}>
|
||||||
|
<DynamicT
|
||||||
|
forKey="subscription.plan_cycle"
|
||||||
|
interpolation={{
|
||||||
|
period: formatPeriod({
|
||||||
|
periodStart: currentPeriodStart,
|
||||||
|
periodEnd: currentPeriodEnd,
|
||||||
|
}),
|
||||||
|
renewDate: dayjs(currentPeriodEnd).add(1, 'day').format('MMM D, YYYY'),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.newPricingModelUsage}>
|
||||||
|
{usages.map((props, index) => (
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
<ProPlanUsageCard key={index} className={styles.cardItem} {...props} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.usage}>
|
<div className={styles.usage}>
|
||||||
{`${activeUsers} / `}
|
{`${activeUsers} / `}
|
||||||
|
|
77
packages/console/src/components/PlanUsage/utils.ts
Normal file
77
packages/console/src/components/PlanUsage/utils.ts
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import { type TFuncKey } from 'i18next';
|
||||||
|
|
||||||
|
import { type NewSubscriptionQuota } from '@/cloud/types/router';
|
||||||
|
|
||||||
|
type UsageKey = Pick<
|
||||||
|
NewSubscriptionQuota,
|
||||||
|
| 'mauLimit'
|
||||||
|
| 'organizationsEnabled'
|
||||||
|
| 'mfaEnabled'
|
||||||
|
| 'enterpriseSsoLimit'
|
||||||
|
| 'resourcesLimit'
|
||||||
|
| 'machineToMachineLimit'
|
||||||
|
| 'tenantMembersLimit'
|
||||||
|
| 'tokenLimit'
|
||||||
|
| 'hooksLimit'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const usageKeys: Array<keyof UsageKey> = [
|
||||||
|
'mauLimit',
|
||||||
|
'organizationsEnabled',
|
||||||
|
'mfaEnabled',
|
||||||
|
'enterpriseSsoLimit',
|
||||||
|
'resourcesLimit',
|
||||||
|
'machineToMachineLimit',
|
||||||
|
'tenantMembersLimit',
|
||||||
|
'tokenLimit',
|
||||||
|
'hooksLimit',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const usageKeyMap: Record<
|
||||||
|
keyof UsageKey,
|
||||||
|
TFuncKey<'translation', 'admin_console.subscription.usage'>
|
||||||
|
> = {
|
||||||
|
mauLimit: 'mau.description',
|
||||||
|
organizationsEnabled: 'organizations.description',
|
||||||
|
mfaEnabled: 'mfa.description',
|
||||||
|
enterpriseSsoLimit: 'enterprise_sso.description',
|
||||||
|
resourcesLimit: 'api_resources.description',
|
||||||
|
machineToMachineLimit: 'machine_to_machine.description',
|
||||||
|
tenantMembersLimit: 'tenant_members.description',
|
||||||
|
tokenLimit: 'tokens.description',
|
||||||
|
hooksLimit: 'hooks.description',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const titleKeyMap: Record<
|
||||||
|
keyof UsageKey,
|
||||||
|
TFuncKey<'translation', 'admin_console.subscription.usage'>
|
||||||
|
> = {
|
||||||
|
mauLimit: 'mau.title',
|
||||||
|
organizationsEnabled: 'organizations.title',
|
||||||
|
mfaEnabled: 'mfa.title',
|
||||||
|
enterpriseSsoLimit: 'enterprise_sso.title',
|
||||||
|
resourcesLimit: 'api_resources.title',
|
||||||
|
machineToMachineLimit: 'machine_to_machine.title',
|
||||||
|
tenantMembersLimit: 'tenant_members.title',
|
||||||
|
tokenLimit: 'tokens.title',
|
||||||
|
hooksLimit: 'hooks.title',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tooltipKeyMap: Record<
|
||||||
|
keyof UsageKey,
|
||||||
|
TFuncKey<'translation', 'admin_console.subscription.usage'>
|
||||||
|
> = {
|
||||||
|
mauLimit: 'mau.tooltip',
|
||||||
|
organizationsEnabled: 'organizations.tooltip',
|
||||||
|
mfaEnabled: 'mfa.tooltip',
|
||||||
|
enterpriseSsoLimit: 'enterprise_sso.tooltip',
|
||||||
|
resourcesLimit: 'api_resources.tooltip',
|
||||||
|
machineToMachineLimit: 'machine_to_machine.tooltip',
|
||||||
|
tenantMembersLimit: 'tenant_members.tooltip',
|
||||||
|
tokenLimit: 'tokens.tooltip',
|
||||||
|
hooksLimit: 'hooks.tooltip',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatNumber = (number: number): string => {
|
||||||
|
return number.toString().replaceAll(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||||
|
};
|
|
@ -1,5 +1,6 @@
|
||||||
import quota_item from './quota-item.js';
|
import quota_item from './quota-item.js';
|
||||||
import quota_table from './quota-table.js';
|
import quota_table from './quota-table.js';
|
||||||
|
import usage from './usage.js';
|
||||||
|
|
||||||
const subscription = {
|
const subscription = {
|
||||||
free_plan: 'Free plan',
|
free_plan: 'Free plan',
|
||||||
|
@ -64,6 +65,7 @@ const subscription = {
|
||||||
downgrade_success: 'Successfully downgraded to <name/>',
|
downgrade_success: 'Successfully downgraded to <name/>',
|
||||||
subscription_check_timeout: 'Subscription check timed out. Please refresh later.',
|
subscription_check_timeout: 'Subscription check timed out. Please refresh later.',
|
||||||
no_subscription: 'No subscription',
|
no_subscription: 'No subscription',
|
||||||
|
usage,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Object.freeze(subscription);
|
export default Object.freeze(subscription);
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
const usage = {
|
||||||
|
status_active: 'On',
|
||||||
|
status_inactive: 'Off',
|
||||||
|
mau: {
|
||||||
|
title: 'MAU',
|
||||||
|
description: '{{usage}}',
|
||||||
|
tooltip:
|
||||||
|
'A MAU is a unique user who has exchanged at least one token with Logto within a billing cycle. Unlimited for the Pro Plan. <a>Learn more</a>',
|
||||||
|
},
|
||||||
|
organizations: {
|
||||||
|
title: 'Organizations',
|
||||||
|
description: '{{usage}}',
|
||||||
|
tooltip:
|
||||||
|
'Add-on feature with a flat rate of ${{price, number}} per month. Price is not affected by the number of organizations or their activity level.',
|
||||||
|
},
|
||||||
|
mfa: {
|
||||||
|
title: 'MFA',
|
||||||
|
description: '{{usage}}',
|
||||||
|
tooltip:
|
||||||
|
'Add-on feature with a flat rate of ${{price, number}} per month. Price is not affected by the number of authentication factors used.',
|
||||||
|
},
|
||||||
|
enterprise_sso: {
|
||||||
|
title: 'Enterprise SSO',
|
||||||
|
description: '{{usage}}',
|
||||||
|
tooltip: 'Add-on feature with a price of ${{price, number}} per SSO connection per month.',
|
||||||
|
},
|
||||||
|
api_resources: {
|
||||||
|
title: 'API resources',
|
||||||
|
description: '{{usage}} <span>(Free for the first 3)</span>',
|
||||||
|
tooltip:
|
||||||
|
'Add-on feature priced at ${{price, number}} per resource per month. The first 3 API resources are free.',
|
||||||
|
},
|
||||||
|
machine_to_machine: {
|
||||||
|
title: 'Machine-to-machine',
|
||||||
|
description: '{{usage}} <span>(Free for the first 1)</span>',
|
||||||
|
tooltip:
|
||||||
|
'Add-on feature priced at ${{price, number}} per app per month. The first machine-to-machine app is free.',
|
||||||
|
},
|
||||||
|
tenant_members: {
|
||||||
|
title: 'Tenant members',
|
||||||
|
description: '{{usage}} <span>(Free for the first 3)</span>',
|
||||||
|
tooltip:
|
||||||
|
'Add-on feature priced at ${{price, number}} per member per month. The first 3 tenant members are free.',
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
title: 'Tokens',
|
||||||
|
description: '{{usage}}',
|
||||||
|
tooltip:
|
||||||
|
'Add-on feature priced at ${{price, number}} per million tokens. The first 1 million tokens is included.',
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
title: 'Hooks',
|
||||||
|
description: '{{usage}} <span>(Free for the first 10)</span>',
|
||||||
|
tooltip:
|
||||||
|
'Add-on feature priced at ${{price, number}} per hook. The first 10 hooks are included.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Object.freeze(usage);
|
Loading…
Reference in a new issue