mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 10:55:06 +03:00
Chore/request upgrade plan modal (#40889)
* init request upgrade plan modal * Consolidate UpgradeToPro components across UI + use UpgradePlanButton, UpgradePlanButton determines whether to show request upgrade CTA * Allow to pass icon to Admonition * Tiny fix on upgrade plan button CTA to consider addons * Hook up upgrade request endpoint * Update API types * remove hardcode * Add request upgrade CTA in plan side panel * Fix disk compute * Show request upgrade button for change to large compute * Nit
This commit is contained in:
1 parent
4a93272682
commit
de50740b04
31 files changed
+708
-412
No files matched your search
@@ -8,7 +8,7 @@ import { boolean, number, object, string } from 'yup'
|
||||
import { useParams } from 'common'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
import UpgradeToPro from 'components/ui/UpgradeToPro'
|
||||
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
|
||||
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
|
||||
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
@@ -505,8 +505,10 @@ export const MfaAuthSettingsForm = () => {
|
||||
{promptProPlanUpgrade && (
|
||||
<UpgradeToPro
|
||||
fullWidth
|
||||
primaryText="Configuring Advanced MFA is only available on the Pro plan and above"
|
||||
secondaryText="Upgrade to the Pro plan to configure Advanced MFA settings"
|
||||
source="authSmsMfa"
|
||||
featureProposition="configure settings for SMS MFA"
|
||||
primaryText="SMS MFA is only available on the Pro Plan and above"
|
||||
secondaryText="Upgrade to the Pro plan to configure settings for SMS MFA"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as z from 'zod'
|
||||
import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
import UpgradeToPro from 'components/ui/UpgradeToPro'
|
||||
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
|
||||
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
|
||||
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
|
||||
import { useMaxConnectionsQuery } from 'data/database/max-connections-query'
|
||||
@@ -194,6 +194,8 @@ export const PerformanceSettingsForm = () => {
|
||||
<ScaffoldSection isFullWidth>
|
||||
{promptProPlanUpgrade && (
|
||||
<UpgradeToPro
|
||||
source="authPerformance"
|
||||
featureProposition="configure advanced Auth server settings"
|
||||
primaryText="Configuring Auth server performance is only available on the Pro plan and above"
|
||||
secondaryText="Upgrade to the Pro plan to configure settings for your Auth server"
|
||||
/>
|
||||
|
||||
+3
-1
@@ -8,7 +8,7 @@ import * as z from 'zod'
|
||||
import { useParams } from 'common'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
import UpgradeToPro from 'components/ui/UpgradeToPro'
|
||||
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
|
||||
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
|
||||
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
@@ -368,6 +368,8 @@ export const SessionsAuthSettingsForm = () => {
|
||||
{promptProPlanUpgrade && (
|
||||
<UpgradeToPro
|
||||
fullWidth
|
||||
source="authSessions"
|
||||
featureProposition="configure user sessions"
|
||||
primaryText="Configuring user sessions is only available on the Pro Plan and above"
|
||||
secondaryText="Upgrade to the Pro plan to configure settings for user sessions"
|
||||
/>
|
||||
|
||||
@@ -17,7 +17,7 @@ import AlertError from 'components/ui/AlertError'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { InlineLink, InlineLinkClassName } from 'components/ui/InlineLink'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import UpgradeToPro from 'components/ui/UpgradeToPro'
|
||||
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
|
||||
import { useBranchCreateMutation } from 'data/branches/branch-create-mutation'
|
||||
import { useBranchesQuery } from 'data/branches/branches-query'
|
||||
import { useDiskAttributesQuery } from 'data/config/disk-attributes-query'
|
||||
@@ -27,6 +27,7 @@ import { projectKeys } from 'data/projects/keys'
|
||||
import { DesiredInstanceSize, instanceSizeSpecs } from 'data/projects/new-project.constants'
|
||||
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
|
||||
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
|
||||
import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
@@ -60,7 +61,6 @@ import {
|
||||
estimateDiskCost,
|
||||
estimateRestoreTime,
|
||||
} from './BranchManagement.utils'
|
||||
import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
|
||||
|
||||
export const CreateBranchModal = () => {
|
||||
const { ref } = useParams()
|
||||
@@ -268,14 +268,14 @@ export const CreateBranchModal = () => {
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form id={formId} onSubmit={form.handleSubmit(onSubmit)}>
|
||||
{promptPlanUpgrade && (
|
||||
<>
|
||||
<UpgradeToPro
|
||||
primaryText="Upgrade to unlock branching"
|
||||
secondaryText="Create and test schema changes, functions, and more in a separate, temporary instance without affecting production"
|
||||
source="create-branch"
|
||||
/>
|
||||
<DialogSectionSeparator />
|
||||
</>
|
||||
<UpgradeToPro
|
||||
fullWidth
|
||||
layout="vertical"
|
||||
source="create-branch"
|
||||
featureProposition="enable branching"
|
||||
primaryText="Upgrade to unlock branching"
|
||||
secondaryText="Create and test schema changes, functions, and more in a separate, temporary instance without affecting production"
|
||||
/>
|
||||
)}
|
||||
|
||||
<DialogSection
|
||||
|
||||
@@ -6,7 +6,7 @@ import { toast } from 'sonner'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import UpgradeToPro from 'components/ui/UpgradeToPro'
|
||||
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
|
||||
import { useBackupRestoreMutation } from 'data/database/backup-restore-mutation'
|
||||
import { DatabaseBackup, useBackupsQuery } from 'data/database/backups-query'
|
||||
import { useSetProjectStatus } from 'data/projects/project-detail-query'
|
||||
@@ -57,10 +57,11 @@ export const BackupsList = () => {
|
||||
return (
|
||||
<UpgradeToPro
|
||||
addon="pitr"
|
||||
icon={<Clock size={20} />}
|
||||
source="backups"
|
||||
featureProposition="have up to 7 days of scheduled backups"
|
||||
icon={<Clock size={20} strokeWidth={1.5} />}
|
||||
primaryText="Free Plan does not include project backups."
|
||||
secondaryText="Upgrade to the Pro Plan for up to 7 days of scheduled backups."
|
||||
source="backups"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -147,10 +147,9 @@ export const Destinations = () => {
|
||||
{isPaidPlan ? (
|
||||
<EnableReplicationModal />
|
||||
) : (
|
||||
<UpgradePlanButton type="primary" plan="Pro" />
|
||||
<UpgradePlanButton source="replication" featureProposition="use replication" />
|
||||
)}
|
||||
{/* [Joshen] Placeholder for when we have documentation */}
|
||||
<DocsButton href={`${DOCS_URL}`} />
|
||||
<DocsButton href={`${DOCS_URL}/guides/database/replication#replication`} />
|
||||
</div>
|
||||
</div>
|
||||
) : hasDestinations ? (
|
||||
|
||||
+2
-1
@@ -14,7 +14,7 @@ import AlertError from 'components/ui/AlertError'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import UpgradeToPro from 'components/ui/UpgradeToPro'
|
||||
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
|
||||
import { useDiskAttributesQuery } from 'data/config/disk-attributes-query'
|
||||
import { useCloneBackupsQuery } from 'data/projects/clone-query'
|
||||
import { useCloneStatusQuery } from 'data/projects/clone-status-query'
|
||||
@@ -106,6 +106,7 @@ export const RestoreToNewProject = () => {
|
||||
<UpgradeToPro
|
||||
buttonText="Upgrade"
|
||||
source="backupsRestoreToNewProject"
|
||||
featureProposition="enable restoring to new project"
|
||||
primaryText="Restore to a new project requires a pro plan or above."
|
||||
secondaryText="To restore to a new project, you need to upgrade to a Pro plan and have physical backups enabled."
|
||||
/>
|
||||
|
||||
@@ -9,7 +9,8 @@ import { toast } from 'sonner'
|
||||
import { useParams } from 'common'
|
||||
import { MAX_WIDTH_CLASSES, PADDING_CLASSES, ScaffoldContainer } from 'components/layouts/Scaffold'
|
||||
import { DocsButton } from 'components/ui/DocsButton'
|
||||
import { UpgradePlanButton } from 'components/ui/UpgradePlanButton'
|
||||
import { RequestUpgradeToBillingOwners } from 'components/ui/RequestUpgradeToBillingOwners'
|
||||
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
|
||||
import {
|
||||
useDiskAttributesQuery,
|
||||
useRemainingDurationForDiskAttributeUpdate,
|
||||
@@ -211,7 +212,7 @@ export function DiskManagementForm() {
|
||||
const usedPercentage = (usedSize / totalSize) * 100
|
||||
|
||||
const disableIopsThroughputConfig =
|
||||
RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(modifiedComputeSize)
|
||||
!isSpendCapEnabled && RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(modifiedComputeSize)
|
||||
|
||||
const isBranch = project?.parent_project_ref !== undefined
|
||||
|
||||
@@ -321,103 +322,121 @@ export function DiskManagementForm() {
|
||||
if (isDiskAttributesSuccess || isSuccess) {
|
||||
form.reset(defaultValues, {})
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSuccess, isDiskAttributesSuccess])
|
||||
|
||||
return (
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-8">
|
||||
<ScaffoldContainer className="relative flex flex-col gap-10" bottomPadding>
|
||||
<NoticeBar
|
||||
type="default"
|
||||
visible={isEntitlementsLoaded && isPlanUpgradeRequired}
|
||||
title="Compute and Disk configuration is not available on the Free Plan"
|
||||
actions={<UpgradePlanButton source="diskManagementConfigure" plan="Pro" />}
|
||||
description="You will need to upgrade to at least the Pro Plan to configure compute and disk"
|
||||
<>
|
||||
<ScaffoldContainer className="relative flex flex-col gap-10" bottomPadding>
|
||||
{isEntitlementsLoaded && isPlanUpgradeRequired && (
|
||||
<UpgradeToPro
|
||||
featureProposition="configure compute and disk"
|
||||
primaryText="Compute and Disk configuration is not available on the Free Plan"
|
||||
secondaryText="You will need to upgrade to at least the Pro Plan to configure compute and disk"
|
||||
/>
|
||||
)}
|
||||
|
||||
{isProjectResizing ||
|
||||
isProjectRequestingDiskChanges ||
|
||||
(noPermissions && (
|
||||
<div className="relative flex flex-col gap-10">
|
||||
<DiskMangementRestartRequiredSection
|
||||
visible={isProjectResizing}
|
||||
title="Your project will now automatically restart."
|
||||
description="Your project will be unavailable for up to 2 mins."
|
||||
/>
|
||||
<NoticeBar
|
||||
type="default"
|
||||
visible={isProjectRequestingDiskChanges}
|
||||
title="Disk configuration changes have been requested"
|
||||
description="The requested changes will be applied to your disk shortly"
|
||||
/>
|
||||
<NoticeBar
|
||||
type="default"
|
||||
visible={noPermissions}
|
||||
title="You do not have permission to update disk configuration"
|
||||
description="Please contact your organization administrator to update your disk configuration"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Separator />
|
||||
|
||||
<ComputeSizeField form={form} disabled={disableComputeInputs} />
|
||||
|
||||
{!(isAws || isAwsNimbus) && <Separator />}
|
||||
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<SpendCapDisabledSection />
|
||||
|
||||
{(isProjectResizing ||
|
||||
isProjectRequestingDiskChanges ||
|
||||
(isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions)) && (
|
||||
<div className="relative flex flex-col gap-10">
|
||||
<DiskMangementRestartRequiredSection
|
||||
visible={isProjectResizing}
|
||||
title="Your project will now automatically restart."
|
||||
description="Your project will be unavailable for up to 2 mins."
|
||||
/>
|
||||
<NoticeBar
|
||||
type="default"
|
||||
visible={!(isAws || isAwsNimbus)}
|
||||
title="Disk configuration is only available for projects in the AWS cloud provider"
|
||||
description={
|
||||
isAwsK8s
|
||||
? 'Configuring your disk for AWS (Revamped) projects is unavailable for now.'
|
||||
: isBranch
|
||||
? 'Delete and recreate your Preview Branch to configure disk size. It was deployed on an older branching infrastructure.'
|
||||
: 'The Fly Postgres offering is deprecated - please migrate your instance to the AWS cloud prov to configure your disk.'
|
||||
}
|
||||
visible={isProjectRequestingDiskChanges}
|
||||
title="Disk configuration changes have been requested"
|
||||
description="The requested changes will be applied to your disk shortly"
|
||||
/>
|
||||
<NoticeBar
|
||||
type="default"
|
||||
visible={isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions}
|
||||
title="You do not have permission to update disk configuration"
|
||||
description="Please contact your organization administrator to update your disk configuration"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
</ScaffoldContainer>
|
||||
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form
|
||||
id="disk-compute-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-8"
|
||||
>
|
||||
<ScaffoldContainer className="relative flex flex-col gap-10" bottomPadding>
|
||||
<ComputeSizeField form={form} disabled={disableComputeInputs} />
|
||||
|
||||
{!(isAws || isAwsNimbus) && <Separator />}
|
||||
|
||||
<SpendCapDisabledSection />
|
||||
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<NoticeBar
|
||||
type="default"
|
||||
visible={!(isAws || isAwsNimbus)}
|
||||
title="Disk configuration is only available for projects in the AWS cloud provider"
|
||||
description={
|
||||
isAwsK8s
|
||||
? 'Configuring your disk for AWS (Revamped) projects is unavailable for now.'
|
||||
: isBranch
|
||||
? 'Delete and recreate your Preview Branch to configure disk size. It was deployed on an older branching infrastructure.'
|
||||
: 'The Fly Postgres offering is deprecated - please migrate your instance to the AWS cloud prov to configure your disk.'
|
||||
}
|
||||
/>
|
||||
|
||||
{isAws && (
|
||||
<>
|
||||
<div className="flex flex-col gap-y-3">
|
||||
<DiskCountdownRadial />
|
||||
{!isReadOnlyMode && usedPercentage >= 90 && isWithinCooldownWindow && (
|
||||
<Admonition
|
||||
type="destructive"
|
||||
className="mb-0"
|
||||
title="Database size is currently over 90% of disk size"
|
||||
description="Your project will enter read-only mode once you reach 95% of the disk space to prevent your database from exceeding the disk limitations"
|
||||
>
|
||||
<DocsButton
|
||||
abbrev={false}
|
||||
className="mt-2"
|
||||
href={`${DOCS_URL}/guides/platform/database-size#read-only-mode`}
|
||||
/>
|
||||
</Admonition>
|
||||
)}
|
||||
{isReadOnlyMode && (
|
||||
<Admonition
|
||||
type="destructive"
|
||||
className="mb-0"
|
||||
title="Project is currently in read-only mode"
|
||||
description="You will need to manually override read-only mode and reduce the database size to below 95% of the disk size"
|
||||
>
|
||||
<DocsButton
|
||||
abbrev={false}
|
||||
className="mt-2"
|
||||
href={`${DOCS_URL}/guides/platform/database-size#disabling-read-only-mode`}
|
||||
/>
|
||||
</Admonition>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DiskSizeField
|
||||
form={form}
|
||||
disableInput={disableDiskInputs}
|
||||
setAdvancedSettingsOpenState={setAdvancedSettingsOpenState}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isAws && (
|
||||
<>
|
||||
<div className="flex flex-col gap-y-3">
|
||||
<DiskCountdownRadial />
|
||||
{!isReadOnlyMode && usedPercentage >= 90 && isWithinCooldownWindow && (
|
||||
<Admonition
|
||||
type="destructive"
|
||||
title="Database size is currently over 90% of disk size"
|
||||
description="Your project will enter read-only mode once you reach 95% of the disk space to prevent your database from exceeding the disk limitations"
|
||||
>
|
||||
<DocsButton
|
||||
abbrev={false}
|
||||
className="mt-2"
|
||||
href={`${DOCS_URL}/guides/platform/database-size#read-only-mode`}
|
||||
/>
|
||||
</Admonition>
|
||||
)}
|
||||
{isReadOnlyMode && (
|
||||
<Admonition
|
||||
type="destructive"
|
||||
title="Project is currently in read-only mode"
|
||||
description="You will need to manually override read-only mode and reduce the database size to below 95% of the disk size"
|
||||
>
|
||||
<DocsButton
|
||||
abbrev={false}
|
||||
className="mt-2"
|
||||
href={`${DOCS_URL}/guides/platform/database-size#disabling-read-only-mode`}
|
||||
/>
|
||||
</Admonition>
|
||||
)}
|
||||
</div>
|
||||
<DiskSizeField
|
||||
form={form}
|
||||
disableInput={disableDiskInputs}
|
||||
setAdvancedSettingsOpenState={setAdvancedSettingsOpenState}
|
||||
/>
|
||||
<Separator />
|
||||
|
||||
<Collapsible_Shadcn_
|
||||
// TO DO: wrap component into pattern
|
||||
className="-space-y-px"
|
||||
@@ -440,90 +459,100 @@ export function DiskManagementForm() {
|
||||
</CollapsibleTrigger_Shadcn_>
|
||||
<CollapsibleContent_Shadcn_
|
||||
className={cn(
|
||||
'flex flex-col gap-8 py-8 transition-all',
|
||||
'transition-all rounded-b',
|
||||
'data-[state=open]:border data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down'
|
||||
)}
|
||||
>
|
||||
<div className="px-8 flex flex-col gap-y-8">
|
||||
<AutoScaleFields form={form} />
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="px-8 flex flex-col gap-y-8">
|
||||
<NoticeBar
|
||||
type="default"
|
||||
visible={disableIopsThroughputConfig}
|
||||
title="Adjusting disk configuration requires LARGE Compute size or above"
|
||||
description={`Increase your compute size to adjust your disk's storage type, ${form.getValues('storageType') === 'gp3' ? 'IOPS, ' : ''} and throughput`}
|
||||
actions={
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => {
|
||||
form.setValue('computeSize', 'ci_large')
|
||||
}}
|
||||
>
|
||||
Change to LARGE Compute
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<StorageTypeField
|
||||
form={form}
|
||||
disableInput={disableIopsThroughputConfig || disableDiskInputs}
|
||||
/>
|
||||
<IOPSField
|
||||
form={form}
|
||||
disableInput={disableIopsThroughputConfig || disableDiskInputs}
|
||||
/>
|
||||
<ThroughputField
|
||||
form={form}
|
||||
disableInput={disableIopsThroughputConfig || disableDiskInputs}
|
||||
/>
|
||||
<div className="flex flex-col gap-y-8 py-8">
|
||||
<div className="px-8 flex flex-col gap-y-8">
|
||||
<AutoScaleFields form={form} />
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="px-8 flex flex-col gap-y-8">
|
||||
<NoticeBar
|
||||
type="default"
|
||||
visible={disableIopsThroughputConfig}
|
||||
title="Adjusting disk configuration requires LARGE Compute size or above"
|
||||
description={`Increase your compute size to adjust your disk's storage type, ${form.getValues('storageType') === 'gp3' ? 'IOPS, ' : ''} and throughput`}
|
||||
actions={
|
||||
canUpdateDiskConfiguration ? (
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => {
|
||||
form.setValue('computeSize', 'ci_large')
|
||||
}}
|
||||
>
|
||||
Change to LARGE Compute
|
||||
</Button>
|
||||
) : (
|
||||
<RequestUpgradeToBillingOwners
|
||||
addon="computeSize"
|
||||
featureProposition="adjust disk configuration"
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<StorageTypeField
|
||||
form={form}
|
||||
disableInput={disableIopsThroughputConfig || disableDiskInputs}
|
||||
/>
|
||||
<IOPSField
|
||||
form={form}
|
||||
disableInput={disableIopsThroughputConfig || disableDiskInputs}
|
||||
/>
|
||||
<ThroughputField
|
||||
form={form}
|
||||
disableInput={disableIopsThroughputConfig || disableDiskInputs}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent_Shadcn_>
|
||||
</Collapsible_Shadcn_>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ScaffoldContainer>
|
||||
<AnimatePresence>
|
||||
{isDirty ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.1, delay: 0.2 }}
|
||||
className="z-10 w-full left-0 right-0 sticky bottom-0 bg-surface-100 border-t h-16 items-center flex"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
MAX_WIDTH_CLASSES,
|
||||
PADDING_CLASSES,
|
||||
'flex items-center gap-3 justify-end'
|
||||
)}
|
||||
</ScaffoldContainer>
|
||||
|
||||
<AnimatePresence>
|
||||
{isDirty ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.1, delay: 0.2 }}
|
||||
className="z-10 w-full left-0 right-0 sticky bottom-0 bg-surface-100 border-t h-16 items-center flex"
|
||||
>
|
||||
<FormFooterChangeBadge formState={formState} />
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => form.reset()}
|
||||
disabled={!isDirty}
|
||||
size="medium"
|
||||
<div
|
||||
className={cn(
|
||||
MAX_WIDTH_CLASSES,
|
||||
PADDING_CLASSES,
|
||||
'flex items-center gap-3 justify-end'
|
||||
)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<DiskManagementReviewAndSubmitDialog
|
||||
loading={isUpdatingConfig}
|
||||
disabled={noPermissions}
|
||||
form={form}
|
||||
numReplicas={readReplicas.length}
|
||||
isDialogOpen={isDialogOpen}
|
||||
onSubmit={onSubmit}
|
||||
setIsDialogOpen={setIsDialogOpen}
|
||||
message={message}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
<FormFooterChangeBadge formState={formState} />
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => form.reset()}
|
||||
disabled={!isDirty}
|
||||
size="medium"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<DiskManagementReviewAndSubmitDialog
|
||||
loading={isUpdatingConfig}
|
||||
disabled={noPermissions}
|
||||
form={form}
|
||||
numReplicas={readReplicas.length}
|
||||
isDialogOpen={isDialogOpen}
|
||||
onSubmit={onSubmit}
|
||||
setIsDialogOpen={setIsDialogOpen}
|
||||
message={message}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { LucideIcon } from 'lucide-react'
|
||||
import { ComponentProps } from 'react'
|
||||
import { type ComponentProps, type ReactNode } from 'react'
|
||||
import { cn } from 'ui'
|
||||
|
||||
import { Admonition } from 'ui-patterns'
|
||||
|
||||
interface NoticeBarProps extends Omit<ComponentProps<typeof Admonition>, 'description'> {
|
||||
title?: string
|
||||
description?: string
|
||||
icon?: LucideIcon
|
||||
icon?: ReactNode
|
||||
visible: boolean
|
||||
actions?: React.ReactNode
|
||||
actions?: ReactNode
|
||||
}
|
||||
export function NoticeBar({ visible, description, actions, ...props }: NoticeBarProps) {
|
||||
return (
|
||||
@@ -21,7 +21,7 @@ export function NoticeBar({ visible, description, actions, ...props }: NoticeBar
|
||||
exit={{ opacity: 0, height: 0, y: 4 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<Admonition {...props}>
|
||||
<Admonition {...props} className={cn(props.className, 'mb-0')}>
|
||||
{description}
|
||||
<div className="flex flex-col gap-2">
|
||||
{actions && <div className="mt-2">{actions}</div>}
|
||||
|
||||
@@ -27,7 +27,7 @@ export function SpendCapDisabledSection() {
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<Admonition type="default">
|
||||
<Admonition type="default" className="mb-0">
|
||||
<AlertTitle>Disable spend cap to manage Disk configuration</AlertTitle>
|
||||
<AlertDescription>
|
||||
To access disk management features, you need to disable your spend cap. These features
|
||||
|
||||
@@ -36,9 +36,11 @@ export const LogDrainsEmpty = () => {
|
||||
<p className="text-foreground-light mb-6">
|
||||
Upgrade to a Team or Enterprise Plan to send your logs to your preferred platform
|
||||
</p>
|
||||
<UpgradePlanButton type="primary" plan="Team" source="log-drains-empty-state">
|
||||
Upgrade plan
|
||||
</UpgradePlanButton>
|
||||
<UpgradePlanButton
|
||||
plan="Team"
|
||||
source="log-drains-empty-state"
|
||||
featureProposition="use Log Drains"
|
||||
/>
|
||||
</div>
|
||||
<Card
|
||||
className={cn('grid grid-cols-1 bg divide-x mb-8', IS_PLATFORM && '@xl:grid-cols-2')}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { FilterPopover } from 'components/ui/FilterPopover'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { UpgradePlanButton } from 'components/ui/UpgradePlanButton'
|
||||
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
|
||||
import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query'
|
||||
import {
|
||||
AuditLog,
|
||||
@@ -153,31 +153,13 @@ export const AuditLogs = () => {
|
||||
return (
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldSection isFullWidth>
|
||||
<Alert_Shadcn_
|
||||
variant="default"
|
||||
title="Organization Audit Logs are not available on Free or Pro plans"
|
||||
>
|
||||
<WarningIcon />
|
||||
<div className="flex flex-col md:flex-row pt-1 gap-4">
|
||||
<div className="grow">
|
||||
<AlertTitle_Shadcn_>
|
||||
Organization Audit Logs are not available on Free or Pro plans
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_ className="flex flex-row justify-between gap-3">
|
||||
<p>
|
||||
Upgrade to Team or Enterprise to view up to 62 days of Audit Logs for your
|
||||
organization.
|
||||
</p>
|
||||
</AlertDescription_Shadcn_>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<UpgradePlanButton source="auditLogs" plan="Team" type="primary">
|
||||
Upgrade subscription
|
||||
</UpgradePlanButton>
|
||||
</div>
|
||||
</div>
|
||||
</Alert_Shadcn_>
|
||||
<UpgradeToPro
|
||||
plan="Team"
|
||||
source="organizationAuditLogs"
|
||||
primaryText="Organization Audit Logs are not available on Free or Pro plans"
|
||||
secondaryText="Upgrade to Team or Enterprise to view up to 62 days of Audit Logs for your organization."
|
||||
featureProposition="enable audit logs"
|
||||
/>
|
||||
</ScaffoldSection>
|
||||
</ScaffoldContainer>
|
||||
)
|
||||
|
||||
+4
-1
@@ -157,7 +157,10 @@ export const BillingMetric = ({
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<UpgradePlanButton source={`billingBreakdownUsage${metric.anchor}`}>
|
||||
<UpgradePlanButton
|
||||
source={`billingBreakdownUsage${metric.anchor}`}
|
||||
featureProposition={`to use ${metric.name}`}
|
||||
>
|
||||
Upgrade
|
||||
</UpgradePlanButton>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -135,7 +135,7 @@ const DowngradeModal = ({
|
||||
<strong>Before you downgrade to the {selectedPlan?.name} plan, consider:</strong>
|
||||
<ul className="space-y-2 mt-2">
|
||||
<li className="list-disc ml-6 text-foreground-light">
|
||||
Your projects no longer require their respective add ons.
|
||||
Your projects no longer require their respective add-ons.
|
||||
</li>
|
||||
<li className="list-disc ml-6 text-foreground-light">
|
||||
Your resource consumption are well within the {selectedPlan?.name} plan's quota.
|
||||
|
||||
+7
-8
@@ -9,6 +9,7 @@ import { StudioPricingSidePanelOpenedEvent } from 'common/telemetry-constants'
|
||||
import { getPlanChangeType } from 'components/interfaces/Billing/Subscription/Subscription.utils'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import PartnerManagedResource from 'components/ui/PartnerManagedResource'
|
||||
import { RequestUpgradeToBillingOwners } from 'components/ui/RequestUpgradeToBillingOwners'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query'
|
||||
import { useOrganizationBillingSubscriptionPreview } from 'data/organizations/organization-billing-subscription-preview'
|
||||
@@ -232,6 +233,8 @@ export const PlanUpdateSidePanel = () => {
|
||||
<Button block disabled type="default">
|
||||
Current plan
|
||||
</Button>
|
||||
) : !canUpdateSubscription ? (
|
||||
<RequestUpgradeToBillingOwners block plan={plan.name as 'Pro' | 'Team'} />
|
||||
) : (
|
||||
<ButtonTooltip
|
||||
block
|
||||
@@ -243,8 +246,7 @@ export const PlanUpdateSidePanel = () => {
|
||||
plan.id !== 'tier_free') ||
|
||||
// Orgs managed by AWS marketplace are not allowed to change the plan
|
||||
selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE ||
|
||||
hasOrioleProjects ||
|
||||
!canUpdateSubscription
|
||||
hasOrioleProjects
|
||||
}
|
||||
onClick={() => {
|
||||
setSelectedTier(plan.id as any)
|
||||
@@ -266,12 +268,9 @@ export const PlanUpdateSidePanel = () => {
|
||||
? 'Reach out to us via support to update your plan from Enterprise'
|
||||
: hasOrioleProjects
|
||||
? 'Your organization has projects that are using the OrioleDB extension which is only available on the Free plan. Remove all OrioleDB projects before changing your plan.'
|
||||
: !canUpdateSubscription
|
||||
? 'You do not have permission to change the subscription plan'
|
||||
: selectedOrganization?.managed_by ===
|
||||
MANAGED_BY.AWS_MARKETPLACE
|
||||
? 'You cannot change the plan for an organization managed by AWS Marketplace'
|
||||
: undefined,
|
||||
: selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE
|
||||
? 'You cannot change the plan for an organization managed by AWS Marketplace'
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -7,16 +7,13 @@ import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { InlineLink } from 'components/ui/InlineLink'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { UpgradePlanButton } from 'components/ui/UpgradePlanButton'
|
||||
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
|
||||
import { useSSOConfigCreateMutation } from 'data/sso/sso-config-create-mutation'
|
||||
import { useOrgSSOConfigQuery } from 'data/sso/sso-config-query'
|
||||
import { useSSOConfigUpdateMutation } from 'data/sso/sso-config-update-mutation'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { DOCS_URL } from 'lib/constants'
|
||||
import {
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Alert_Shadcn_,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -25,7 +22,6 @@ import {
|
||||
FormField_Shadcn_,
|
||||
Form_Shadcn_,
|
||||
Switch,
|
||||
WarningIcon,
|
||||
} from 'ui'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { AttributeMapping } from './AttributeMapping'
|
||||
@@ -159,30 +155,13 @@ export const SSOConfig = () => {
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldSection isFullWidth>
|
||||
{!!plan && !canSetupSSOConfig ? (
|
||||
<Alert_Shadcn_
|
||||
variant="default"
|
||||
title="Organization MFA enforcement is not available on Free plan"
|
||||
>
|
||||
<WarningIcon />
|
||||
<div className="flex flex-col md:flex-row pt-1 gap-4">
|
||||
<div className="grow">
|
||||
<AlertTitle_Shadcn_>
|
||||
Organization Single Sign-on (SSO) is available from Team plan and above
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_ className="flex flex-row justify-between gap-3">
|
||||
<p className="max-w-3xl">
|
||||
SSO as a login option provides additional acccount security for your team by
|
||||
enforcing the use of an identity provider when logging into Supabase. Upgrade to
|
||||
Team or above to set up SSO for your organization.
|
||||
</p>
|
||||
</AlertDescription_Shadcn_>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<UpgradePlanButton type="primary" source="sso" plan="Team" />
|
||||
</div>
|
||||
</div>
|
||||
</Alert_Shadcn_>
|
||||
<UpgradeToPro
|
||||
plan="Team"
|
||||
source="organizationSso"
|
||||
primaryText="Organization Single Sign-on (SSO) is available from Team plan and above"
|
||||
secondaryText="SSO as a login option provides additional acccount security for your team by enforcing the use of an identity provider when logging into Supabase. Upgrade to Team or above to set up SSO for your organization."
|
||||
featureProposition="enable Single Sign-on (SSO)"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{isLoadingSSOConfig && (
|
||||
|
||||
@@ -11,7 +11,7 @@ import AlertError from 'components/ui/AlertError'
|
||||
import { InlineLink } from 'components/ui/InlineLink'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { UpgradePlanButton } from 'components/ui/UpgradePlanButton'
|
||||
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
|
||||
import { useOrganizationMembersQuery } from 'data/organizations/organization-members-query'
|
||||
import { useOrganizationMfaToggleMutation } from 'data/organizations/organization-mfa-mutation'
|
||||
import { useOrganizationMfaQuery } from 'data/organizations/organization-mfa-query'
|
||||
@@ -20,9 +20,6 @@ import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import {
|
||||
Alert_Shadcn_,
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -34,7 +31,6 @@ import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
WarningIcon,
|
||||
} from 'ui'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
|
||||
@@ -112,28 +108,12 @@ export const SecuritySettings = () => {
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldSection isFullWidth>
|
||||
{!isPaidPlan ? (
|
||||
<Alert_Shadcn_
|
||||
variant="default"
|
||||
title="Organization MFA enforcement is not available on Free plan"
|
||||
>
|
||||
<WarningIcon />
|
||||
<div className="flex flex-col md:flex-row pt-1 gap-4">
|
||||
<div className="grow">
|
||||
<AlertTitle_Shadcn_>
|
||||
Organization MFA enforcement is not available on Free plan
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_ className="flex flex-row justify-between gap-3">
|
||||
<p>Upgrade to Pro or above to enforce MFA requirements for your organization.</p>
|
||||
</AlertDescription_Shadcn_>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<UpgradePlanButton type="primary" source="mfa" plan="Pro">
|
||||
Upgrade subscription
|
||||
</UpgradePlanButton>
|
||||
</div>
|
||||
</div>
|
||||
</Alert_Shadcn_>
|
||||
<UpgradeToPro
|
||||
source="organizationMfa"
|
||||
primaryText="Organization MFA enforcement is not available on Free plan"
|
||||
secondaryText="Upgrade to Pro or above to enforce MFA requirements for your organization."
|
||||
featureProposition="enforce MFA requirements"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{isLoadingMfa || isLoadingPermissions ? (
|
||||
|
||||
@@ -311,7 +311,11 @@ export const InviteMemberButton = () => {
|
||||
</Link>
|
||||
</Button>
|
||||
{(currentPlan?.id === 'free' || currentPlan?.id === 'pro') && (
|
||||
<UpgradePlanButton source="inviteMemberSSO" plan="Team" />
|
||||
<UpgradePlanButton
|
||||
plan="Team"
|
||||
source="inviteMemberSSO"
|
||||
featureProposition="enable Single Sign-on (SSO)"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,10 @@ export const FreeProjectLimitWarning = ({ membersExceededLimit }: FreeProjectLim
|
||||
projects before you're able to create a free project within this organization.
|
||||
</p>
|
||||
|
||||
<UpgradePlanButton source="freeProjectLimitWarning">Upgrade plan</UpgradePlanButton>
|
||||
<UpgradePlanButton
|
||||
source="freeProjectLimitWarning"
|
||||
featureProposition="create more projects"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { SubmitHandler, useForm } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
@@ -20,7 +21,6 @@ import {
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -399,7 +399,11 @@ export const RealtimeSettings = () => {
|
||||
</div>
|
||||
<div className="flex-grow flex items-center justify-end">
|
||||
{isFreePlan ? (
|
||||
<UpgradePlanButton source="realtimeSettings" plan="Pro" />
|
||||
<UpgradePlanButton
|
||||
source="realtimeSettings"
|
||||
addon="spendCap"
|
||||
featureProposition="configure the max events per second parameter of realtime settings"
|
||||
/>
|
||||
) : (
|
||||
<ToggleSpendCapButton />
|
||||
)}
|
||||
@@ -457,7 +461,11 @@ export const RealtimeSettings = () => {
|
||||
</div>
|
||||
<div className="flex-grow flex items-center justify-end">
|
||||
{isFreePlan ? (
|
||||
<UpgradePlanButton source="realtimeSettings" plan="Pro" />
|
||||
<UpgradePlanButton
|
||||
source="realtimeSettings"
|
||||
addon="spendCap"
|
||||
featureProposition="configure the max presence events per second parameter of realtime settings"
|
||||
/>
|
||||
) : (
|
||||
<ToggleSpendCapButton />
|
||||
)}
|
||||
@@ -515,7 +523,11 @@ export const RealtimeSettings = () => {
|
||||
</div>
|
||||
<div className="flex-grow flex items-center justify-end">
|
||||
{isFreePlan ? (
|
||||
<UpgradePlanButton source="realtimeSettings" plan="Pro" />
|
||||
<UpgradePlanButton
|
||||
addon="spendCap"
|
||||
source="realtimeSettings"
|
||||
featureProposition="configure the max payload size parameter of realtime settings"
|
||||
/>
|
||||
) : (
|
||||
<ToggleSpendCapButton />
|
||||
)}
|
||||
|
||||
@@ -8,13 +8,14 @@ import { toast } from 'sonner'
|
||||
import { useParams } from 'common'
|
||||
import { subscriptionHasHipaaAddon } from 'components/interfaces/Billing/Subscription/Subscription.utils'
|
||||
import { SupportLink } from 'components/interfaces/Support/SupportLink'
|
||||
import { UpgradePlanButton } from 'components/ui/UpgradePlanButton'
|
||||
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
|
||||
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
|
||||
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
|
||||
import { useProjectAddonRemoveMutation } from 'data/subscriptions/project-addon-remove-mutation'
|
||||
import { useProjectAddonUpdateMutation } from 'data/subscriptions/project-addon-update-mutation'
|
||||
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
|
||||
import type { AddonVariantId } from 'data/subscriptions/types'
|
||||
import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
@@ -22,7 +23,6 @@ import { BASE_PATH, DOCS_URL } from 'lib/constants'
|
||||
import { formatCurrency } from 'lib/helpers'
|
||||
import { useAddonsPagePanel } from 'state/addons-page'
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Alert_Shadcn_,
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
SidePanel,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
|
||||
|
||||
const PITR_CATEGORY_OPTIONS: {
|
||||
id: 'off' | 'on'
|
||||
@@ -272,32 +271,21 @@ const PITRSidePanel = () => {
|
||||
{selectedCategory === 'on' && (
|
||||
<div className="!mt-8 pb-4">
|
||||
{!hasAccessToPitrVariants ? (
|
||||
<Alert
|
||||
withIcon
|
||||
variant="info"
|
||||
<UpgradeToPro
|
||||
className="mb-4"
|
||||
title="Changing your Point-In-Time-Recovery is only available on the Pro Plan"
|
||||
actions={<UpgradePlanButton source="pitrSidePanel" plan="Pro" />}
|
||||
>
|
||||
Upgrade your plan to change PITR for your project
|
||||
</Alert>
|
||||
addon="pitr"
|
||||
primaryText="Changing your Point-In-Time-Recovery is only available on the Pro Plan"
|
||||
secondaryText="Upgrade your plan to change PITR for your project"
|
||||
featureProposition="enable PITR"
|
||||
/>
|
||||
) : !hasSufficientCompute ? (
|
||||
<Alert
|
||||
withIcon
|
||||
variant="warning"
|
||||
<UpgradeToPro
|
||||
className="mb-4"
|
||||
title="Your project is required to minimally be on a Small compute size to enable PITR"
|
||||
actions={[
|
||||
<Button asChild key="change-compute" type="default">
|
||||
<Link href={`/project/${projectRef}/settings/compute-and-disk`}>
|
||||
Change compute size
|
||||
</Link>
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
This is to ensure that your project has enough resources to execute PITR
|
||||
successfully
|
||||
</Alert>
|
||||
addon="computeSize"
|
||||
primaryText="Project needs to be at least on a Small compute size to enable PITR"
|
||||
secondaryText="This ensures enough resources to execute PITR successfully"
|
||||
featureProposition="enable PITR"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Radio.Group
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@ import { SupportLink } from 'components/interfaces/Support/SupportLink'
|
||||
import { FormHeader } from 'components/ui/Forms/FormHeader'
|
||||
import { InlineLinkClassName } from 'components/ui/InlineLink'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import UpgradeToPro from 'components/ui/UpgradeToPro'
|
||||
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
|
||||
import {
|
||||
useCustomDomainsQuery,
|
||||
type CustomDomainsData,
|
||||
@@ -63,7 +63,6 @@ export const CustomDomainConfig = () => {
|
||||
</Panel>
|
||||
) : !hasCustomDomainAddon ? (
|
||||
<UpgradeToPro
|
||||
icon={<AlertCircle size={18} strokeWidth={1.5} />}
|
||||
primaryText={
|
||||
customDomainsDisabledDueToQuota
|
||||
? 'New custom domains are temporarily disabled'
|
||||
@@ -74,10 +73,11 @@ export const CustomDomainConfig = () => {
|
||||
? 'We are working with our upstream DNS provider before we are able to sign up new custom domains. Please check back in a few hours.'
|
||||
: plan === 'free'
|
||||
? 'Paid Plans come with free vanity subdomains or Custom Domains for an additional $10/month per domain.'
|
||||
: 'To configure a custom domain for your project, please enable the add-on. Each Custom Domains costs $10 per month.'
|
||||
: 'To configure a custom domain for your project, please enable the add-on. Each Custom Domain costs $10 per month.'
|
||||
}
|
||||
addon="customDomain"
|
||||
source="customDomain"
|
||||
featureProposition="enable custom domains"
|
||||
disabled={customDomainsDisabledDueToQuota}
|
||||
/>
|
||||
) : isCustomDomainsLoading ? (
|
||||
|
||||
+8
-6
@@ -9,7 +9,7 @@ import {
|
||||
ScaffoldSectionDetail,
|
||||
} from 'components/layouts/Scaffold'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
import UpgradeToPro from 'components/ui/UpgradeToPro'
|
||||
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
|
||||
import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
@@ -71,18 +71,20 @@ const GitHubSection = () => {
|
||||
branch, keep your production branch in sync, and automatically create preview
|
||||
branches for every pull request.
|
||||
</p>
|
||||
{promptProPlanUpgrade && (
|
||||
{promptProPlanUpgrade ? (
|
||||
<div className="mb-6">
|
||||
<UpgradeToPro
|
||||
source="github-integration"
|
||||
featureProposition="use GitHub integrations"
|
||||
primaryText="Upgrade to unlock GitHub integration"
|
||||
secondaryText="Connect your GitHub repository to automatically sync preview branches and deploy changes."
|
||||
source="github-integration"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={cn(promptProPlanUpgrade && 'opacity-25 pointer-events-none')}>
|
||||
<GitHubIntegrationConnectionForm connection={existingConnection} />
|
||||
</div>
|
||||
)}
|
||||
<div className={cn(promptProPlanUpgrade && 'opacity-25 pointer-events-none')}>
|
||||
<GitHubIntegrationConnectionForm connection={existingConnection} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -29,7 +29,10 @@ export const BucketsUpgradePlan = ({ type }: { type: 'analytics' | 'vector' }) =
|
||||
description={`Upgrade to Pro to use ${type} buckets for your project`}
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<UpgradePlanButton type="primary" plan="Pro" />
|
||||
<UpgradePlanButton
|
||||
source={`${type}Buckets`}
|
||||
featureProposition={`use ${type} buckets`}
|
||||
/>
|
||||
</div>
|
||||
</EmptyStatePresentational>
|
||||
</PageSectionContent>
|
||||
|
||||
@@ -11,7 +11,7 @@ import AlertError from 'components/ui/AlertError'
|
||||
import { InlineLink } from 'components/ui/InlineLink'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import UpgradeToPro from 'components/ui/UpgradeToPro'
|
||||
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
|
||||
import { useProjectStorageConfigQuery } from 'data/config/project-storage-config-query'
|
||||
import { useProjectStorageConfigUpdateUpdateMutation } from 'data/config/project-storage-config-update-mutation'
|
||||
import { useBucketsQuery } from 'data/storage/buckets-query'
|
||||
@@ -408,22 +408,25 @@ export const StorageSettings = () => {
|
||||
{isFreeTier && (
|
||||
<UpgradeToPro
|
||||
fullWidth
|
||||
source="storageSizeLimit"
|
||||
featureProposition="configure upload file size limits in Storage"
|
||||
primaryText="Free Plan has a fixed upload file size limit of 50 MB."
|
||||
secondaryText={`Upgrade to Pro Plan for a configurable upload file size limit of ${formatBytes(
|
||||
STORAGE_FILE_SIZE_LIMIT_MAX_BYTES_UNCAPPED
|
||||
)} and unlock image transformations.`}
|
||||
source="storageSizeLimit"
|
||||
/>
|
||||
)}
|
||||
{isSpendCapOn && (
|
||||
<UpgradeToPro
|
||||
fullWidth
|
||||
addon="spendCap"
|
||||
source="storageSizeLimit"
|
||||
featureProposition="increase the file upload size limits in Storage"
|
||||
buttonText="Disable Spend Cap"
|
||||
primaryText="Reduced max upload file size limit due to Spend Cap"
|
||||
secondaryText={`Disable your Spend Cap to allow file uploads of up to ${formatBytes(
|
||||
STORAGE_FILE_SIZE_LIMIT_MAX_BYTES_UNCAPPED
|
||||
)}.`}
|
||||
source="storageSizeLimit"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { PropsWithChildren, useState } from 'react'
|
||||
import { SubmitHandler, useForm } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
import z from 'zod'
|
||||
|
||||
import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query'
|
||||
import { useOrganizationMembersQuery } from 'data/organizations/organization-members-query'
|
||||
import {
|
||||
PlanRequest,
|
||||
useSendUpgradeRequestMutation,
|
||||
} from 'data/organizations/request-upgrade-mutation'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogSection,
|
||||
DialogSectionSeparator,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
Form_Shadcn_,
|
||||
FormControl_Shadcn_,
|
||||
FormField_Shadcn_,
|
||||
TextArea_Shadcn_,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from 'ui'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
|
||||
const FormSchema = z.object({
|
||||
note: z.string().optional(),
|
||||
})
|
||||
|
||||
const formId = 'request-upgrade-form'
|
||||
|
||||
interface RequestUpgradeToBillingOwnersProps {
|
||||
block?: boolean
|
||||
plan?: PlanRequest
|
||||
addon?: 'pitr' | 'customDomain' | 'spendCap' | 'computeSize'
|
||||
/** Used in the default message template, e.g: "Upgrade to ..." */
|
||||
featureProposition?: string
|
||||
}
|
||||
|
||||
export const RequestUpgradeToBillingOwners = ({
|
||||
block = false,
|
||||
plan = 'Pro',
|
||||
addon,
|
||||
featureProposition,
|
||||
children,
|
||||
}: PropsWithChildren<RequestUpgradeToBillingOwnersProps>) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { data: organization } = useSelectedOrganizationQuery()
|
||||
const slug = organization?.slug
|
||||
const isFreePlan = organization?.plan?.id === 'free'
|
||||
|
||||
const { data: members = [] } = useOrganizationMembersQuery({ slug: organization?.slug })
|
||||
const { data: roles } = useOrganizationRolesV2Query({ slug: organization?.slug })
|
||||
const orgRoles = roles?.org_scoped_roles ?? []
|
||||
|
||||
const { mutate: sendUpgradeRequest, isPending: isSubmitting } = useSendUpgradeRequestMutation({
|
||||
onSuccess: () => {
|
||||
toast.success('Successfully sent request to billing owners!')
|
||||
setOpen(false)
|
||||
},
|
||||
})
|
||||
|
||||
const formattedAddonName =
|
||||
addon === 'pitr' ? 'PITR' : addon === 'customDomain' ? 'Custom Domain' : ''
|
||||
|
||||
const target = !!project
|
||||
? `for the project "${project?.name}"`
|
||||
: !!organization
|
||||
? `for the organization "${organization.name}"`
|
||||
: ''
|
||||
const action =
|
||||
addon === 'spendCap'
|
||||
? `disable spend cap`
|
||||
: addon === 'computeSize'
|
||||
? `change the compute size`
|
||||
: `enable the ${formattedAddonName} add-on`
|
||||
const titleText = !!addon
|
||||
? addon === 'spendCap'
|
||||
? `Request to disable spend cap`
|
||||
: addon === 'computeSize'
|
||||
? 'Request to change compute size'
|
||||
: `Request to enable the ${formattedAddonName} add-on`
|
||||
: `Request an upgrade for the ${plan} Plan`
|
||||
const buttonText = !!children
|
||||
? children
|
||||
: !!addon
|
||||
? addon === 'spendCap'
|
||||
? 'Request to disable spend cap'
|
||||
: addon === 'computeSize'
|
||||
? 'Request to change compute'
|
||||
: 'Request to enable addon'
|
||||
: `Request upgrade to ${plan}`
|
||||
|
||||
const defaultValues = {
|
||||
note: !!addon
|
||||
? addon === 'spendCap'
|
||||
? `We'd like to ${isFreePlan ? 'upgrade to Pro and ' : ''}${action} ${target} so that we can ${featureProposition}`
|
||||
: `We'd like to ${isFreePlan ? 'upgrade to Pro and ' : ''}${action} ${target} so that we can ${featureProposition}`
|
||||
: `We'd like to upgrade to the ${plan} plan ${!!featureProposition ? ` to ${featureProposition} ` : ''}${target}`,
|
||||
}
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues,
|
||||
values: defaultValues,
|
||||
})
|
||||
|
||||
// [Joshen] This is a pretty naive way of checking billing owners by raw role names
|
||||
// Ideally we derive billing owners using permissions checking - but the current permissions
|
||||
// logic is only contextualized to that of the current user, not other members
|
||||
const billingOwners = members.filter((member) => {
|
||||
const roles = member.role_ids
|
||||
.map((x) => orgRoles.find((role) => role.id === x)?.name)
|
||||
.filter(Boolean)
|
||||
return !member.invited_id && (roles.includes('Owner') || roles.includes('Administrator'))
|
||||
})
|
||||
|
||||
const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (values) => {
|
||||
if (!slug) return console.error('Slug is required')
|
||||
sendUpgradeRequest({ slug, plan, note: values.note })
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button block={block} type="primary">
|
||||
{buttonText}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form id={formId} onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{titleText}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Let your organization's billing owners know your interest in this
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogSectionSeparator />
|
||||
|
||||
<DialogSection className="flex flex-col gap-y-6">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<p className="text-sm">
|
||||
Your request will be sent to the following emails, who are billing owners of your
|
||||
organization:
|
||||
</p>
|
||||
<div className="text-sm flex gap-x-2">
|
||||
<p>
|
||||
{billingOwners
|
||||
.slice(0, 2)
|
||||
.map((x) => x.primary_email)
|
||||
.join(', ')}
|
||||
</p>
|
||||
{billingOwners.length > 2 && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger tabIndex={-1}>
|
||||
<Badge>+1 others</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<ul className="">
|
||||
{billingOwners.slice(2).map((x) => (
|
||||
<li key={x.gotrue_id}>{x.primary_email}</li>
|
||||
))}
|
||||
</ul>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="note"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
name="note"
|
||||
label="Add a note to your request (optional)"
|
||||
layout="vertical"
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<TextArea_Shadcn_
|
||||
id="note"
|
||||
{...field}
|
||||
rows={3}
|
||||
placeholder={
|
||||
!!addon
|
||||
? addon === 'spendCap'
|
||||
? 'e.g. We need to disabled spend cap on this project to do something'
|
||||
: 'e.g. We need to enable this add-on to do something with the project'
|
||||
: 'e.g. We need to upgrade to the Pro plan to use this feature'
|
||||
}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</DialogSection>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="default" disabled={isSubmitting} onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button htmlType="submit" form={formId} loading={isSubmitting}>
|
||||
Submit request
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,43 +1,69 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import Link from 'next/link'
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
import { useFlag, useParams } from 'common'
|
||||
import { SupportLink } from 'components/interfaces/Support/SupportLink'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { Button } from 'ui'
|
||||
import { ButtonTooltip } from './ButtonTooltip'
|
||||
import { RequestUpgradeToBillingOwners } from './RequestUpgradeToBillingOwners'
|
||||
|
||||
export const PLAN_REQUEST_EMPTY_PLACEHOLDER =
|
||||
'<Specify which plan to upgrade to: Pro | Team | Enterprise>'
|
||||
|
||||
interface UpgradePlanButtonProps {
|
||||
source?: string
|
||||
/** Stick to camel case for consistency */
|
||||
source: string
|
||||
type?: 'default' | 'primary'
|
||||
plan?: 'Pro' | 'Team' | 'Enterprise'
|
||||
href?: string // [Joshen] As an override if needed (Used in UpgradeToPro)
|
||||
addon?: 'pitr' | 'customDomain' | 'spendCap' | 'computeSize'
|
||||
/** Used in the default message template for request upgrade dialog, e.g: "Upgrade to ..." */
|
||||
featureProposition?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* If `billingAll` is enabled, links to upgrade paths (e.g organization settings, addons).
|
||||
*
|
||||
* Otherwise, links to support form instead
|
||||
*/
|
||||
export const UpgradePlanButton = ({
|
||||
source,
|
||||
type = 'default',
|
||||
plan,
|
||||
href: propsHref,
|
||||
type = 'primary',
|
||||
plan = 'Pro',
|
||||
addon,
|
||||
featureProposition,
|
||||
disabled,
|
||||
children,
|
||||
}: PropsWithChildren<UpgradePlanButtonProps>) => {
|
||||
const { ref } = useParams()
|
||||
const { data: organization } = useSelectedOrganizationQuery()
|
||||
const isFreePlan = organization?.plan?.id === 'free'
|
||||
const slug = organization?.slug ?? '_'
|
||||
|
||||
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
|
||||
const { billingAll } = useIsFeatureEnabled(['billing:all'])
|
||||
|
||||
const { can: canUpdateSubscription } = useAsyncCheckPermissions(
|
||||
PermissionAction.BILLING_WRITE,
|
||||
'stripe.subscriptions'
|
||||
)
|
||||
|
||||
const subject = `Enquiry to upgrade ${!!plan ? `to ${plan} ` : ''}plan for organization`
|
||||
const message = `Name: ${organization?.name}\nSlug: ${organization?.slug}\nRequested plan: ${plan ?? PLAN_REQUEST_EMPTY_PLACEHOLDER}`
|
||||
const message = `Name: ${organization?.name}\nSlug: ${slug}\nRequested plan: ${plan ?? PLAN_REQUEST_EMPTY_PLACEHOLDER}`
|
||||
|
||||
const href = billingAll
|
||||
? propsHref ??
|
||||
`/org/${slug}/billing?panel=subscriptionPlan${!!source ? `&source=${source}` : ''}`
|
||||
: ''
|
||||
const linkChildren = children || `Upgrade to ${plan}`
|
||||
? isFreePlan
|
||||
? `/org/${slug ?? '_'}/billing?panel=subscriptionPlan&source=${source}`
|
||||
: addon === 'spendCap'
|
||||
? `/org/${slug ?? '_'}/billing?panel=costControl&source=${source}`
|
||||
: `/project/${ref ?? '_'}/settings/addons?panel=${addon}&source=${source}`
|
||||
: '/'
|
||||
|
||||
const linkChildren = children || (!!addon ? 'Enable add-on' : `Upgrade to ${plan}`)
|
||||
const link = billingAll ? (
|
||||
<Link href={href}>{linkChildren}</Link>
|
||||
) : (
|
||||
@@ -46,6 +72,35 @@ export const UpgradePlanButton = ({
|
||||
</SupportLink>
|
||||
)
|
||||
|
||||
if (!canUpdateSubscription) {
|
||||
return (
|
||||
<RequestUpgradeToBillingOwners
|
||||
plan={plan}
|
||||
addon={addon}
|
||||
featureProposition={featureProposition}
|
||||
>
|
||||
{children}
|
||||
</RequestUpgradeToBillingOwners>
|
||||
)
|
||||
}
|
||||
|
||||
if (projectUpdateDisabled) {
|
||||
return (
|
||||
<ButtonTooltip
|
||||
disabled
|
||||
type="primary"
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: 'Plan changes are currently disabled, our engineers are working on a fix',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{linkChildren}
|
||||
</ButtonTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button asChild type={type} disabled={disabled}>
|
||||
{link}
|
||||
|
||||
@@ -1,100 +1,65 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
import { useFlag } from 'common'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { cn } from 'ui'
|
||||
import { ButtonTooltip } from './ButtonTooltip'
|
||||
import { Admonition } from 'ui-patterns'
|
||||
import { UpgradePlanButton } from './UpgradePlanButton'
|
||||
|
||||
interface UpgradeToProProps {
|
||||
icon?: ReactNode
|
||||
primaryText: string
|
||||
secondaryText: string
|
||||
addon?: 'pitr' | 'customDomain'
|
||||
plan?: 'Pro' | 'Team' | 'Enterprise'
|
||||
addon?: 'pitr' | 'customDomain' | 'spendCap' | 'computeSize'
|
||||
/** Used in the default message template for request upgrade dialog, e.g: "Upgrade to ..." */
|
||||
featureProposition?: string
|
||||
/** As an override for the button text in both upgrade + request to upgrade scenario */
|
||||
buttonText?: string
|
||||
/** Where the upgrade interest is coming from */
|
||||
source?: string
|
||||
disabled?: boolean
|
||||
fullWidth?: boolean
|
||||
className?: string
|
||||
layout?: 'vertical' | 'horizontal'
|
||||
}
|
||||
|
||||
const UpgradeToPro = ({
|
||||
export const UpgradeToPro = ({
|
||||
icon,
|
||||
primaryText,
|
||||
secondaryText,
|
||||
plan: planToUpgrade = 'Pro',
|
||||
addon,
|
||||
featureProposition,
|
||||
buttonText,
|
||||
source = 'upgrade',
|
||||
disabled = false,
|
||||
fullWidth = false,
|
||||
layout = 'horizontal',
|
||||
className,
|
||||
}: UpgradeToProProps) => {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { data: organization } = useSelectedOrganizationQuery()
|
||||
const plan = organization?.plan?.id
|
||||
|
||||
const { can: canUpdateSubscription } = useAsyncCheckPermissions(
|
||||
PermissionAction.BILLING_WRITE,
|
||||
'stripe.subscriptions'
|
||||
)
|
||||
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
|
||||
|
||||
return (
|
||||
<div
|
||||
<Admonition
|
||||
type="default"
|
||||
icon={icon}
|
||||
layout={layout}
|
||||
title={primaryText}
|
||||
description={secondaryText}
|
||||
className={cn(
|
||||
'block w-full py-4 px-6 bg-surface-200',
|
||||
fullWidth ? 'border-b' : 'border rounded'
|
||||
'bg-surface-100 mb-0',
|
||||
fullWidth &&
|
||||
'border-0 rounded-none border-b px-6 [&>svg]:left-6 [&>div>button]:w-min [&>div>button]:mt-3 [&>div>a]:w-min [&>div>a]:mt-3',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-x-3">
|
||||
{icon && <div className="mt-1">{icon}</div>}
|
||||
<div className="flex flex-col @md:flex-row w-full @md:items-center justify-between gap-4 @md:gap-x-8">
|
||||
<div className="space-y-1 flex-1 max-w-2xl">
|
||||
<p className="text-sm">{primaryText}</p>
|
||||
<div>
|
||||
<p className="text-sm text-foreground-light">{secondaryText}</p>
|
||||
</div>
|
||||
</div>
|
||||
{!canUpdateSubscription || projectUpdateDisabled ? (
|
||||
<ButtonTooltip
|
||||
disabled
|
||||
type="primary"
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: projectUpdateDisabled
|
||||
? 'Subscription changes are currently disabled, our engineers are working on a fix'
|
||||
: !canUpdateSubscription
|
||||
? 'You need additional permissions to amend subscriptions'
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{buttonText || (plan === 'free' ? 'Upgrade to Pro' : 'Enable add on')}
|
||||
</ButtonTooltip>
|
||||
) : (
|
||||
<UpgradePlanButton
|
||||
type="primary"
|
||||
plan="Pro"
|
||||
source={source}
|
||||
disabled={disabled}
|
||||
href={
|
||||
plan === 'free'
|
||||
? `/org/${organization?.slug ?? '_'}/billing?panel=subscriptionPlan&source=${source}`
|
||||
: addon == null
|
||||
? `/org/${organization?.slug ?? '_'}/billing?panel=costControl&source=${source}`
|
||||
: `/project/${project?.ref ?? '_'}/settings/addons?panel=${addon}&source=${source}`
|
||||
}
|
||||
>
|
||||
{buttonText || (plan === 'free' ? 'Upgrade to Pro' : 'Enable add on')}
|
||||
</UpgradePlanButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
actions={
|
||||
<UpgradePlanButton
|
||||
plan={planToUpgrade}
|
||||
addon={addon}
|
||||
source={source}
|
||||
featureProposition={featureProposition}
|
||||
disabled={disabled}
|
||||
>
|
||||
{buttonText}
|
||||
</UpgradePlanButton>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpgradeToPro
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { components } from 'api-types'
|
||||
import { handleError, post } from 'data/fetchers'
|
||||
import type { ResponseError, UseCustomMutationOptions } from 'types'
|
||||
|
||||
export type PlanRequest = components['schemas']['RequestUpgradeBody']['requested_plan']
|
||||
|
||||
type SendUpgradeRequestVariables = {
|
||||
slug?: string
|
||||
plan: PlanRequest
|
||||
note?: string
|
||||
}
|
||||
|
||||
async function sendUpgradeRequest({ slug, plan, note }: SendUpgradeRequestVariables) {
|
||||
if (!slug) throw new Error('Slug is required')
|
||||
|
||||
const { data, error } = await post('/platform/organizations/{slug}/billing/upgrade-request', {
|
||||
params: { path: { slug } },
|
||||
body: { requested_plan: plan, note },
|
||||
})
|
||||
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
type SendUpgradeRequestData = Awaited<ReturnType<typeof sendUpgradeRequest>>
|
||||
|
||||
export const useSendUpgradeRequestMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseCustomMutationOptions<SendUpgradeRequestData, ResponseError, SendUpgradeRequestVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
return useMutation<SendUpgradeRequestData, ResponseError, SendUpgradeRequestVariables>({
|
||||
mutationFn: (vars) => sendUpgradeRequest(vars),
|
||||
async onSuccess(data, variables, context) {
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to send upgrade request: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import AlertError from 'components/ui/AlertError'
|
||||
import { DocsButton } from 'components/ui/DocsButton'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import UpgradeToPro from 'components/ui/UpgradeToPro'
|
||||
import { UpgradeToPro } from 'components/ui/UpgradeToPro'
|
||||
import { useBackupsQuery } from 'data/database/backups-query'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
||||
@@ -104,6 +104,7 @@ const PITR = () => {
|
||||
<UpgradeToPro
|
||||
addon="pitr"
|
||||
source="pitr"
|
||||
featureProposition="enable Point in Time Recovery"
|
||||
primaryText="Point in Time Recovery is a Pro Plan add-on."
|
||||
secondaryText={
|
||||
plan === 'free'
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface AdmonitionProps {
|
||||
}
|
||||
layout?: 'horizontal' | 'vertical'
|
||||
actions?: ReactNode
|
||||
icon?: ReactNode
|
||||
}
|
||||
|
||||
const admonitionToAlertMapping: Record<
|
||||
@@ -104,6 +105,7 @@ export const Admonition = forwardRef<
|
||||
layout = 'vertical',
|
||||
actions,
|
||||
childProps = {},
|
||||
icon,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@@ -122,7 +124,9 @@ export const Admonition = forwardRef<
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
{(showIcon && typeMapped === 'warning') || typeMapped === 'destructive' ? (
|
||||
{!!icon ? (
|
||||
icon
|
||||
) : (showIcon && typeMapped === 'warning') || typeMapped === 'destructive' ? (
|
||||
<WarningIcon />
|
||||
) : showIcon ? (
|
||||
<InfoIcon />
|
||||
|
||||
Reference in new issue
Block a user