Files
Danny WhiteandJoshen Lim a052dd7409 chore(studio): clarify dedicated pooler compatibility (#43320)
## What kind of change does this PR introduce?

UI clarification.

## What is the current behavior?

Our messaging around the compatibility of:

- Dedicated pooler
- Shared pooler

...in conjunction with the following connection types:

- IPv4
- IPv6
- Direct connection

...is hard to parse.

## What is the new behavior?

Copywriting clarifications to all of the above. Plus:

- Broader copywriting clarifications to `IPV4SidePanel`
- Replace deprecated `Radio` component with `RadioGroupItem_Shadcn_` as
the former has little-to-no visual difference between unselected and
selected states
- Fixed `SidePanel` header gap and button labelling

## Additional context

| Before | After |
| --- | --- |
| <img width="1024" height="563" alt="AWS Healthy Toolshed
Supabase-4587771A-83A2-40E8-845B-256CE7E1A7DB"
src="https://github.com/user-attachments/assets/09591fa1-48b8-4f3c-aaef-733af8dbdc9a"
/> | <img width="1024" height="563" alt="AWS Healthy Toolshed
Supabase-D56A2209-7421-43C2-A7C1-6C6965B3F008"
src="https://github.com/user-attachments/assets/13a05e51-02a5-486a-9549-4c5fe049c65a"
/> |
| <img width="1024" height="563" alt="Add ons
Supabase-10CC55BC-C3D8-4E40-9F73-31FFE8D7F42A"
src="https://github.com/user-attachments/assets/73e206dc-58a3-4356-8e31-cf2ad74383f8"
/> | <img width="1024" height="563" alt="Add ons
Supabase-6311B7FE-4291-4982-968E-5EDBA76DE519"
src="https://github.com/user-attachments/assets/503667ef-9b36-4d62-9872-53ac6b32f56f"
/> |

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-03-05 15:41:22 +08:00

208 lines
8.5 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useFlag, useParams } from 'common'
import {
ScaffoldSection,
ScaffoldSectionContent,
ScaffoldSectionDetail,
} from 'components/layouts/Scaffold'
import AlertError from 'components/ui/AlertError'
import NoPermission from 'components/ui/NoPermission'
import PartnerIcon from 'components/ui/PartnerIcon'
import { PARTNER_TO_NAME } from 'components/ui/PartnerManagedResource'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { BASE_PATH, DOCS_URL } from 'lib/constants'
import { MANAGED_BY } from 'lib/constants/infrastructure'
import { ExternalLink } from 'lucide-react'
import { useTheme } from 'next-themes'
import Image from 'next/image'
import Link from 'next/link'
import { useOrgSettingsPageStateSnapshot } from 'state/organization-settings'
import { Alert, Alert_Shadcn_, AlertTitle_Shadcn_, Button } from 'ui'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { ProjectUpdateDisabledTooltip } from '../ProjectUpdateDisabledTooltip'
import SpendCapSidePanel from './SpendCapSidePanel'
export interface CostControlProps {}
const CostControl = ({}: CostControlProps) => {
const { slug } = useParams()
const { resolvedTheme } = useTheme()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const { isSuccess: isPermissionsLoaded, can: canReadSubscriptions } = useAsyncCheckPermissions(
PermissionAction.BILLING_READ,
'stripe.subscriptions'
)
const snap = useOrgSettingsPageStateSnapshot()
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
const {
data: subscription,
error,
isPending: isLoading,
isSuccess,
isError,
} = useOrgSubscriptionQuery({ orgSlug: slug }, { enabled: canReadSubscriptions })
const currentPlan = subscription?.plan
const isUsageBillingEnabled = subscription?.usage_billing_enabled ?? false
const canChangeTier =
!projectUpdateDisabled && !['team', 'enterprise', 'platform'].includes(currentPlan?.id || '')
const costControlDisabled = selectedOrganization?.managed_by === MANAGED_BY.AWS_MARKETPLACE
return (
<>
<ScaffoldSection>
<ScaffoldSectionDetail>
<div className="sticky space-y-6 top-12">
<div className="space-y-2">
<p className="text-foreground text-base m-0">Cost Control</p>
<p className="text-sm text-foreground-light m-0">
Allow scaling beyond your plan's{' '}
<Link
href={`/org/${slug}/usage`}
className="text-green-900 transition hover:text-green-1000"
>
included quota
</Link>
.
</p>
</div>
<div className="space-y-2">
<p className="text-sm text-foreground-light m-0">More information</p>
<div>
<Link
href={`${DOCS_URL}/guides/platform/cost-control#spend-cap`}
target="_blank"
rel="noreferrer"
>
<div className="flex items-center space-x-2 opacity-50 hover:opacity-100 transition">
<p className="text-sm m-0">Spend cap</p>
<ExternalLink size={16} strokeWidth={1.5} />
</div>
</Link>
</div>
</div>
</div>
</ScaffoldSectionDetail>
<ScaffoldSectionContent>
{isPermissionsLoaded && !canReadSubscriptions ? (
<NoPermission resourceText="update this organization's cost control" />
) : (
<>
{isLoading && (
<div className="space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
)}
{isError && <AlertError subject="Failed to retrieve subscription" error={error} />}
{isSuccess && costControlDisabled && (
<Alert_Shadcn_ className="flex flex-col items-center gap-y-2 border-0 rounded-none">
<PartnerIcon
organization={{ managed_by: selectedOrganization?.managed_by }}
showTooltip={false}
size="large"
/>
<AlertTitle_Shadcn_ className="text-sm">
The Spend Cap is not available for organizations managed by{' '}
{PARTNER_TO_NAME[selectedOrganization?.managed_by]}.
</AlertTitle_Shadcn_>
</Alert_Shadcn_>
)}
{isSuccess && !costControlDisabled && (
<div className="space-y-6">
{['team', 'enterprise', 'platform'].includes(currentPlan?.id || '') ? (
<Alert
withIcon
variant="info"
title={`You will be charged for any additional usage on the ${
currentPlan?.name || ''
} plan`}
>
{currentPlan?.name || ''} plan requires you to have spend cap off at all
times. Your projects will never become unresponsive. Only when your{' '}
<Link
href={`/org/${slug}/usage`}
className="text-green-900 transition hover:text-green-1000"
>
included usage
</Link>{' '}
is exceeded will you be charged for any additional usage.
</Alert>
) : (
<p className="text-sm text-foreground-light">
If you need to go beyond the included quota, simply switch off your spend cap
to pay for additional usage.
</p>
)}
<div className="flex flex-col md:flex-row gap-6">
<div>
<div className="rounded-md bg-surface-200 w-[160px] h-[96px] shadow">
<Image
alt="Spend Cap"
width={160}
height={96}
src={
isUsageBillingEnabled
? `${BASE_PATH}/img/spend-cap-off${
resolvedTheme?.includes('dark') ? '' : '--light'
}.png`
: `${BASE_PATH}/img/spend-cap-on${
resolvedTheme?.includes('dark') ? '' : '--light'
}.png`
}
/>
</div>
</div>
<div>
<p className="mb-1">
Spend cap is {isUsageBillingEnabled ? 'disabled' : 'enabled'}
</p>
<p className="text-sm text-foreground-light">
{isUsageBillingEnabled ? (
<span>You will be charged for usage beyond the included quota.</span>
) : (
<span>
You won't be charged any extra for usage. However, your projects could
become unresponsive or enter read only mode if you exceed the included
quota.
</span>
)}
</p>
<ProjectUpdateDisabledTooltip projectUpdateDisabled={projectUpdateDisabled}>
<Button
type="default"
className="mt-4 pointer-events-auto"
disabled={!canChangeTier}
onClick={() => snap.setPanelKey('costControl')}
>
Change spend cap
</Button>
</ProjectUpdateDisabledTooltip>
</div>
</div>
</div>
)}
</>
)}
</ScaffoldSectionContent>
</ScaffoldSection>
<SpendCapSidePanel />
</>
)
}
export default CostControl