0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

refactor(console): reorg downgrade plan notification modal (#5419)

This commit is contained in:
Xiao Yijun 2024-02-23 10:32:54 +08:00 committed by GitHub
parent be9428fa0d
commit 9fbb4196f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 137 additions and 382 deletions

View file

@ -1,36 +0,0 @@
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;

View file

@ -1,40 +0,0 @@
import classNames from 'classnames';
import { useMemo, type ReactNode } from 'react';
import { planQuotaItemOrder } from '@/consts/plan-quotas';
import {
type SubscriptionPlanQuotaEntries,
type SubscriptionPlanQuota,
} from '@/types/subscriptions';
import { sortBy } from '@/utils/sort';
import * as styles from './index.module.scss';
type Props = {
entries: SubscriptionPlanQuotaEntries;
itemRenderer: (
quotaKey: keyof SubscriptionPlanQuota,
quotaValue: SubscriptionPlanQuota[keyof SubscriptionPlanQuota]
) => ReactNode;
className?: string;
};
function PlanQuotaList({ entries, itemRenderer, className }: Props) {
const sortedEntries = useMemo(
() =>
entries
.slice()
.sort(([preQuotaKey], [nextQuotaKey]) =>
sortBy(planQuotaItemOrder)(preQuotaKey, nextQuotaKey)
),
[entries]
);
return (
<ul className={classNames(styles.planQuotaList, className)}>
{sortedEntries.map(([quotaKey, quotaValue]) => itemRenderer(quotaKey, quotaValue))}
</ul>
);
}
export default PlanQuotaList;

View file

@ -1,75 +1,40 @@
import { ReservedPlanId } from '@logto/schemas';
import {
type SubscriptionPlanTableGroupKeyMap,
SubscriptionPlanTableGroupKey,
type SubscriptionPlanQuota,
} from '@/types/subscriptions';
import { type SubscriptionPlanQuota } from '@/types/subscriptions';
type EnabledFeatureMap = Record<string, boolean | undefined>;
export const communitySupportEnabledMap: EnabledFeatureMap = {
[ReservedPlanId.Free]: true,
[ReservedPlanId.Hobby]: true,
[ReservedPlanId.Pro]: true,
};
export const ticketSupportResponseTimeMap: Record<string, number | undefined> = {
/**
* Manually add this support quota item to the plan since it will be compared in the downgrade plan notification modal.
*/
export const ticketSupportResponseTimeMap: Record<string, number> = {
[ReservedPlanId.Free]: 0,
[ReservedPlanId.Hobby]: 48,
[ReservedPlanId.Pro]: 48,
};
const planTableGroupKeyMap: SubscriptionPlanTableGroupKeyMap = Object.freeze({
[SubscriptionPlanTableGroupKey.base]: ['basePrice', 'mauLimit', 'tokenLimit'],
[SubscriptionPlanTableGroupKey.applications]: [
'applicationsLimit',
'machineToMachineLimit',
'thirdPartyApplicationsLimit',
],
[SubscriptionPlanTableGroupKey.resources]: ['resourcesLimit', 'scopesPerResourceLimit'],
[SubscriptionPlanTableGroupKey.branding]: [
'customDomainEnabled',
'customCssEnabled',
'appLogoAndFaviconEnabled',
'darkModeEnabled',
'i18nEnabled',
],
[SubscriptionPlanTableGroupKey.userAuthentication]: [
'omniSignInEnabled',
'passwordSignInEnabled',
'passwordlessSignInEnabled',
'emailConnectorsEnabled',
'smsConnectorsEnabled',
'socialConnectorsLimit',
'mfaEnabled',
'ssoEnabled',
],
[SubscriptionPlanTableGroupKey.roles]: [
'userManagementEnabled',
'rolesLimit',
'machineToMachineRolesLimit',
'scopesPerRoleLimit',
],
[SubscriptionPlanTableGroupKey.organizations]: [
'organizationsEnabled',
'allowedUsersPerOrganization',
'invitationEnabled',
'orgRolesLimit',
'orgPermissionsLimit',
'justInTimeProvisioningEnabled',
],
[SubscriptionPlanTableGroupKey.auditLogs]: ['auditLogsRetentionDays'],
[SubscriptionPlanTableGroupKey.hooks]: ['hooksLimit'],
[SubscriptionPlanTableGroupKey.support]: [
'communitySupportEnabled',
'ticketSupportResponseTime',
'soc2ReportEnabled',
'hipaaOrBaaReportEnabled',
],
}) satisfies SubscriptionPlanTableGroupKeyMap;
export const planQuotaItemOrder = Object.values(planTableGroupKeyMap).flat();
/**
* Define the order of quota items in the downgrade plan notification modal and not eligible for downgrade plan modal.
*/
export const planQuotaItemOrder: Array<keyof SubscriptionPlanQuota> = [
'mauLimit',
'tokenLimit',
'applicationsLimit',
'machineToMachineLimit',
'thirdPartyApplicationsLimit',
'resourcesLimit',
'scopesPerResourceLimit',
'customDomainEnabled',
'omniSignInEnabled',
'socialConnectorsLimit',
'mfaEnabled',
'ssoEnabled',
'rolesLimit',
'machineToMachineRolesLimit',
'scopesPerRoleLimit',
'organizationsEnabled',
'auditLogsRetentionDays',
'hooksLimit',
'ticketSupportResponseTime',
];
/**
* Unreleased quota keys will be added here, and it will effect the following:

View file

@ -22,7 +22,6 @@ export const quotaItemPhrasesMap: Record<
scopesPerRoleLimit: 'scopes_per_role_limit.name',
hooksLimit: 'hooks_limit.name',
auditLogsRetentionDays: 'audit_logs_retention_days.name',
communitySupportEnabled: 'community_support_enabled.name',
ticketSupportResponseTime: 'email_ticket_support.name',
mfaEnabled: 'mfa_enabled.name',
organizationsEnabled: 'organizations_enabled.name',
@ -49,7 +48,6 @@ export const quotaItemUnlimitedPhrasesMap: Record<
scopesPerRoleLimit: 'scopes_per_role_limit.unlimited',
hooksLimit: 'hooks_limit.unlimited',
auditLogsRetentionDays: 'audit_logs_retention_days.unlimited',
communitySupportEnabled: 'community_support_enabled.unlimited',
ticketSupportResponseTime: 'email_ticket_support.unlimited',
mfaEnabled: 'mfa_enabled.unlimited',
organizationsEnabled: 'organizations_enabled.unlimited',
@ -76,7 +74,6 @@ export const quotaItemLimitedPhrasesMap: Record<
scopesPerRoleLimit: 'scopes_per_role_limit.limited',
hooksLimit: 'hooks_limit.limited',
auditLogsRetentionDays: 'audit_logs_retention_days.limited',
communitySupportEnabled: 'community_support_enabled.limited',
ticketSupportResponseTime: 'email_ticket_support.limited',
mfaEnabled: 'mfa_enabled.limited',
organizationsEnabled: 'organizations_enabled.limited',
@ -103,18 +100,8 @@ export const quotaItemNotEligiblePhrasesMap: Record<
scopesPerRoleLimit: 'scopes_per_role_limit.not_eligible',
hooksLimit: 'hooks_limit.not_eligible',
auditLogsRetentionDays: 'audit_logs_retention_days.not_eligible',
communitySupportEnabled: 'community_support_enabled.not_eligible',
ticketSupportResponseTime: 'email_ticket_support.not_eligible',
mfaEnabled: 'mfa_enabled.not_eligible',
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',
};

View file

@ -64,7 +64,6 @@ export const defaultSubscriptionPlan: SubscriptionPlan = {
hooksLimit: null,
organizationsEnabled: true,
ssoEnabled: true,
communitySupportEnabled: true,
ticketSupportResponseTime: 48,
thirdPartyApplicationsLimit: null,
},

View file

@ -1,12 +0,0 @@
.icon {
width: 16px;
height: 16px;
&.notCapable {
color: var(--color-error);
}
}
.lineThrough {
text-decoration: line-through;
}

View file

@ -1,37 +0,0 @@
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;

View file

@ -4,7 +4,6 @@ import {
quotaItemUnlimitedPhrasesMap,
quotaItemPhrasesMap,
quotaItemLimitedPhrasesMap,
quotaItemAddOnPhrasesMap,
} from '@/consts/quota-item-phrases';
import DynamicT from '@/ds-components/DynamicT';
import { type SubscriptionPlanQuota } from '@/types/subscriptions';
@ -14,21 +13,17 @@ const quotaItemPhraseKeyPrefix = 'subscription.quota_item';
type Props = {
quotaKey: keyof SubscriptionPlanQuota;
quotaValue: SubscriptionPlanQuota[keyof SubscriptionPlanQuota];
isAddOn?: boolean;
};
function QuotaItemPhrase({ quotaKey, quotaValue, isAddOn = false }: Props) {
function QuotaItemPhrase({ quotaKey, quotaValue }: 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);
cond(isLimited && quotaItemLimitedPhrasesMap[quotaKey]);
if (!phraseKey) {
// Should not happen

View file

@ -11,6 +11,19 @@
gap: _.unit(2);
}
.icon {
width: 16px;
height: 16px;
&.notCapable {
color: var(--color-error);
}
}
.lineThrough {
text-decoration: line-through;
}
&.withIcon {
list-style-type: none;
// Unset a margin to the left for the list item marker

View file

@ -0,0 +1,43 @@
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 { 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];
hasStatusIcon?: boolean;
};
function DiffQuotaItem({ quotaKey, quotaValue, hasStatusIcon }: Props) {
const isNotCapable = quotaValue === 0 || quotaValue === false;
const DowngradeStatusIcon = isNotCapable ? Failed : DescendArrow;
return (
<li className={classNames(styles.quotaListItem, hasStatusIcon && 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}>
{cond(
hasStatusIcon && (
<DowngradeStatusIcon
className={classNames(styles.icon, isNotCapable && styles.notCapable)}
/>
)
)}
<span className={cond(isNotCapable && styles.lineThrough)}>
<QuotaItemPhrase quotaKey={quotaKey} quotaValue={quotaValue} />
</span>
</span>
</li>
);
}
export default DiffQuotaItem;

View file

@ -0,0 +1,29 @@
import classNames from 'classnames';
import { type SubscriptionPlanQuotaEntries } from '@/types/subscriptions';
import DiffQuotaItem from './DiffQuotaItem';
import * as styles from './index.module.scss';
type Props = {
entries: SubscriptionPlanQuotaEntries;
isDowngradeTargetPlan: boolean;
className?: string;
};
function PlanQuotaList({ entries, isDowngradeTargetPlan, className }: Props) {
return (
<ul className={classNames(styles.planQuotaList, className)}>
{entries.map(([quotaKey, quotaValue]) => (
<DiffQuotaItem
key={quotaKey}
quotaKey={quotaKey}
quotaValue={quotaValue}
hasStatusIcon={isDowngradeTargetPlan}
/>
))}
</ul>
);
}
export default PlanQuotaList;

View file

@ -1,14 +1,16 @@
import { useMemo } from 'react';
import { Trans } from 'react-i18next';
import PlanName from '@/components/PlanName';
import PlanQuotaList from '@/components/PlanQuotaList';
import { planQuotaItemOrder } from '@/consts/plan-quotas';
import DynamicT from '@/ds-components/DynamicT';
import {
type SubscriptionPlanQuotaEntries,
type SubscriptionPlanQuota,
} from '@/types/subscriptions';
import { sortBy } from '@/utils/sort';
import DiffQuotaItem from './DiffQuotaItem';
import PlanQuotaList from './PlanQuotaList';
import * as styles from './index.module.scss';
type Props = {
@ -18,6 +20,17 @@ type Props = {
};
function PlanQuotaDiffCard({ planName, quotaDiff, isDowngradeTargetPlan = false }: Props) {
// eslint-disable-next-line no-restricted-syntax
const sortedEntries = useMemo(
() =>
Object.entries(quotaDiff)
.slice()
.sort(([preQuotaKey], [nextQuotaKey]) =>
sortBy(planQuotaItemOrder)(preQuotaKey, nextQuotaKey)
),
[quotaDiff]
) as SubscriptionPlanQuotaEntries;
return (
<div className={styles.container}>
<div className={styles.title}>
@ -31,20 +44,7 @@ function PlanQuotaDiffCard({ planName, quotaDiff, isDowngradeTargetPlan = false
/>
</Trans>
</div>
<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}
/>
)}
/>
<PlanQuotaList entries={sortedEntries} isDowngradeTargetPlan={isDowngradeTargetPlan} />
</div>
);
}

View file

@ -22,7 +22,6 @@ import * as styles from './index.module.scss';
const excludedQuotaKeys = new Set<keyof SubscriptionPlanQuota>([
'auditLogsRetentionDays',
'communitySupportEnabled',
'ticketSupportResponseTime',
]);

View file

@ -1,4 +1,3 @@
import { type Nullable } from '@silverhand/essentials';
import { z } from 'zod';
import { type InvoicesResponse, type SubscriptionPlanResponse } from '@/cloud/types/router';
@ -14,8 +13,7 @@ export type SubscriptionPlanQuota = Omit<
SubscriptionPlanResponse['quota'],
'builtInEmailConnectorEnabled'
> & {
// Support
communitySupportEnabled: boolean;
// Add ticket support quota item to the plan since it will be compared in the downgrade plan notification modal.
ticketSupportResponseTime: number;
};
@ -27,51 +25,6 @@ export type SubscriptionPlan = Omit<SubscriptionPlanResponse, 'quota'> & {
quota: SubscriptionPlanQuota;
};
type SubscriptionPlanTable = Partial<
SubscriptionPlanQuota & {
// Base quota
basePrice: string;
// UI and branding
customCssEnabled: boolean;
appLogoAndFaviconEnabled: boolean;
darkModeEnabled: boolean;
i18nEnabled: boolean;
// User authn
passwordSignInEnabled: boolean;
passwordlessSignInEnabled: boolean;
emailConnectorsEnabled: boolean;
smsConnectorsEnabled: boolean;
// User management
userManagementEnabled: boolean;
// Organization
allowedUsersPerOrganization: Nullable<number>;
invitationEnabled: boolean;
orgRolesLimit: Nullable<number>;
orgPermissionsLimit: Nullable<number>;
justInTimeProvisioningEnabled: boolean;
// Support
soc2ReportEnabled: boolean;
hipaaOrBaaReportEnabled: boolean;
}
>;
export enum SubscriptionPlanTableGroupKey {
base = 'base',
applications = 'applications',
resources = 'resources',
branding = 'branding',
userAuthentication = 'userAuthentication',
roles = 'roles',
auditLogs = 'auditLogs',
hooks = 'hooks',
organizations = 'organizations',
support = 'support',
}
export type SubscriptionPlanTableGroupKeyMap = {
[key in SubscriptionPlanTableGroupKey]: Array<keyof Required<SubscriptionPlanTable>>;
};
export const localCheckoutSessionGuard = z.object({
state: z.string(),
sessionId: z.string(),

View file

@ -4,7 +4,7 @@ import dayjs from 'dayjs';
import { tryReadResponseErrorBody } from '@/cloud/hooks/use-cloud-api';
import { type SubscriptionPlanResponse } from '@/cloud/types/router';
import { communitySupportEnabledMap, ticketSupportResponseTimeMap } from '@/consts/plan-quotas';
import { ticketSupportResponseTimeMap } from '@/consts/plan-quotas';
import { featuredPlanIdOrder, featuredPlanIds } from '@/consts/subscriptions';
import { type SubscriptionPlanQuota, type SubscriptionPlan } from '@/types/subscriptions';
@ -15,7 +15,9 @@ export const addSupportQuotaToPlan = (subscriptionPlanResponse: SubscriptionPlan
...subscriptionPlanResponse,
quota: {
...quota,
communitySupportEnabled: communitySupportEnabledMap[id] ?? false, // Fallback to not supported
/**
* Manually add this support quota item to the plan since it will be compared in the downgrade plan notification modal.
*/
ticketSupportResponseTime: ticketSupportResponseTimeMap[id] ?? 0, // Fallback to not supported
},
};

