Files
supabase/apps/studio/components/interfaces/Settings/Addons/IPv4SidePanel.tsx
Danny White 4c148ea060 chore(studio): move short Admonitions to descriptions (#46049)
## What kind of change does this PR introduce?

Chore. Follow-up to DEPR-551, #45302, #45535, and #45618.

## What is the current behaviour?

Some short Studio Admonitions still put their entire message in `title`
or legacy `label`, so body-copy callouts render as headings.

## What is the new behaviour?

Moves selected single-message Studio Admonitions to `description`,
keeping the follow-up deliberately limited to Studio callsites.

This PR does not touch Docs content, shared Alert styling, ui-patterns,
design-system registry/docs, or Tailwind config.

| Before | After |
| --- | --- |
| <img width="1818" height="388" alt="Image"
src="https://github.com/user-attachments/assets/283a1853-348a-4d74-a408-013957350e5e"
/> | <img width="1380" height="462" alt="Image"
src="https://github.com/user-attachments/assets/e5761e8e-3697-423b-805b-45110205099a"
/> |
| <img width="1640" height="716" alt="CleanShot 2026-04-28 at 15 17
25@2x"
src="https://github.com/user-attachments/assets/a5be4d5f-2bf7-4dc2-b396-56129fe64ec9"
/> | <img width="1630" height="716" alt="CleanShot 2026-04-28 at 15 16
00@2x"
src="https://github.com/user-attachments/assets/0d589252-aaf8-4efc-9d81-15ec4f99ec61"
/> |


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Style**
* Refined message displays and admonition styling across settings,
database, dashboard, and admin interfaces for improved visual
consistency and clarity.

* **UI Updates**
* Updated search input layouts and form element styling in publications
tables and other admin pages.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46049?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-19 10:20:54 +10:00

248 lines
9.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { cn, RadioGroup, RadioGroupLargeItem, SidePanel } from 'ui'
import { Admonition } from 'ui-patterns'
import { TaxDisclaimer } from '@/components/interfaces/Billing/TaxDisclaimer'
import { DocsButton } from '@/components/ui/DocsButton'
import { InlineLink } from '@/components/ui/InlineLink'
import { UpgradeToPro } from '@/components/ui/UpgradeToPro'
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 { useIsAwsCloudProvider } from '@/hooks/misc/useSelectedProject'
import { DOCS_URL } from '@/lib/constants'
import { formatCurrency } from '@/lib/helpers'
import { useAddonsPagePanel } from '@/state/addons-page'
const IPv4SidePanel = () => {
const isAws = useIsAwsCloudProvider()
const { ref: projectRef } = useParams()
const [selectedOption, setSelectedOption] = useState<string>('ipv4_none')
const { can: canUpdateIPv4 } = useAsyncCheckPermissions(
PermissionAction.BILLING_WRITE,
'stripe.subscriptions'
)
const { panel, closePanel } = useAddonsPagePanel()
const visible = panel === 'ipv4'
const { data: addons, isPending: isLoading } = useProjectAddonsQuery({ projectRef })
const { mutate: updateAddon, isPending: isUpdating } = useProjectAddonUpdateMutation({
onSuccess: () => {
toast.success(`Successfully enabled IPv4`)
closePanel()
},
onError: (error) => {
toast.error(`Unable to enable IPv4: ${error.message}`)
},
})
const { mutate: removeAddon, isPending: isRemoving } = useProjectAddonRemoveMutation({
onSuccess: () => {
toast.success(`Successfully disabled IPv4.`)
closePanel()
},
onError: (error) => {
toast.error(`Unable to disable IPv4: ${error.message}`)
},
})
const isSubmitting = isUpdating || isRemoving
const subscriptionIpV4Option = (addons?.selected_addons ?? []).find(
(addon) => addon.type === 'ipv4'
)
const availableOptions =
(addons?.available_addons ?? []).find((addon) => addon.type === 'ipv4')?.variants ?? []
const { hasAccess: hasAccessToIPv4, isLoading: isLoadingEntitlement } =
useCheckEntitlements('ipv4')
const hasChanges = selectedOption !== (subscriptionIpV4Option?.variant.identifier ?? 'ipv4_none')
const selectedIPv4 = availableOptions.find((option) => option.identifier === selectedOption)
const ipv4Options = [
{
value: 'ipv4_none',
id: 'ipv4_none',
title: 'No IPv4 address',
description: 'Use shared pooler or IPv6 for database connections.',
priceContent: (
<>
<p className="text-foreground text-sm">$0</p>
<p className="text-foreground-light translate-y-px text-sm">/ month</p>
</>
),
priceRowClassName: 'mt-2',
},
...availableOptions.map((option) => ({
value: option.identifier,
id: option.identifier,
title: 'Dedicated IPv4 address',
description: 'Allow database connections from IPv4 networks.',
priceContent: (
<>
<p className="text-sm" translate="no">
{formatCurrency(option.price)}
</p>
<p className="text-foreground-light translate-y-[0.5px]">/ month / database</p>
</>
),
priceRowClassName: 'mt-3',
})),
]
useEffect(() => {
if (visible) {
if (subscriptionIpV4Option !== undefined) {
setSelectedOption(subscriptionIpV4Option.variant.identifier)
} else {
setSelectedOption('ipv4_none')
}
}
}, [visible, isLoading])
const onConfirm = async () => {
if (!projectRef) return console.error('Project ref is required')
if (selectedOption === 'ipv4_none' && subscriptionIpV4Option !== undefined) {
removeAddon({ projectRef, variant: subscriptionIpV4Option.variant.identifier })
} else {
updateAddon({ projectRef, type: 'ipv4', variant: selectedOption as AddonVariantId })
}
}
return (
<SidePanel
size="large"
visible={visible}
onCancel={closePanel}
onConfirm={onConfirm}
loading={isLoading || isSubmitting || isLoadingEntitlement}
disabled={
!hasAccessToIPv4 ||
isLoadingEntitlement ||
isLoading ||
!hasChanges ||
isSubmitting ||
!canUpdateIPv4 ||
!isAws
}
tooltip={
!hasAccessToIPv4
? 'Unable to enable IPv4 on a Free Plan'
: !canUpdateIPv4
? 'You do not have permission to update IPv4'
: undefined
}
header={
<div className="flex w-full items-center justify-between">
<h4>Dedicated IPv4 address</h4>
<DocsButton href={`${DOCS_URL}/guides/platform/ipv4-address`} />
</div>
}
>
<SidePanel.Content>
<div className="py-6 space-y-4">
<p className="text-sm">
Your projects direct connection endpoint and dedicated pooler are IPv6-only by default.
Enable the dedicated IPv4 address add-on to connect from IPv4-only networks.
</p>
<p className="text-sm">
The shared pooler endpoint accepts IPv4 connections by default and does not require this
add-on.
</p>
{!isAws && (
<Admonition
type="default"
description="Dedicated IPv4 address is only available for AWS projects."
/>
)}
{isAws && (
<div className={cn('mt-8! pb-4', !hasAccessToIPv4 && 'opacity-75')}>
<RadioGroup
name="ipv4"
value={selectedOption}
onValueChange={setSelectedOption}
className="grid grid-cols-1 md:grid-cols-2 gap-4"
>
{ipv4Options.map((option) => (
<RadioGroupLargeItem
key={option.id}
value={option.value}
label=""
showIndicator={false}
className={cn(
'w-full gap-0 p-0 shadow-none bg-transparent cursor-pointer text-left',
'border-default hover:border-control hover:bg-transparent'
)}
>
<div className="px-4 py-3">
<p className="text-sm font-medium">{option.title}</p>
<p className="text-foreground-light text-sm mt-1">{option.description}</p>
<div
className={cn(
'flex items-center space-x-1 text-sm',
option.priceRowClassName
)}
>
{option.priceContent}
</div>
</div>
</RadioGroupLargeItem>
))}
</RadioGroup>
<TaxDisclaimer className="mt-3" />
</div>
)}
{hasChanges && (
<>
<Admonition
type="note"
title="Potential downtime"
description="There might be some downtime when enabling the add-on since some DNS clients might
have cached the old DNS entry. Generally, this should be less than a minute."
/>
{selectedOption !== 'ipv4_none' && (
<p className="text-sm text-foreground-light">
By default, this is only applied to the primary database for your project. If{' '}
<InlineLink href={`${DOCS_URL}/guides/platform/read-replicas`} target="_blank">
read replicas
</InlineLink>{' '}
are used, each replica also gets its own IPv4 address, with a corresponding{' '}
<span className="text-foreground">{formatCurrency(selectedIPv4?.price)}</span>{' '}
charge.
</p>
)}
<p className="text-sm text-foreground-light">
There are no immediate charges. The add-on is billed at the end of your billing
cycle based on your usage and prorated to the hour.
</p>
</>
)}
{!hasAccessToIPv4 && (
<UpgradeToPro
addon="ipv4"
source="ipv4SidePanel"
featureProposition="connect from IPv4-only networks"
primaryText="Dedicated IPv4 address is a Pro Plan add-on"
secondaryText="Enable the add-on to connect to your project from IPv4-only networks."
/>
)}
</div>
</SidePanel.Content>
</SidePanel>
)
}
export default IPv4SidePanel