diff --git a/apps/studio/components/interfaces/ConnectSheet/CopyPromptAdmonition.tsx b/apps/studio/components/interfaces/ConnectSheet/CopyPromptAdmonition.tsx
index ab704a4729e..c5993d85022 100644
--- a/apps/studio/components/interfaces/ConnectSheet/CopyPromptAdmonition.tsx
+++ b/apps/studio/components/interfaces/ConnectSheet/CopyPromptAdmonition.tsx
@@ -26,6 +26,10 @@ export function CopyPromptAdmonition({ stepsContainerRef }: CopyPromptAdmonition
element.remove()
})
+ clone.querySelectorAll('p, div').forEach((el) => {
+ el.appendChild(document.createTextNode('\n'))
+ })
+
const text = clone.textContent ?? ''
return normalizeTextLines(text)
}
@@ -141,15 +145,7 @@ export function CopyPromptAdmonition({ stepsContainerRef }: CopyPromptAdmonition
-
-
- Skip the steps
-
-
Prompt your agent
-
-
- Copy a prompt with everything your agent needs to connect your app for you.
-
+
Give your agent everything it needs
diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/direct-connection/IPv4StatusPanel.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/direct-connection/IPv4StatusPanel.tsx
new file mode 100644
index 00000000000..e777c92ac8e
--- /dev/null
+++ b/apps/studio/components/interfaces/ConnectSheet/content/steps/direct-connection/IPv4StatusPanel.tsx
@@ -0,0 +1,182 @@
+import { ChevronRight, X } from 'lucide-react'
+import Link from 'next/link'
+import { ReactNode } from 'react'
+import {
+ Button,
+ cn,
+ Collapsible_Shadcn_,
+ CollapsibleContent_Shadcn_,
+ CollapsibleTrigger_Shadcn_,
+ WarningIcon,
+} from 'ui'
+
+import { IS_PLATFORM } from '@/lib/constants'
+
+const IPv4StatusIcon = ({ className, active }: { className?: string; active: boolean }) => {
+ return (
+
+
+
+ {!active ? (
+
+
+
+ ) : (
+
+ )}
+
+ )
+}
+
+export interface IPv4Status {
+ type: 'error' | 'success'
+ title: string
+ description?: string | ReactNode
+ links?: { text: string; url: string }[]
+}
+
+interface IPv4StatusPanelProps {
+ method: 'direct' | 'transaction' | 'session'
+ ipv4Status: IPv4Status
+ projectRef: string
+}
+
+export function IPv4StatusPanel({ method, ipv4Status, projectRef }: IPv4StatusPanelProps) {
+ if (!IS_PLATFORM) return null
+
+ const links = ipv4Status.links ?? []
+
+ return (
+
+ {method === 'session' ? (
+
+
+
+
+
+
Only use on a IPv4 network
+
+
Session pooler connections are IPv4 proxied for free.
+
Use Direct Connection if connecting via an IPv6 network.
+
+
+
+ ) : (
+ <>
+
+
+
+
+
+
{ipv4Status.title}
+ {ipv4Status.description &&
+ (typeof ipv4Status.description === 'string' ? (
+
{ipv4Status.description}
+ ) : (
+ ipv4Status.description
+ ))}
+ {links.length > 0 && (
+
+ {links.map((link) => (
+
+ ))}
+
+ )}
+
+
+
+ {ipv4Status.type === 'error' && (
+
+
+
+ }
+ >
+ Some platforms are IPv4-only:
+
+
+
+
+
+ A few major platforms are IPv4-only and may not work with a Direct Connection:
+
+
+
Vercel
+
GitHub Actions
+
Render
+
Retool
+
+
+ If you wish to use a Direct Connection with these, please purchase{' '}
+
+ IPv4 support
+
+ .
+
+
+ You may also use the{' '}
+ Session Pooler or{' '}
+ Transaction Pooler if you are on
+ a IPv4 network.
+
+
+
+
+ )}
+ >
+ )}
+
+ )
+}
diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/direct-connection/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/direct-connection/content.tsx
index d2683ba6bcf..af67c0bbfe7 100644
--- a/apps/studio/components/interfaces/ConnectSheet/content/steps/direct-connection/content.tsx
+++ b/apps/studio/components/interfaces/ConnectSheet/content/steps/direct-connection/content.tsx
@@ -1,12 +1,16 @@
import { useParams } from 'common'
import { useMemo } from 'react'
+import { Badge } from 'ui'
import { CodeBlock } from 'ui-patterns/CodeBlock'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { getConnectionStrings } from '../../../DatabaseSettings.utils'
+import { IPv4StatusPanel, type IPv4Status } from './IPv4StatusPanel'
import { getAddons } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
import {
DATABASE_CONNECTION_TYPES,
+ IPV4_ADDON_TEXT,
+ PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT,
type ConnectionStringMethod,
type DatabaseConnectionType,
} from '@/components/interfaces/ConnectSheet/Connect.constants'
@@ -121,6 +125,9 @@ const CONNECTION_METHOD_TO_TELEMETRY: Record<
*/
function DirectConnectionContent({ state }: StepContentProps) {
const track = useTrack()
+ const { ref: projectRef } = useParams()
+ const { hasAccess: hasDedicatedPooler } = useCheckEntitlements('dedicated_pooler')
+
const connectionSource = state.connectionSource
const connectionType = (state.connectionType as DatabaseConnectionType) ?? 'uri'
const connectionMethod = (state.connectionMethod as ConnectionStringMethod) ?? 'direct'
@@ -129,6 +136,7 @@ function DirectConnectionContent({ state }: StepContentProps) {
const connectionStrings = useConnectionStringDatabases()
const connectionStringPooler =
connectionStrings[connectionSource as keyof typeof connectionStrings]
+ const hasIPv4Addon = connectionStringPooler.ipv4SupportedForDedicatedPooler
// Determine which connection string to use
const resolvedConnectionString = useMemo(
@@ -184,8 +192,69 @@ function DirectConnectionContent({ state }: StepContentProps) {
)
}
+ const sharedPoolerPreferred = !hasDedicatedPooler
+ const ipv4AddOnUrl = {
+ text: 'IPv4 add-on',
+ url: `/project/${projectRef}/settings/addons?panel=ipv4`,
+ }
+ const ipv4SettingsUrl = {
+ text: 'IPv4 settings',
+ url: `/project/${projectRef}/settings/addons?panel=ipv4`,
+ }
+ const poolerSettingsUrl = {
+ text: 'Pooler settings',
+ url: `/project/${projectRef}/database/settings#connection-pooling`,
+ }
+ const buttonLinks = !hasIPv4Addon
+ ? [ipv4AddOnUrl, ...(sharedPoolerPreferred ? [poolerSettingsUrl] : [])]
+ : [ipv4SettingsUrl, ...(sharedPoolerPreferred ? [poolerSettingsUrl] : [])]
+
+ let ipv4Status: IPv4Status
+ if (connectionMethod === 'direct') {
+ ipv4Status = {
+ type: !hasIPv4Addon ? 'error' : 'success',
+ title: !hasIPv4Addon ? 'Not IPv4 compatible' : 'IPv4 compatible',
+ description:
+ !sharedPoolerPreferred && !hasIPv4Addon
+ ? PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT
+ : sharedPoolerPreferred
+ ? 'Use Session Pooler if on a IPv4 network or purchase IPv4 add-on'
+ : IPV4_ADDON_TEXT,
+ links: buttonLinks,
+ }
+ } else if (connectionMethod === 'transaction') {
+ const isUsingSharedPooler = useSharedPooler || !hasDedicatedPooler
+ ipv4Status = {
+ type: !isUsingSharedPooler && !hasIPv4Addon ? 'error' : 'success',
+ title: !isUsingSharedPooler && !hasIPv4Addon ? 'Not IPv4 compatible' : 'IPv4 compatible',
+ description:
+ !isUsingSharedPooler && !hasIPv4Addon
+ ? PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT
+ : isUsingSharedPooler
+ ? 'Transaction pooler connections are IPv4 proxied for free.'
+ : IPV4_ADDON_TEXT,
+ links: !isUsingSharedPooler ? buttonLinks : undefined,
+ }
+ } else {
+ ipv4Status = {
+ type: 'success',
+ title: 'IPv4 compatible',
+ description: 'Session pooler connections are IPv4 proxied for free',
+ }
+ }
+
+ const poolerBadge =
+ connectionMethod === 'transaction'
+ ? useSharedPooler || !hasDedicatedPooler
+ ? 'Shared Pooler'
+ : 'Dedicated Pooler'
+ : connectionMethod === 'session'
+ ? 'Shared Pooler'
+ : null
+
return (
+
{poolerBadge && {poolerBadge}}
{connectionString}
+ {projectRef && (
+
+
+
+ )}
({
useReadReplicasQuery: () => ({ data: [] }),
}))
+vi.mock('@/hooks/misc/useCheckEntitlements', () => ({
+ useCheckEntitlements: vi.fn().mockImplementation(() => ({ hasAccess: true })),
+}))
+
describe('useConnectState', () => {
// ============================================================================
// Initial State Tests
@@ -276,7 +280,10 @@ describe('useConnectState', () => {
expect(fieldIds).toContain('connectionType')
})
- test('should show useSharedPooler only for transaction connection method', () => {
+ test('should show useSharedPooler only for transaction connection method when user has dedicated_pooler entitlement', async () => {
+ const { useCheckEntitlements } = await import('@/hooks/misc/useCheckEntitlements')
+ vi.mocked(useCheckEntitlements).mockReturnValue({ hasAccess: true } as any)
+
const { result } = renderHook(() =>
useConnectState({ mode: 'direct', connectionMethod: 'transaction' })
)
@@ -285,6 +292,18 @@ describe('useConnectState', () => {
expect(fieldIds).toContain('useSharedPooler')
})
+ test('should hide useSharedPooler even if using transaction method when user lacks dedicated_pooler entitlement', async () => {
+ const { useCheckEntitlements } = await import('@/hooks/misc/useCheckEntitlements')
+ vi.mocked(useCheckEntitlements).mockReturnValue({ hasAccess: false } as any)
+
+ const { result } = renderHook(() =>
+ useConnectState({ mode: 'direct', connectionMethod: 'transaction' })
+ )
+
+ const fieldIds = result.current.activeFields.map((f) => f.id)
+ expect(fieldIds).not.toContain('useSharedPooler')
+ })
+
test('should hide useSharedPooler for direct connection method', () => {
const { result } = renderHook(() =>
useConnectState({ mode: 'direct', connectionMethod: 'direct' })
diff --git a/apps/studio/components/interfaces/ConnectSheet/useConnectState.ts b/apps/studio/components/interfaces/ConnectSheet/useConnectState.ts
index 5e55f1e1ad7..9bccdbd626d 100644
--- a/apps/studio/components/interfaces/ConnectSheet/useConnectState.ts
+++ b/apps/studio/components/interfaces/ConnectSheet/useConnectState.ts
@@ -27,6 +27,7 @@ import type {
import { resolveFrameworkLibraryKey } from './Connect.utils'
import { Database, useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
import { formatDatabaseID, formatDatabaseRegion } from '@/data/read-replicas/replicas.utils'
+import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
// ============================================================================
// Data Source Helpers
@@ -194,6 +195,7 @@ export interface UseConnectStateReturn {
export function useConnectState(initialState?: Partial): UseConnectStateReturn {
const { ref: projectRef } = useParams()
const { data: databases = [] } = useReadReplicasQuery({ projectRef })
+ const { hasAccess: hasDedicatedPooler } = useCheckEntitlements('dedicated_pooler')
const [state, setState] = useState(() => {
const defaults = getDefaultState({ schema: connectSchema })
@@ -318,7 +320,13 @@ export function useConnectState(initialState?: Partial): UseConnec
[projectRef]
)
- const activeFields = useMemo(() => getActiveFields(connectSchema, state), [state])
+ const activeFields = useMemo(() => {
+ const fields = getActiveFields(connectSchema, state)
+ if (!hasDedicatedPooler) {
+ return fields.filter((f) => f.id !== 'useSharedPooler')
+ }
+ return fields
+ }, [state, hasDedicatedPooler])
const resolvedSteps = useMemo(() => resolveSteps(connectSchema, state), [state])