View file

@ -32,7 +32,6 @@ 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',
add_on: 'Zusätzliche Maschine-zu-Maschine-Apps',
},
third_party_applications_limit: {
/** UNTRANSLATED */
@ -133,12 +132,6 @@ const quota_item = {
unlimited: 'Unbegrenzte Tage',
not_eligible: 'Keine Audit-Logs',
},
community_support_enabled: {
name: 'Community-Support',
limited: 'Community-Support',
unlimited: 'Community-Support',
not_eligible: 'Kein Community-Support',
},
email_ticket_support: {
name: 'E-Mail-Ticket-Support',
limited: '{{count, number}} Stunde E-Mail-Ticket-Support',

View file

@ -32,7 +32,6 @@ 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',
},
third_party_applications_limit: {
name: 'Third-party apps',
@ -128,12 +127,6 @@ const quota_item = {
unlimited: 'Unlimited days',
not_eligible: 'No audit logs',
},
community_support_enabled: {
name: 'Community support',
limited: 'Community support',
unlimited: 'Community support',
not_eligible: 'No community support',
},
email_ticket_support: {
name: 'Email ticket support',
limited: '{{count, number}} hour email ticket support',

View file

@ -32,7 +32,6 @@ 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',
add_on: 'Aplicaciones de máquina a máquina adicionales',
},
third_party_applications_limit: {
/** UNTRANSLATED */
@ -133,12 +132,6 @@ const quota_item = {
unlimited: 'Días ilimitados',
not_eligible: 'Sin registros de auditoría',
},
community_support_enabled: {
name: 'Soporte de la comunidad',
limited: 'Soporte de la comunidad',
unlimited: 'Soporte de la comunidad',
not_eligible: 'Sin soporte de la comunidad',
},
email_ticket_support: {
name: 'Soporte de boletos de correo electrónico',
limited: '{{count, number}} hora de soporte de boletos de correo electrónico',

View file

@ -32,7 +32,6 @@ const quota_item = {
limited_other: '{{count, number}} applications machine à machine',
unlimited: 'Illimité applications machine à machine',
not_eligible: 'Supprimez vos applications machine à machine',
add_on: 'Applications machine à machine supplémentaires',
},
third_party_applications_limit: {
/** UNTRANSLATED */
@ -133,12 +132,6 @@ const quota_item = {
unlimited: 'Jours illimités',
not_eligible: "Pas de journalisation d'audit",
},
community_support_enabled: {
name: 'Support de la communauté',
limited: 'Support de la communauté',
unlimited: 'Support de la communauté',
not_eligible: 'Pas de support de la communauté',
},
email_ticket_support: {
name: 'Assistance par ticket de messagerie électronique',
limited: "{{count, number}} heure d'assistance par ticket de messagerie électronique",

View file

@ -32,7 +32,6 @@ 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',
add_on: 'Applicazioni Machine-to-Machine aggiuntive',
},
third_party_applications_limit: {
/** UNTRANSLATED */
@ -133,12 +132,6 @@ const quota_item = {
unlimited: 'Giorni illimitati',
not_eligible: 'Nessun log di audit',
},
community_support_enabled: {
name: 'Supporto della comunità',
limited: 'Supporto della comunità',
unlimited: 'Supporto della comunità',
not_eligible: 'Nessun supporto della comunità',
},
email_ticket_support: {
name: 'Supporto tramite ticket email',
limited: '{{count, number}} ora di supporto tramite ticket email',

View file

@ -32,7 +32,6 @@ const quota_item = {
limited_other: '{{count, number}} マシン間アプリケーション',
unlimited: '無制限のマシン間アプリケーション',
not_eligible: 'マシン間アプリケーションを削除してください',
add_on: '追加のマシン間アプリケーション',
},
third_party_applications_limit: {
/** UNTRANSLATED */
@ -133,12 +132,6 @@ const quota_item = {
unlimited: '無制限の日数',
not_eligible: '監査ログがありません',
},
community_support_enabled: {
name: 'コミュニティサポート',
limited: 'コミュニティサポート',
unlimited: 'コミュニティサポート',
not_eligible: 'コミュニティサポートなし',
},
email_ticket_support: {
name: 'メールチケットサポート',
limited: '{{count, number}}時間のメールチケットサポート',

View file

@ -32,7 +32,6 @@ const quota_item = {
limited_other: '{{count, number}} 기계 간 앱',
unlimited: '제한 없는 기계 간 앱',
not_eligible: '기계 간 앱을 제거하십시오',
add_on: '추가 기계간 앱',
},
third_party_applications_limit: {
/** UNTRANSLATED */
@ -133,12 +132,6 @@ const quota_item = {
unlimited: '제한 없는 기간',
not_eligible: '감사 로그 없음',
},
community_support_enabled: {
name: '커뮤니티 지원',
limited: '커뮤니티 지원',
unlimited: '커뮤니티 지원',
not_eligible: '커뮤니티 지원 없음',
},
email_ticket_support: {
name: '이메일 지원 티켓',
limited: '{{count, number}}시간 이메일 지원 티켓',

View file

@ -32,7 +32,6 @@ 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',
add_on: 'Dodatkowe aplikacje machine-to-machine',
},
third_party_applications_limit: {
/** UNTRANSLATED */
@ -133,12 +132,6 @@ const quota_item = {
unlimited: 'Nieograniczona liczba dni',
not_eligible: 'Brak dzienników audytowych',
},
community_support_enabled: {
name: 'Wsparcie społeczności',
limited: 'Wsparcie społeczności',
unlimited: 'Wsparcie społeczności',
not_eligible: 'Brak wsparcia społecznościowego',
},
email_ticket_support: {
name: 'Wsparcie poprzez e-maile',
limited: '{{count, number}} godzina wsparcia poprzez e-maile',

View file

@ -32,7 +32,6 @@ 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',
add_on: 'Aplicativos adicionais de máquina a máquina',
},
third_party_applications_limit: {
/** UNTRANSLATED */
@ -133,12 +132,6 @@ const quota_item = {
unlimited: 'Dias ilimitados',
not_eligible: 'Nenhum registro de auditoria',
},
community_support_enabled: {
name: 'Suporte da comunidade',
limited: 'Suporte da comunidade',
unlimited: 'Suporte da comunidade',
not_eligible: 'Nenhum suporte da comunidade',
},
email_ticket_support: {
name: 'Suporte de ingressos de e-mail',
limited: '{{count, number}} hora de suporte de ingressos de e-mail',

View file

@ -32,7 +32,6 @@ 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',
add_on: 'Aplicações adicionais de máquina para máquina',
},
third_party_applications_limit: {
/** UNTRANSLATED */
@ -133,12 +132,6 @@ const quota_item = {
unlimited: 'Dias ilimitados',
not_eligible: 'Sem registos de auditoria',
},
community_support_enabled: {
name: 'Apoio da comunidade',
limited: 'Apoio da comunidade',
unlimited: 'Apoio da comunidade',
not_eligible: 'Sem apoio da comunidade',
},
email_ticket_support: {
name: 'Suporte por e-mail',
limited: '{{count, number}} horas de suporte por e-mail',

View file

@ -32,7 +32,6 @@ const quota_item = {
limited_other: '{{count, number}} приложения для машин ко машине',
unlimited: 'Неограниченное количество приложений для машин ко машине',
not_eligible: 'Удалите свои приложения для машин ко машине',
add_on: 'Дополнительные приложения для машины ко машине',
},
third_party_applications_limit: {
/** UNTRANSLATED */
@ -133,12 +132,6 @@ const quota_item = {
unlimited: 'Неограниченное количество дней',
not_eligible: 'Без аудит-логов',
},
community_support_enabled: {
name: 'Поддержка сообщества',
limited: 'Поддержка сообщества',
unlimited: 'Поддержка сообщества',
not_eligible: 'Без поддержки сообщества',
},
email_ticket_support: {
name: 'Поддержка по электронной почте',
limited: '{{count, number}} час поддержки по электронной почте',

View file

@ -32,7 +32,6 @@ 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',
add_on: 'Ek makineye makine uygulamaları',
},
third_party_applications_limit: {
/** UNTRANSLATED */
@ -133,12 +132,6 @@ const quota_item = {
unlimited: 'Sınırsız günler',
not_eligible: 'Denetim günlüğünüz yok',
},
community_support_enabled: {
name: 'Topluluk desteği',
limited: 'Topluluk desteği',
unlimited: 'Topluluk desteği',
not_eligible: 'Topluluk desteği yok',
},
email_ticket_support: {
name: 'E-posta bilet desteği',
limited: '{{count, number}} saat e-posta bilet desteği',

View file

@ -32,7 +32,6 @@ const quota_item = {
limited_other: '{{count, number}} 个机器到机器应用',
unlimited: '无限制机器到机器应用',
not_eligible: '移除你的机器到机器应用',
add_on: '更多机器到机器应用',
},
third_party_applications_limit: {
/** UNTRANSLATED */
@ -133,12 +132,6 @@ const quota_item = {
unlimited: '无限制天数',
not_eligible: '无审计日志',
},
community_support_enabled: {
name: '社区支持',
limited: '社区支持',
unlimited: '社区支持',
not_eligible: '无社区支持',
},
email_ticket_support: {
name: '电子邮件支持',
limited: '{{count, number}} 小时电子邮件支持',

View file

@ -32,7 +32,6 @@ const quota_item = {
limited_other: '{{count, number}} 個機器對機器應用程式',
unlimited: '無限機器對機器應用程式',
not_eligible: '刪除您的機器對機器應用程式',
add_on: '附加的機器對機器應用程式',
},
third_party_applications_limit: {
/** UNTRANSLATED */
@ -133,12 +132,6 @@ const quota_item = {
unlimited: '無限天數',
not_eligible: '無審計日誌',
},
community_support_enabled: {
name: '社區支援',
limited: '社區支援',
unlimited: '社區支援',
not_eligible: '無社區支援',
},
email_ticket_support: {
name: '電子郵件票務支援',
limited: '{{count, number}} 小時電子郵件票務支援',

View file

@ -32,7 +32,6 @@ const quota_item = {
limited_other: '{{count, number}} 機器對機器應用程式',
unlimited: '不限機器對機器應用程式數',
not_eligible: '移除你的機器對機器應用程式',
add_on: '額外的機器對機器應用程式',
},
third_party_applications_limit: {
/** UNTRANSLATED */
@ -133,12 +132,6 @@ const quota_item = {
unlimited: '不限天數',
not_eligible: '無審計記錄',
},
community_support_enabled: {
name: '社群支援',
limited: '社群支援',
unlimited: '社群支援',
not_eligible: '無社群支援',
},
email_ticket_support: {
name: '電子郵件票務支援',
limited: '{{count, number}}小時的電子郵件票務支援',