0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-24 22:41:28 -05:00

fix(console): fix add-on console issues

This commit is contained in:
Darcy Ye 2024-08-15 20:33:07 +08:00
parent 0004d682b0
commit bc8317706f
No known key found for this signature in database
GPG key ID: B46F4C07EDEFC610
5 changed files with 90 additions and 104 deletions
packages/console/src
components/PlanUsage
pages/TenantSettings/Subscription

View file

@ -22,6 +22,10 @@
.description {
font: var(--font-title-3);
color: var(--color-text);
&.quotaExceeded {
color: var(--color-danger-default);
}
}
.usageTip {

View file

@ -1,4 +1,5 @@
import { type AdminConsoleKey } from '@logto/phrases';
import { conditional, type Nullable } from '@silverhand/essentials';
import classNames from 'classnames';
import { Trans, useTranslation } from 'react-i18next';
@ -15,15 +16,15 @@ import styles from './index.module.scss';
export type Props = {
readonly usage: number | boolean;
readonly quota?: number;
readonly quota?: Nullable<number>;
readonly usageKey: AdminConsoleKey;
readonly titleKey: AdminConsoleKey;
readonly tooltipKey: AdminConsoleKey;
readonly tooltipKey?: AdminConsoleKey;
readonly unitPrice: number;
readonly className?: string;
};
function ProPlanUsageCard({
function PlanUsageCard({
usage,
quota,
unitPrice,
@ -34,32 +35,43 @@ function ProPlanUsageCard({
}: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const usagePercent = conditional(
typeof quota === 'number' && typeof usage === 'number' && usage / quota
);
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, {
price: unitPrice,
})}
</Trans>
}
>
<IconButton size="small">
<Tip />
</IconButton>
</ToggleTip>
{tooltipKey && (
<ToggleTip
content={
<Trans
components={{
a: <TextLink to="https://blog.logto.io/pricing-add-ons/" />,
}}
>
{t(tooltipKey, {
price: unitPrice,
})}
</Trans>
}
>
<IconButton size="small">
<Tip />
</IconButton>
</ToggleTip>
)}
</div>
{typeof usage === 'number' ? (
<div className={styles.description}>
<div
className={classNames(
styles.description,
typeof usagePercent === 'number' && usagePercent >= 1 && styles.quotaExceeded
)}
>
<Trans
components={{
span: <span className={styles.usageTip} />,
@ -67,9 +79,13 @@ function ProPlanUsageCard({
>
{t(usageKey, {
usage:
quota && typeof quota === 'number'
? `${formatNumber(usage)} / ${formatNumber(quota)}`
: formatNumber(usage),
quota === undefined
? formatNumber(usage)
: typeof quota === 'number'
? `${formatNumber(usage)} / ${formatNumber(quota)}${
usagePercent === undefined ? '' : ` (${(usagePercent * 100).toFixed(0)}%)`
}`
: `${formatNumber(usage)} / ${String(t('subscription.quota_table.unlimited'))}`,
})}
</Trans>
</div>
@ -86,4 +102,4 @@ function ProPlanUsageCard({
);
}
export default ProPlanUsageCard;
export default PlanUsageCard;

View file

@ -9,11 +9,6 @@
> div:not(:first-child) {
margin-top: _.unit(4);
}
&.freeUser {
flex: 0 0 calc((100% - _.unit(2)) / 2);
max-width: calc((100% - _.unit(2)) / 2);
}
}
.usage {
@ -34,6 +29,11 @@
flex: 0 0 calc((100% - _.unit(2) * 2) / 3);
max-width: calc((100% - _.unit(2) * 2) / 3);
max-height: 112px;
&.freeUser {
flex: 0 0 calc((100% - _.unit(2)) / 2);
max-width: calc((100% - _.unit(2)) / 2);
}
}
.planCycle {

View file

@ -12,7 +12,7 @@ import DynamicT from '@/ds-components/DynamicT';
import { type SubscriptionPlan } from '@/types/subscriptions';
import { formatPeriod } from '@/utils/subscription';
import ProPlanUsageCard, { type Props as ProPlanUsageCardProps } from './ProPlanUsageCard';
import PlanUsageCard, { type Props as PlanUsageCardProps } from './PlanUsageCard';
import styles from './index.module.scss';
import { usageKeys, usageKeyPriceMap, usageKeyMap, titleKeyMap, tooltipKeyMap } from './utils';
@ -56,28 +56,36 @@ function PlanUsage({ currentSubscription, currentPlan, periodicUsage: rawPeriodi
periodicUsage.mauLimit,
isDevFeaturesEnabled ? currentSubscriptionQuota.mauLimit : currentPlan.quota.mauLimit,
];
const [tokenUsage, tokenLimit] = [periodicUsage.tokenLimit, currentSubscriptionQuota.tokenLimit];
const mauUsagePercent = conditional(mauLimit && activeUsers / mauLimit) ?? 0;
const tokenUsagePercent = conditional(tokenLimit && tokenUsage / tokenLimit) ?? 0;
const mauUsagePercent = conditional(mauLimit && activeUsers / mauLimit);
const usages: ProPlanUsageCardProps[] = usageKeys.map((key) => ({
usage:
key === 'mauLimit' || key === 'tokenLimit'
? periodicUsage[key]
: currentSubscriptionUsage[key],
usageKey: `subscription.usage.${usageKeyMap[key]}`,
titleKey: `subscription.usage.${titleKeyMap[key]}`,
tooltipKey: `subscription.usage.${tooltipKeyMap[key]}`,
unitPrice: usageKeyPriceMap[key],
...cond(
key === 'tokenLimit' &&
currentSubscriptionQuota.tokenLimit && { quota: currentSubscriptionQuota.tokenLimit }
),
}));
const usages: PlanUsageCardProps[] = usageKeys
// Show all usages for Pro plan and only show MAU and token usage for Free plan
.filter(
(key) =>
currentSubscriptionFromNewPricingModel.planId === ReservedPlanId.Pro ||
(currentSubscriptionFromNewPricingModel.planId === ReservedPlanId.Free &&
(key === 'mauLimit' || key === 'tokenLimit'))
)
.map((key) => ({
usage:
key === 'mauLimit' || key === 'tokenLimit'
? periodicUsage[key]
: currentSubscriptionUsage[key],
usageKey: `subscription.usage.${usageKeyMap[key]}`,
titleKey: `subscription.usage.${titleKeyMap[key]}`,
unitPrice: usageKeyPriceMap[key],
...conditional(
currentSubscriptionFromNewPricingModel.planId === ReservedPlanId.Pro && {
tooltipKey: `subscription.usage.${tooltipKeyMap[key]}`,
}
),
...cond(
(key === 'tokenLimit' || key === 'mauLimit') && { quota: currentSubscriptionQuota[key] }
),
}));
return isDevFeaturesEnabled &&
currentSubscriptionFromNewPricingModel.planId === ReservedPlanId.Pro ? (
return isDevFeaturesEnabled ? (
<div>
<div className={classNames(styles.planCycle, styles.planCycleNewPricingModel)}>
<DynamicT
@ -93,62 +101,19 @@ function PlanUsage({ currentSubscription, currentPlan, periodicUsage: rawPeriodi
</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} />
<PlanUsageCard
// eslint-disable-next-line react/no-array-index-key
key={index}
className={classNames(
styles.cardItem,
currentSubscriptionFromNewPricingModel.planId === ReservedPlanId.Free &&
styles.freeUser
)}
{...props}
/>
))}
</div>
</div>
) : isDevFeaturesEnabled ? (
<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}>
<div className={classNames(styles.container, styles.freeUser)}>
<div className={styles.usage}>
{`${activeUsers} / `}
{mauLimit === null ? (
<DynamicT forKey="subscription.quota_table.unlimited" />
) : (
mauLimit.toLocaleString()
)}
{` MAU (${(mauUsagePercent * 100).toFixed(2)}%)`}
</div>
<div className={styles.usageBar}>
<div
className={classNames(styles.usageBarInner, mauUsagePercent >= 1 && styles.overuse)}
style={{ width: `${Math.min(mauUsagePercent, 1) * 100}%` }}
/>
</div>
</div>
<div className={classNames(styles.container, styles.freeUser)}>
<div className={styles.usage}>
{`${tokenUsage} / `}
{tokenLimit === null ? (
<DynamicT forKey="subscription.quota_table.unlimited" />
) : (
tokenLimit.toLocaleString()
)}
{` Token usage (${(tokenUsagePercent * 100).toFixed(2)}%)`}
</div>
<div className={styles.usageBar}>
<div
className={classNames(styles.usageBarInner, tokenUsagePercent >= 1 && styles.overuse)}
style={{ width: `${Math.min(tokenUsagePercent, 1) * 100}%` }}
/>
</div>
</div>
</div>
</div>
) : (
<div className={styles.container}>
<div className={styles.usage}>

View file

@ -4,11 +4,12 @@ import useSWR from 'swr';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
import PageMeta from '@/components/PageMeta';
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
import { Skeleton } from '@/containers/ConsoleContent/Sidebar';
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
import { TenantsContext } from '@/contexts/TenantsProvider';
import { pickupFeaturedLogtoSkus, pickupFeaturedPlans } from '@/utils/subscription';
import Skeleton from '../components/Skeleton';
import CurrentPlan from './CurrentPlan';
import PlanComparisonTable from './PlanComparisonTable';
import SwitchPlanActionBar from './SwitchPlanActionBar';