mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
refactor(console): update featured plan quota list (#5147)
This commit is contained in:
parent
8d5ff29e27
commit
f33ef7067f
32 changed files with 374 additions and 163 deletions
|
@ -0,0 +1,14 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
&.notCapable {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
&.capable {
|
||||
color: var(--color-on-success-container);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import { cond } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import Failed from '@/assets/icons/failed.svg';
|
||||
import Success from '@/assets/icons/success.svg';
|
||||
import QuotaListItem from '@/components/PlanQuotaList/QuotaListItem';
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
import Tag from '@/ds-components/Tag';
|
||||
import { type SubscriptionPlanQuota } from '@/types/subscriptions';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
quotaKey: keyof SubscriptionPlanQuota;
|
||||
quotaValue: SubscriptionPlanQuota[keyof SubscriptionPlanQuota];
|
||||
isAddOnQuota: boolean;
|
||||
isComingSoonTagVisible: boolean;
|
||||
};
|
||||
|
||||
function FeaturedQuotaItem({ quotaKey, quotaValue, isAddOnQuota, isComingSoonTagVisible }: Props) {
|
||||
const isNotCapable = quotaValue === 0 || quotaValue === false;
|
||||
const Icon = isNotCapable ? Failed : Success;
|
||||
|
||||
return (
|
||||
<QuotaListItem
|
||||
quotaKey={quotaKey}
|
||||
quotaValue={quotaValue}
|
||||
isAddOn={isAddOnQuota}
|
||||
icon={
|
||||
<Icon
|
||||
className={classNames(styles.icon, isNotCapable ? styles.notCapable : styles.capable)}
|
||||
/>
|
||||
}
|
||||
suffix={cond(
|
||||
isComingSoonTagVisible && (
|
||||
<Tag>
|
||||
<DynamicT forKey="general.coming_soon" />
|
||||
</Tag>
|
||||
)
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default FeaturedQuotaItem;
|
|
@ -0,0 +1,6 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.featuredQuotaList {
|
||||
flex: 1;
|
||||
padding-bottom: _.unit(8);
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import { ReservedPlanId } from '@logto/schemas';
|
||||
import { condArray } from '@silverhand/essentials';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import PlanQuotaList from '@/components/PlanQuotaList';
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { comingSoonQuotaKeys } from '@/consts/plan-quotas';
|
||||
import { quotaItemAddOnPhrasesMap } from '@/consts/quota-item-phrases';
|
||||
import {
|
||||
type SubscriptionPlanQuotaEntries,
|
||||
type SubscriptionPlan,
|
||||
type SubscriptionPlanQuota,
|
||||
} from '@/types/subscriptions';
|
||||
|
||||
import FeaturedQuotaItem from './FeaturedQuotaItem';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const featuredQuotaKeys = new Set<keyof SubscriptionPlanQuota>([
|
||||
'mauLimit',
|
||||
'machineToMachineLimit',
|
||||
// Todo @xiaoyijun [Pricing] Remove feature flag
|
||||
...condArray(!isDevFeaturesEnabled && 'standardConnectorsLimit'),
|
||||
'rolesLimit',
|
||||
'scopesPerRoleLimit',
|
||||
'mfaEnabled',
|
||||
'ssoEnabled',
|
||||
'organizationsEnabled',
|
||||
'auditLogsRetentionDays',
|
||||
]);
|
||||
|
||||
type Props = {
|
||||
plan: SubscriptionPlan;
|
||||
};
|
||||
|
||||
function FeaturedPlanQuotaList({ plan }: Props) {
|
||||
const { id: planId, quota } = plan;
|
||||
|
||||
const featuredEntries = useMemo(
|
||||
() =>
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
(Object.entries(quota) as SubscriptionPlanQuotaEntries).filter(([key]) =>
|
||||
featuredQuotaKeys.has(key)
|
||||
),
|
||||
[quota]
|
||||
);
|
||||
|
||||
return (
|
||||
<PlanQuotaList
|
||||
className={styles.featuredQuotaList}
|
||||
entries={featuredEntries}
|
||||
itemRenderer={(quotaKey, quotaValue) => (
|
||||
<FeaturedQuotaItem
|
||||
key={quotaKey}
|
||||
quotaKey={quotaKey}
|
||||
quotaValue={quotaValue}
|
||||
isAddOnQuota={
|
||||
// Todo @xiaoyijun [Pricing] Remove feature flag
|
||||
isDevFeaturesEnabled &&
|
||||
planId !== ReservedPlanId.Free &&
|
||||
Boolean(quotaItemAddOnPhrasesMap[quotaKey])
|
||||
}
|
||||
isComingSoonTagVisible={comingSoonQuotaKeys.includes(quotaKey)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default FeaturedPlanQuotaList;
|
|
@ -6,10 +6,8 @@ import { Trans, useTranslation } from 'react-i18next';
|
|||
import ArrowRight from '@/assets/icons/arrow-right.svg';
|
||||
import PlanDescription from '@/components/PlanDescription';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import PlanQuotaList from '@/components/PlanQuotaList';
|
||||
import { pricingLink } from '@/consts';
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { comingSoonQuotaKeys } from '@/consts/plan-quotas';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
import DangerousRaw from '@/ds-components/DangerousRaw';
|
||||
|
@ -17,12 +15,12 @@ import DynamicT from '@/ds-components/DynamicT';
|
|||
import TextLink from '@/ds-components/TextLink';
|
||||
import { type SubscriptionPlanQuota, type SubscriptionPlan } from '@/types/subscriptions';
|
||||
|
||||
import FeaturedPlanQuotaList from './FeaturedPlanQuotaList';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const featuredQuotaKeys: Array<keyof SubscriptionPlanQuota> = [
|
||||
'mauLimit',
|
||||
'machineToMachineLimit',
|
||||
'standardConnectorsLimit',
|
||||
'rolesLimit',
|
||||
'scopesPerRoleLimit',
|
||||
'mfaEnabled',
|
||||
|
@ -90,13 +88,7 @@ function PlanCardItem({ plan, onSelect }: Props) {
|
|||
</div>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<PlanQuotaList
|
||||
hasIcon
|
||||
quota={quota}
|
||||
featuredQuotaKeys={featuredQuotaKeys}
|
||||
comingSoonQuotaKeys={comingSoonQuotaKeys}
|
||||
className={styles.list}
|
||||
/>
|
||||
<FeaturedPlanQuotaList plan={plan} />
|
||||
{isFreePlan && isFreeTenantExceeded && (
|
||||
<div className={classNames(styles.tip, styles.exceedFreeTenantsTip)}>
|
||||
{t('free_tenants_limit', { count: maxFreeTenantLimit })}
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.item {
|
||||
margin-left: _.unit(4);
|
||||
font: var(--font-body-2);
|
||||
|
||||
&.withIcon {
|
||||
list-style-type: none;
|
||||
margin-left: unset;
|
||||
}
|
||||
|
||||
.itemContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: _.unit(2);
|
||||
|
||||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
&.notCapable {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
&.capable {
|
||||
color: var(--color-on-success-container);
|
||||
}
|
||||
}
|
||||
|
||||
.lineThrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
import { conditional } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import DescendArrow from '@/assets/icons/descend-arrow.svg';
|
||||
import Failed from '@/assets/icons/failed.svg';
|
||||
import Success from '@/assets/icons/success.svg';
|
||||
import {
|
||||
quotaItemUnlimitedPhrasesMap,
|
||||
quotaItemPhrasesMap,
|
||||
quotaItemLimitedPhrasesMap,
|
||||
} from '@/consts/quota-item-phrases';
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
import Tag from '@/ds-components/Tag';
|
||||
import { type SubscriptionPlanQuota } from '@/types/subscriptions';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
hasIcon?: boolean;
|
||||
quotaKey: keyof SubscriptionPlanQuota;
|
||||
quotaValue: SubscriptionPlanQuota[keyof SubscriptionPlanQuota];
|
||||
isDiffItem?: boolean;
|
||||
isComingSoonTagVisible?: boolean;
|
||||
};
|
||||
|
||||
function QuotaItem({ hasIcon, quotaKey, quotaValue, isDiffItem, isComingSoonTagVisible }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.subscription.quota_item' });
|
||||
const isUnlimited = quotaValue === null;
|
||||
const isNotCapable = quotaValue === 0 || quotaValue === false;
|
||||
const isLimited = Boolean(quotaValue);
|
||||
|
||||
const Icon = isNotCapable ? Failed : isDiffItem ? DescendArrow : Success;
|
||||
|
||||
return (
|
||||
<li className={classNames(styles.item, hasIcon && styles.withIcon)}>
|
||||
<span className={styles.itemContent}>
|
||||
{hasIcon && (
|
||||
<Icon
|
||||
className={classNames(styles.icon, isNotCapable ? styles.notCapable : styles.capable)}
|
||||
/>
|
||||
)}
|
||||
<span className={classNames(isDiffItem && isNotCapable && styles.lineThrough)}>
|
||||
{isUnlimited && <>{t(quotaItemUnlimitedPhrasesMap[quotaKey])}</>}
|
||||
{isNotCapable && <>{t(quotaItemPhrasesMap[quotaKey])}</>}
|
||||
{isLimited && (
|
||||
<>
|
||||
{t(
|
||||
quotaItemLimitedPhrasesMap[quotaKey],
|
||||
conditional(typeof quotaValue === 'number' && { count: quotaValue }) ?? {}
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
{isComingSoonTagVisible && (
|
||||
<Tag>
|
||||
<DynamicT forKey="general.coming_soon" />
|
||||
</Tag>
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default QuotaItem;
|
|
@ -0,0 +1,46 @@
|
|||
import { cond } from '@silverhand/essentials';
|
||||
|
||||
import {
|
||||
quotaItemUnlimitedPhrasesMap,
|
||||
quotaItemPhrasesMap,
|
||||
quotaItemLimitedPhrasesMap,
|
||||
quotaItemAddOnPhrasesMap,
|
||||
} from '@/consts/quota-item-phrases';
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
import { type SubscriptionPlanQuota } from '@/types/subscriptions';
|
||||
|
||||
const quotaItemPhraseKeyPrefix = 'subscription.quota_item';
|
||||
|
||||
type Props = {
|
||||
quotaKey: keyof SubscriptionPlanQuota;
|
||||
quotaValue: SubscriptionPlanQuota[keyof SubscriptionPlanQuota];
|
||||
isAddOn?: boolean;
|
||||
};
|
||||
|
||||
function QuotaItemPhrase({ quotaKey, quotaValue, isAddOn = false }: Props) {
|
||||
const isUnlimited = quotaValue === null;
|
||||
const isNotCapable = quotaValue === 0 || quotaValue === false;
|
||||
const isLimited = Boolean(quotaValue);
|
||||
|
||||
const limitedPhraseKey =
|
||||
cond(isAddOn && quotaItemAddOnPhrasesMap[quotaKey]) ?? quotaItemLimitedPhrasesMap[quotaKey];
|
||||
|
||||
const phraseKey =
|
||||
cond(isUnlimited && quotaItemUnlimitedPhrasesMap[quotaKey]) ??
|
||||
cond(isNotCapable && quotaItemPhrasesMap[quotaKey]) ??
|
||||
cond(isLimited && limitedPhraseKey);
|
||||
|
||||
if (!phraseKey) {
|
||||
// Should not happen
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DynamicT
|
||||
forKey={`${quotaItemPhraseKeyPrefix}.${phraseKey}`}
|
||||
interpolation={cond(isLimited && typeof quotaValue === 'number' && { count: quotaValue })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default QuotaItemPhrase;
|
|
@ -0,0 +1,19 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.quotaListItem {
|
||||
font: var(--font-body-2);
|
||||
// Add a margin to the left for the list item marker
|
||||
margin-left: _.unit(4);
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: _.unit(2);
|
||||
}
|
||||
|
||||
&.withIcon {
|
||||
list-style-type: none;
|
||||
// Unset a margin to the left for the list item marker
|
||||
margin-left: unset;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import classNames from 'classnames';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { type SubscriptionPlanQuota } from '@/types/subscriptions';
|
||||
|
||||
import QuotaItemPhrase from './QuotaItemPhrase';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
quotaKey: keyof SubscriptionPlanQuota;
|
||||
quotaValue: SubscriptionPlanQuota[keyof SubscriptionPlanQuota];
|
||||
icon?: ReactNode;
|
||||
suffix?: ReactNode;
|
||||
isAddOn?: boolean;
|
||||
phraseClassName?: string;
|
||||
};
|
||||
|
||||
function QuotaListItem({ icon, suffix, phraseClassName, ...rest }: Props) {
|
||||
return (
|
||||
<li className={classNames(styles.quotaListItem, icon && styles.withIcon)}>
|
||||
{/**
|
||||
* Add a `span` as a wrapper to apply the flex layout to the content.
|
||||
* If we apply the flex layout to the `li` directly, the `li` circle bullet will disappear.
|
||||
*/}
|
||||
<span className={styles.content}>
|
||||
{icon}
|
||||
<span className={phraseClassName}>
|
||||
<QuotaItemPhrase {...rest} />
|
||||
</span>
|
||||
{suffix}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default QuotaListItem;
|
|
@ -1,6 +1,6 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.list {
|
||||
.planQuotaList {
|
||||
margin-block: 0;
|
||||
padding-inline: 0;
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import classNames from 'classnames';
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, type ReactNode } from 'react';
|
||||
|
||||
import { planQuotaItemOrder } from '@/consts/plan-quotas';
|
||||
import {
|
||||
|
@ -8,55 +8,31 @@ import {
|
|||
} from '@/types/subscriptions';
|
||||
import { sortBy } from '@/utils/sort';
|
||||
|
||||
import QuotaItem from './QuotaItem';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
quota: Partial<SubscriptionPlanQuota>;
|
||||
featuredQuotaKeys?: Array<keyof SubscriptionPlanQuota>;
|
||||
comingSoonQuotaKeys?: Array<keyof SubscriptionPlanQuota>;
|
||||
entries: SubscriptionPlanQuotaEntries;
|
||||
itemRenderer: (
|
||||
quotaKey: keyof SubscriptionPlanQuota,
|
||||
quotaValue: SubscriptionPlanQuota[keyof SubscriptionPlanQuota]
|
||||
) => ReactNode;
|
||||
className?: string;
|
||||
isDiff?: boolean;
|
||||
hasIcon?: boolean;
|
||||
};
|
||||
|
||||
function PlanQuotaList({
|
||||
quota,
|
||||
featuredQuotaKeys,
|
||||
comingSoonQuotaKeys,
|
||||
isDiff,
|
||||
hasIcon,
|
||||
className,
|
||||
}: Props) {
|
||||
const items = useMemo(() => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const entries = Object.entries(quota) as SubscriptionPlanQuotaEntries;
|
||||
|
||||
const featuredEntries = featuredQuotaKeys
|
||||
? entries.filter(([key]) => featuredQuotaKeys.includes(key))
|
||||
: entries;
|
||||
|
||||
return featuredEntries
|
||||
.slice()
|
||||
.sort(([preQuotaKey], [nextQuotaKey]) =>
|
||||
sortBy(planQuotaItemOrder)(preQuotaKey, nextQuotaKey)
|
||||
);
|
||||
}, [quota, featuredQuotaKeys]);
|
||||
function PlanQuotaList({ entries, itemRenderer, className }: Props) {
|
||||
const sortedEntries = useMemo(
|
||||
() =>
|
||||
entries
|
||||
.slice()
|
||||
.sort(([preQuotaKey], [nextQuotaKey]) =>
|
||||
sortBy(planQuotaItemOrder)(preQuotaKey, nextQuotaKey)
|
||||
),
|
||||
[entries]
|
||||
);
|
||||
|
||||
return (
|
||||
<ul className={classNames(styles.list, className)}>
|
||||
{items.map(([quotaKey, quotaValue]) => (
|
||||
<QuotaItem
|
||||
key={quotaKey}
|
||||
isDiffItem={isDiff}
|
||||
quotaKey={quotaKey}
|
||||
quotaValue={quotaValue}
|
||||
hasIcon={hasIcon}
|
||||
isComingSoonTagVisible={
|
||||
(quotaValue === null || Boolean(quotaValue)) && comingSoonQuotaKeys?.includes(quotaKey)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<ul className={classNames(styles.planQuotaList, className)}>
|
||||
{sortedEntries.map(([quotaKey, quotaValue]) => itemRenderer(quotaKey, quotaValue))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -105,3 +105,12 @@ export const quotaItemNotEligiblePhrasesMap: Record<
|
|||
organizationsEnabled: 'organizations_enabled.not_eligible',
|
||||
ssoEnabled: 'sso_enabled.not_eligible',
|
||||
};
|
||||
|
||||
export const quotaItemAddOnPhrasesMap: Partial<
|
||||
Record<
|
||||
keyof SubscriptionPlanQuota,
|
||||
TFuncKey<'translation', 'admin_console.subscription.quota_item'>
|
||||
>
|
||||
> = {
|
||||
machineToMachineLimit: 'machine_to_machine_limit.add_on',
|
||||
};
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
&.notCapable {
|
||||
color: var(--color-error);
|
||||
}
|
||||
}
|
||||
|
||||
.lineThrough {
|
||||
text-decoration: line-through;
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { cond } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import DescendArrow from '@/assets/icons/descend-arrow.svg';
|
||||
import Failed from '@/assets/icons/failed.svg';
|
||||
import QuotaListItem from '@/components/PlanQuotaList/QuotaListItem';
|
||||
import { type SubscriptionPlanQuota } from '@/types/subscriptions';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
quotaKey: keyof SubscriptionPlanQuota;
|
||||
quotaValue: SubscriptionPlanQuota[keyof SubscriptionPlanQuota];
|
||||
isForDowngradeTargetPlan?: boolean;
|
||||
};
|
||||
|
||||
function DiffQuotaItem({ quotaKey, quotaValue, isForDowngradeTargetPlan }: Props) {
|
||||
const isNotCapable = quotaValue === 0 || quotaValue === false;
|
||||
const DowngradeStatusIcon = isNotCapable ? Failed : DescendArrow;
|
||||
|
||||
return (
|
||||
<QuotaListItem
|
||||
quotaKey={quotaKey}
|
||||
quotaValue={quotaValue}
|
||||
icon={cond(
|
||||
isForDowngradeTargetPlan && (
|
||||
<DowngradeStatusIcon
|
||||
className={classNames(styles.icon, isNotCapable && styles.notCapable)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
phraseClassName={cond(isNotCapable && styles.lineThrough)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default DiffQuotaItem;
|
|
@ -1,22 +1,23 @@
|
|||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import PlanName from '@/components/PlanName';
|
||||
import PlanQuotaList from '@/components/PlanQuotaList';
|
||||
import { type SubscriptionPlanQuota } from '@/types/subscriptions';
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
import {
|
||||
type SubscriptionPlanQuotaEntries,
|
||||
type SubscriptionPlanQuota,
|
||||
} from '@/types/subscriptions';
|
||||
|
||||
import DiffQuotaItem from './DiffQuotaItem';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
planName: string;
|
||||
quotaDiff: Partial<SubscriptionPlanQuota>;
|
||||
isTarget?: boolean;
|
||||
isDowngradeTargetPlan?: boolean;
|
||||
};
|
||||
|
||||
function PlanQuotaDiffCard({ planName, quotaDiff, isTarget = false }: Props) {
|
||||
const { t } = useTranslation(undefined, {
|
||||
keyPrefix: 'admin_console.subscription.downgrade_modal',
|
||||
});
|
||||
|
||||
function PlanQuotaDiffCard({ planName, quotaDiff, isDowngradeTargetPlan = false }: Props) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.title}>
|
||||
|
@ -25,10 +26,25 @@ function PlanQuotaDiffCard({ planName, quotaDiff, isTarget = false }: Props) {
|
|||
name: <PlanName name={planName} />,
|
||||
}}
|
||||
>
|
||||
{t(isTarget ? 'after' : 'before')}
|
||||
<DynamicT
|
||||
forKey={`subscription.downgrade_modal.${isDowngradeTargetPlan ? 'after' : 'before'}`}
|
||||
/>
|
||||
</Trans>
|
||||
</div>
|
||||
<PlanQuotaList isDiff quota={quotaDiff} hasIcon={isTarget} />
|
||||
<PlanQuotaList
|
||||
entries={
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
Object.entries(quotaDiff) as SubscriptionPlanQuotaEntries
|
||||
}
|
||||
itemRenderer={(quotaKey, quotaValue) => (
|
||||
<DiffQuotaItem
|
||||
key={quotaKey}
|
||||
quotaKey={quotaKey}
|
||||
quotaValue={quotaValue}
|
||||
isForDowngradeTargetPlan={isDowngradeTargetPlan}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -57,7 +57,11 @@ function DowngradeConfirmModalContent({ currentPlan, targetPlan }: Props) {
|
|||
</div>
|
||||
<div className={styles.content}>
|
||||
<PlanQuotaDiffCard planName={currentPlanName} quotaDiff={currentQuotaDiff} />
|
||||
<PlanQuotaDiffCard isTarget planName={targetPlanName} quotaDiff={targetQuotaDiff} />
|
||||
<PlanQuotaDiffCard
|
||||
isDowngradeTargetPlan
|
||||
planName={targetPlanName}
|
||||
quotaDiff={targetQuotaDiff}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -32,6 +32,8 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} Maschine-zu-Maschine-Apps',
|
||||
unlimited: 'Unbegrenzte Maschine-zu-Maschine-Apps',
|
||||
not_eligible: 'Entferne deine Maschine-zu-Maschine-Apps',
|
||||
/** UNTRANSLATED */
|
||||
add_on: 'Additional machine-to-machine apps',
|
||||
},
|
||||
resources_limit: {
|
||||
name: 'API-Ressourcen',
|
||||
|
|
|
@ -32,6 +32,7 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} machine to machine apps',
|
||||
unlimited: 'Unlimited machine to machine apps',
|
||||
not_eligible: 'Remove your machine to machine apps',
|
||||
add_on: 'Additional machine-to-machine apps',
|
||||
},
|
||||
resources_limit: {
|
||||
name: 'API resources',
|
||||
|
|
|
@ -32,6 +32,8 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} aplicaciones de dispositivo a dispositivo',
|
||||
unlimited: 'Aplicaciones de dispositivo a dispositivo ilimitadas',
|
||||
not_eligible: 'Elimine sus aplicaciones de dispositivo a dispositivo',
|
||||
/** UNTRANSLATED */
|
||||
add_on: 'Additional machine-to-machine apps',
|
||||
},
|
||||
resources_limit: {
|
||||
name: 'Recursos de API',
|
||||
|
|
|
@ -32,6 +32,8 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} applications machine à machine',
|
||||
unlimited: 'Illimité applications machine à machine',
|
||||
not_eligible: 'Supprimez vos applications machine à machine',
|
||||
/** UNTRANSLATED */
|
||||
add_on: 'Additional machine-to-machine apps',
|
||||
},
|
||||
resources_limit: {
|
||||
name: 'Ressources API',
|
||||
|
|
|
@ -32,6 +32,8 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} applicazioni Machine-to-Machine',
|
||||
unlimited: 'Applicazioni Machine-to-Machine illimitate',
|
||||
not_eligible: 'Rimuovi le tue applicazioni Machine-to-Machine',
|
||||
/** UNTRANSLATED */
|
||||
add_on: 'Additional machine-to-machine apps',
|
||||
},
|
||||
resources_limit: {
|
||||
name: 'Risorse API',
|
||||
|
|
|
@ -32,6 +32,8 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} マシン間アプリケーション',
|
||||
unlimited: '無制限のマシン間アプリケーション',
|
||||
not_eligible: 'マシン間アプリケーションを削除してください',
|
||||
/** UNTRANSLATED */
|
||||
add_on: 'Additional machine-to-machine apps',
|
||||
},
|
||||
resources_limit: {
|
||||
name: 'APIリソース',
|
||||
|
|
|
@ -32,6 +32,8 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} 기계 간 앱',
|
||||
unlimited: '제한 없는 기계 간 앱',
|
||||
not_eligible: '기계 간 앱을 제거하십시오',
|
||||
/** UNTRANSLATED */
|
||||
add_on: 'Additional machine-to-machine apps',
|
||||
},
|
||||
resources_limit: {
|
||||
name: 'API 리소스',
|
||||
|
|
|
@ -32,6 +32,8 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} aplikacje machine to machine',
|
||||
unlimited: 'Nieograniczona liczba aplikacji machine to machine',
|
||||
not_eligible: 'Usuń swoje aplikacje machine to machine',
|
||||
/** UNTRANSLATED */
|
||||
add_on: 'Additional machine-to-machine apps',
|
||||
},
|
||||
resources_limit: {
|
||||
name: 'Zasoby API',
|
||||
|
|
|
@ -32,6 +32,8 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} aplicações de máquina a máquina',
|
||||
unlimited: 'Aplicações de máquina a máquina ilimitadas',
|
||||
not_eligible: 'Remova suas aplicações de máquina a máquina',
|
||||
/** UNTRANSLATED */
|
||||
add_on: 'Additional machine-to-machine apps',
|
||||
},
|
||||
resources_limit: {
|
||||
name: 'Recursos da API',
|
||||
|
|
|
@ -32,6 +32,8 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} aplicações de máquina para máquina',
|
||||
unlimited: 'Aplicações de máquina para máquina ilimitadas',
|
||||
not_eligible: 'Remover as tuas aplicações de máquina para máquina',
|
||||
/** UNTRANSLATED */
|
||||
add_on: 'Additional machine-to-machine apps',
|
||||
},
|
||||
resources_limit: {
|
||||
name: 'Recursos de API',
|
||||
|
|
|
@ -32,6 +32,8 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} приложения для машин ко машине',
|
||||
unlimited: 'Неограниченное количество приложений для машин ко машине',
|
||||
not_eligible: 'Удалите свои приложения для машин ко машине',
|
||||
/** UNTRANSLATED */
|
||||
add_on: 'Additional machine-to-machine apps',
|
||||
},
|
||||
resources_limit: {
|
||||
name: 'API ресурсы',
|
||||
|
|
|
@ -32,6 +32,8 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} makineye makine uygulamalar',
|
||||
unlimited: 'Sınırsız makineye makine uygulamalar',
|
||||
not_eligible: 'Makineye makine uygulamalarınızı kaldırın',
|
||||
/** UNTRANSLATED */
|
||||
add_on: 'Additional machine-to-machine apps',
|
||||
},
|
||||
resources_limit: {
|
||||
name: 'API kaynakları',
|
||||
|
|
|
@ -32,6 +32,8 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} 个机器到机器应用',
|
||||
unlimited: '无限制机器到机器应用',
|
||||
not_eligible: '移除你的机器到机器应用',
|
||||
/** UNTRANSLATED */
|
||||
add_on: 'Additional machine-to-machine apps',
|
||||
},
|
||||
resources_limit: {
|
||||
name: 'API 资源',
|
||||
|
|
|
@ -32,6 +32,8 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} 個機器對機器應用程式',
|
||||
unlimited: '無限機器對機器應用程式',
|
||||
not_eligible: '刪除您的機器對機器應用程式',
|
||||
/** UNTRANSLATED */
|
||||
add_on: 'Additional machine-to-machine apps',
|
||||
},
|
||||
resources_limit: {
|
||||
name: 'API 資源',
|
||||
|
|
|
@ -32,6 +32,8 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} 機器對機器應用程式',
|
||||
unlimited: '不限機器對機器應用程式數',
|
||||
not_eligible: '移除你的機器對機器應用程式',
|
||||
/** UNTRANSLATED */
|
||||
add_on: 'Additional machine-to-machine apps',
|
||||
},
|
||||
resources_limit: {
|
||||
name: 'API 資源',
|
||||
|
|
Loading…
Add table
Reference in a new issue