connection string full layout

This commit is contained in:
Francesco Sansalvadore committed 2025-10-17 12:38:16 +02:00
1 parent d57279dde6
commit f4450bfcea
3 files changed
+160 -290

No files matched your search

@@ -356,32 +356,3 @@ export const CONNECTION_TYPES = [
export const PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT =
'Purchase IPv4 add-on or use Shared Pooler if on a IPv4 network'
export const IPV4_ADDON_TEXT = 'Connections are IPv4 proxied with IPv4 add-on'
export type ConnectionStringMethod = 'direct' | 'transaction' | 'session'
export const connectionStringMethodOptions: Record<
ConnectionStringMethod,
{ value: string; label: string; description: string; badge: string }
> = {
direct: {
value: 'direct',
label: 'Direct connection',
description:
'Ideal for applications with persistent and long-lived connections, such as those running on virtual machines or long-standing containers.',
badge: 'IPv4 Compatible',
},
transaction: {
value: 'transaction',
label: 'Transaction pooler',
description:
'Ideal for stateless applications like serverless functions where each interaction with Postgres is brief and isolated.',
badge: 'IPv4 Compatible',
},
session: {
value: 'session',
label: 'Session pooler',
description:
'Only recommended as an alternative to Direct Connection, when connecting via an IPv4 network.',
badge: 'IPv4 Only',
},
}
@@ -87,9 +87,6 @@ export const Connect = () => {
const [queryFramework, setQueryFramework] = useQueryState('framework', parseAsString)
const [queryUsing, setQueryUsing] = useQueryState('using', parseAsString)
const [queryWith, setQueryWith] = useQueryState('with', parseAsString)
const [_, setQueryType] = useQueryState('type', parseAsString)
const [__, setQuerySource] = useQueryState('source', parseAsString)
const [___, setQueryMethod] = useQueryState('method', parseAsString)
const [connectionObject, setConnectionObject] = useState<ConnectionType[]>(FRAMEWORKS)
const [selectedParent, setSelectedParent] = useState(connectionObject[0].key) // aka nextjs
@@ -245,20 +242,13 @@ export const Connect = () => {
selectedGrandchild,
})
const resetQueryStates = () => {
setQueryFramework(null)
setQueryUsing(null)
setQueryWith(null)
setQueryType(null)
setQuerySource(null)
setQueryMethod(null)
}
const handleDialogChange = (open: boolean) => {
if (!open) {
setShowConnect(null)
setTab(null)
resetQueryStates()
setQueryFramework(null)
setQueryUsing(null)
setQueryWith(null)
} else {
setShowConnect(open)
}
@@ -321,8 +311,8 @@ export const Connect = () => {
<span>Connect</span>
</Button>
</DialogTrigger>
<DialogContent className={cn('sm:max-w-5xl p-0 rounded-lg')} centered={false}>
<DialogHeader className={cn('text-left', DIALOG_PADDING_X)}>
<DialogContent className={cn('sm:max-w-5xl p-0')} centered={false}>
<DialogHeader className={DIALOG_PADDING_X}>
<DialogTitle>
Connect to your project
{connectionTypes.length === 1 ? ` via ${connectionTypes[0].label.toLowerCase()}` : null}
@@ -332,13 +322,7 @@ export const Connect = () => {
</DialogDescription>
</DialogHeader>
<Tabs_Shadcn_
defaultValue={tab}
onValueChange={(value) => {
resetQueryStates()
handleConnectionType(value)
}}
>
<Tabs_Shadcn_ defaultValue={tab} onValueChange={(value) => handleConnectionType(value)}>
{connectionTypes.length > 1 ? (
<TabsList_Shadcn_ className={cn('flex overflow-x-scroll gap-x-4', DIALOG_PADDING_X)}>
{connectionTypes.map((type) => (
@@ -1,6 +1,5 @@
import { ChevronDown, GlobeIcon, InfoIcon } from 'lucide-react'
import { parseAsString, useQueryState } from 'nuqs'
import { HTMLAttributes, ReactNode, useEffect, useMemo, useState } from 'react'
import { ChevronDown } from 'lucide-react'
import { HTMLAttributes, ReactNode, useMemo, useState } from 'react'
import { useParams } from 'common'
import { getAddons } from 'components/interfaces/Billing/Subscription/Subscription.utils'
@@ -19,6 +18,7 @@ import { pluckObjectFields } from 'lib/helpers'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import {
Badge,
Button,
CodeBlock,
CollapsibleContent_Shadcn_,
CollapsibleTrigger_Shadcn_,
@@ -35,12 +35,10 @@ import {
import { Admonition } from 'ui-patterns'
import {
CONNECTION_PARAMETERS,
type ConnectionStringMethod,
DATABASE_CONNECTION_TYPES,
DatabaseConnectionType,
IPV4_ADDON_TEXT,
PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT,
connectionStringMethodOptions,
} from './Connect.constants'
import { CodeBlockFileHeader, ConnectionPanel } from './ConnectionPanel'
import { getConnectionStrings } from './DatabaseSettings.utils'
@@ -52,7 +50,7 @@ const StepLabel = ({
...props
}: { number: number; children: ReactNode } & HTMLAttributes<HTMLDivElement>) => (
<div {...props} className={cn('flex items-center gap-2', props.className)}>
<div className="flex font-mono text-xs items-center justify-center min-w-6 w-6 h-6 border border-strong rounded-md bg-surface-100">
<div className="flex font-mono text-xs items-center justify-center w-6 h-6 border border-strong rounded-md bg-surface-100">
{number}
</div>
<span>{children}</span>
@@ -68,74 +66,12 @@ export const DatabaseConnectionString = () => {
const { data: org } = useSelectedOrganizationQuery()
const state = useDatabaseSelectorStateSnapshot()
// URL state management
const [queryType, setQueryType] = useQueryState('type', parseAsString.withDefault('uri'))
const [querySource, setQuerySource] = useQueryState('source', parseAsString)
const [queryMethod, setQueryMethod] = useQueryState('method', parseAsString.withDefault('direct'))
const [selectedTab, setSelectedTab] = useState<DatabaseConnectionType>('uri')
const [selectedMethod, setSelectedMethod] = useState<ConnectionStringMethod>('direct')
const sharedPoolerPreferred = useMemo(() => {
return org?.plan?.id === 'free'
}, [org])
// Sync URL state with component state on mount and when URL changes
useEffect(() => {
const validTypes = DATABASE_CONNECTION_TYPES.map((t) => t.id)
if (queryType && validTypes.includes(queryType as DatabaseConnectionType)) {
setSelectedTab(queryType as DatabaseConnectionType)
} else if (queryType && !validTypes.includes(queryType as DatabaseConnectionType)) {
setQueryType('uri')
setSelectedTab('uri')
}
const validMethods: ConnectionStringMethod[] = ['direct', 'transaction', 'session']
if (queryMethod && validMethods.includes(queryMethod as ConnectionStringMethod)) {
setSelectedMethod(queryMethod as ConnectionStringMethod)
} else if (queryMethod && !validMethods.includes(queryMethod as ConnectionStringMethod)) {
setQueryMethod('direct')
setSelectedMethod('direct')
}
if (querySource && querySource !== state.selectedDatabaseId) {
state.setSelectedDatabaseId(querySource)
} else if (!querySource && state.selectedDatabaseId !== projectRef) {
state.setSelectedDatabaseId(projectRef)
}
}, [queryType, queryMethod, querySource, state])
// Sync component state changes back to URL
const handleTabChange = (connectionType: DatabaseConnectionType) => {
setSelectedTab(connectionType)
setQueryType(connectionType)
}
const handleMethodChange = (method: ConnectionStringMethod) => {
setSelectedMethod(method)
setQueryMethod(method)
}
const handleDatabaseChange = (databaseId: string) => {
if (databaseId === projectRef) {
setQuerySource(null)
} else {
setQuerySource(databaseId)
}
}
// Sync database selector state changes back to URL
useEffect(() => {
if (state.selectedDatabaseId && state.selectedDatabaseId !== querySource) {
// Only set source in URL if it's not the primary database
if (state.selectedDatabaseId === projectRef) {
setQuerySource(null)
} else {
setQuerySource(state.selectedDatabaseId)
}
}
}, [state.selectedDatabaseId, querySource, projectRef])
const {
data: pgbouncerConfig,
error: pgbouncerError,
@@ -200,14 +136,14 @@ export const DatabaseConnectionString = () => {
const handleCopy = (
connectionTypeId: string,
connectionStringMethod: 'direct' | 'transaction_pooler' | 'session_pooler'
connectionMethod: 'direct' | 'transaction_pooler' | 'session_pooler'
) => {
const connectionInfo = DATABASE_CONNECTION_TYPES.find((type) => type.id === connectionTypeId)
const connectionType = connectionInfo?.label ?? 'Unknown'
const lang = connectionInfo?.lang ?? 'Unknown'
sendEvent({
action: 'connection_string_copied',
properties: { connectionType, lang, connectionMethod: connectionStringMethod },
properties: { connectionType, lang, connectionMethod },
groups: { project: projectRef ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
})
}
@@ -285,7 +221,12 @@ export const DatabaseConnectionString = () => {
<span className="flex items-center text-foreground-lighter px-3 rounded-lg rounded-r-none text-xs border border-button border-r-0">
Type
</span>
<Select_Shadcn_ value={selectedTab} onValueChange={handleTabChange}>
<Select_Shadcn_
value={selectedTab}
onValueChange={(connectionType: DatabaseConnectionType) =>
setSelectedTab(connectionType)
}
>
<SelectTrigger_Shadcn_ size="small" className="w-auto rounded-l-none">
<SelectValue_Shadcn_ />
</SelectTrigger_Shadcn_>
@@ -298,32 +239,7 @@ export const DatabaseConnectionString = () => {
</SelectContent_Shadcn_>
</Select_Shadcn_>
</div>
<DatabaseSelector
portal={false}
buttonProps={{ size: 'small' }}
onSelectId={handleDatabaseChange}
/>
<div className="flex">
<span className="flex items-center text-foreground-lighter px-3 rounded-lg rounded-r-none text-xs border border-button border-r-0">
Method
</span>
<Select_Shadcn_ value={selectedMethod} onValueChange={handleMethodChange}>
<SelectTrigger_Shadcn_ size="small" className="w-auto rounded-l-none">
<SelectValue_Shadcn_ size="tiny">
{connectionStringMethodOptions[selectedMethod].label}
</SelectValue_Shadcn_>
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_ className="max-w-sm">
{Object.keys(connectionStringMethodOptions).map((method) => (
<ConnectionStringMethodSelectItem
key={method}
method={method as ConnectionStringMethod}
poolerBadge={poolerBadge}
/>
))}
</SelectContent_Shadcn_>
</Select_Shadcn_>
</div>
<DatabaseSelector portal={false} buttonProps={{ size: 'small' }} />
</div>
{isLoading && (
@@ -342,16 +258,15 @@ export const DatabaseConnectionString = () => {
<div className="flex flex-col divide-y divide-border">
{/* // handle non terminal examples */}
{hasCodeExamples && (
<div className="flex flex-col w-full">
<div className="grid lg:grid-cols-3 gap-4 lg:gap-5 py-8 px-4 md:px-7">
<StepLabel number={++stepNumber} className="items-start">
<div className="grid grid-cols-2 gap-x-20 w-full px-4 md:px-7 py-8">
<div>
<StepLabel number={++stepNumber} className="mb-4">
Install the following
</StepLabel>
{exampleInstallCommands?.map((cmd) => (
{exampleInstallCommands?.map((cmd, i) => (
<CodeBlock
key={`example-install-command-${cmd}`}
key={i}
className="[&_code]:text-[12px] [&_code]:text-foreground"
wrapperClassName="lg:col-span-2"
value={cmd}
hideLineNumbers
language="bash"
@@ -361,12 +276,12 @@ export const DatabaseConnectionString = () => {
))}
</div>
{exampleFiles && exampleFiles?.length > 0 && (
<div className="grid lg:grid-cols-3 gap-4 lg:gap-5 border-t py-8 px-4 md:px-7">
<StepLabel number={++stepNumber} className="items-start">
<div>
<StepLabel number={++stepNumber} className="mb-4">
Add file to project
</StepLabel>
{exampleFiles?.map((file) => (
<div key={`example-files-${file.name}`} className="lg:col-span-2">
{exampleFiles?.map((file, i) => (
<div key={i}>
<CodeBlockFileHeader title={file.name} />
<CodeBlock
wrapperClassName="[&_pre]:max-h-40 [&_pre]:px-4 [&_pre]:py-3 [&_pre]:rounded-t-none"
@@ -385,84 +300,122 @@ export const DatabaseConnectionString = () => {
<div>
{hasCodeExamples && (
<div className="px-4 md:px-7 pt-8">
<StepLabel number={++stepNumber}>Connect to your database</StepLabel>
<StepLabel number={++stepNumber}>Choose type of connection</StepLabel>
</div>
)}
<div className="px-4 md:px-7 py-8">
{selectedMethod === 'direct' && (
<ConnectionPanel
type="direct"
title={connectionStringMethodOptions.direct.label}
contentType={contentType}
lang={lang}
fileTitle={fileTitle}
description={connectionStringMethodOptions.direct.description}
connectionString={connectionStrings['direct'][selectedTab]}
ipv4Status={{
type: !ipv4Addon ? 'error' : 'success',
title: !ipv4Addon ? 'Not IPv4 compatible' : 'IPv4 compatible',
description:
!sharedPoolerPreferred && !ipv4Addon
? 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,
}}
parameters={[
{ ...CONNECTION_PARAMETERS.host, value: connectionInfo.db_host },
{ ...CONNECTION_PARAMETERS.port, value: connectionInfo.db_port },
{ ...CONNECTION_PARAMETERS.database, value: connectionInfo.db_name },
{ ...CONNECTION_PARAMETERS.user, value: connectionInfo.db_user },
]}
onCopyCallback={() => handleCopy(selectedTab, 'direct')}
/>
)}
<div className="divide-y divide-border-muted [&>div]:px-4 [&>div]:md:px-7 [&>div]:py-8">
<ConnectionPanel
type="direct"
title="Direct connection"
contentType={contentType}
lang={lang}
fileTitle={fileTitle}
description="Ideal for applications with persistent, long-lived connections, such as those running on virtual machines or long-standing containers."
connectionString={connectionStrings['direct'][selectedTab]}
ipv4Status={{
type: !ipv4Addon ? 'error' : 'success',
title: !ipv4Addon ? 'Not IPv4 compatible' : 'IPv4 compatible',
description:
!sharedPoolerPreferred && !ipv4Addon
? 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,
}}
parameters={[
{ ...CONNECTION_PARAMETERS.host, value: connectionInfo.db_host },
{ ...CONNECTION_PARAMETERS.port, value: connectionInfo.db_port },
{ ...CONNECTION_PARAMETERS.database, value: connectionInfo.db_name },
{ ...CONNECTION_PARAMETERS.user, value: connectionInfo.db_user },
]}
onCopyCallback={() => handleCopy(selectedTab, 'direct')}
/>
{selectedMethod === 'transaction' && IS_PLATFORM && (
<ConnectionPanel
type="transaction"
title={connectionStringMethodOptions.transaction.label}
contentType={contentType}
lang={lang}
badge={poolerBadge}
fileTitle={fileTitle}
description={connectionStringMethodOptions.transaction.description}
connectionString={connectionStrings['pooler'][selectedTab]}
ipv4Status={{
type: !sharedPoolerPreferred && !ipv4Addon ? 'error' : 'success',
title:
!sharedPoolerPreferred && !ipv4Addon
? 'Not IPv4 compatible'
: 'IPv4 compatible',
description:
!sharedPoolerPreferred && !ipv4Addon
? PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT
: sharedPoolerPreferred
? 'Transaction pooler connections are IPv4 proxied for free.'
: IPV4_ADDON_TEXT,
links: !sharedPoolerPreferred ? buttonLinks : undefined,
}}
notice={['Does not support PREPARE statements']}
parameters={[
{ ...CONNECTION_PARAMETERS.host, value: poolingConfiguration?.db_host ?? '' },
{
...CONNECTION_PARAMETERS.port,
value: poolingConfiguration?.db_port.toString() ?? '6543',
},
{
...CONNECTION_PARAMETERS.database,
value: poolingConfiguration?.db_name ?? '',
},
{ ...CONNECTION_PARAMETERS.user, value: poolingConfiguration?.db_user ?? '' },
{ ...CONNECTION_PARAMETERS.pool_mode, value: 'transaction' },
]}
onCopyCallback={() => handleCopy(selectedTab, 'transaction_pooler')}
/>
)}
{selectedMethod === 'session' && IS_PLATFORM && (
{IS_PLATFORM && (
<>
<ConnectionPanel
type="transaction"
title="Transaction pooler"
contentType={contentType}
lang={lang}
badge={poolerBadge}
fileTitle={fileTitle}
description="Ideal for stateless applications like serverless functions where each interaction with Postgres is brief and isolated."
connectionString={connectionStrings['pooler'][selectedTab]}
ipv4Status={{
type: !sharedPoolerPreferred && !ipv4Addon ? 'error' : 'success',
title:
!sharedPoolerPreferred && !ipv4Addon
? 'Not IPv4 compatible'
: 'IPv4 compatible',
description:
!sharedPoolerPreferred && !ipv4Addon
? PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT
: sharedPoolerPreferred
? 'Transaction pooler connections are IPv4 proxied for free.'
: IPV4_ADDON_TEXT,
links: !sharedPoolerPreferred ? buttonLinks : undefined,
}}
notice={['Does not support PREPARE statements']}
parameters={[
{ ...CONNECTION_PARAMETERS.host, value: poolingConfiguration?.db_host ?? '' },
{
...CONNECTION_PARAMETERS.port,
value: poolingConfiguration?.db_port.toString() ?? '6543',
},
{
...CONNECTION_PARAMETERS.database,
value: poolingConfiguration?.db_name ?? '',
},
{ ...CONNECTION_PARAMETERS.user, value: poolingConfiguration?.db_user ?? '' },
{ ...CONNECTION_PARAMETERS.pool_mode, value: 'transaction' },
]}
onCopyCallback={() => handleCopy(selectedTab, 'transaction_pooler')}
>
{!sharedPoolerPreferred && !ipv4Addon && (
<Collapsible_Shadcn_ className="group">
<CollapsibleTrigger_Shadcn_
asChild
className="w-full justify-start !last:rounded-b group-data-[state=open]:rounded-b-none border-light mt-4 px-3"
>
<Button
type="default"
size="large"
iconRight={
<ChevronDown className="transition group-data-[state=open]:rotate-180" />
}
className="text-foreground !bg-dash-sidebar justify-between"
>
<div className="text-xs flex items-center p-2">
<span>Using the Shared Pooler</span>
<Badge variant={'brand'} size={'small'} className="ml-2">
IPv4 compatible
</Badge>
</div>
</Button>
</CollapsibleTrigger_Shadcn_>
<CollapsibleContent_Shadcn_ className="bg-dash-sidebar rounded-b border text-xs">
<p className="px-3 py-2">
Only recommended when your network does not support IPv6. Added latency
compared to dedicated pooler.
</p>
<CodeBlock
wrapperClassName={cn(
'[&_pre]:border-x-0 [&_pre]:border-b-0 [&_pre]:px-4 [&_pre]:py-3',
'[&_pre]:rounded-t-none'
)}
language={lang}
value={supavisorConnectionStrings['pooler'][selectedTab]}
className="[&_code]:text-[12px] [&_code]:text-foreground"
hideLineNumbers
onCopyCallback={() => handleCopy(selectedTab, 'transaction_pooler')}
/>
</CollapsibleContent_Shadcn_>
</Collapsible_Shadcn_>
)}
</ConnectionPanel>
{sharedPoolerPreferred && ipv4Addon && (
<Admonition
type="warning"
@@ -475,14 +428,15 @@ export const DatabaseConnectionString = () => {
</p>
</Admonition>
)}
<ConnectionPanel
type="session"
title={connectionStringMethodOptions.session.label}
title="Session pooler"
contentType={contentType}
lang={lang}
badge="Shared Pooler"
fileTitle={fileTitle}
description={connectionStringMethodOptions.session.description}
description="Only recommended as an alternative to Direct Connection, when connecting via an IPv4 network."
connectionString={supavisorConnectionStrings['pooler'][selectedTab].replace(
'6543',
'5432'
@@ -511,22 +465,23 @@ export const DatabaseConnectionString = () => {
</div>
{examplePostInstallCommands && (
<div className="grid lg:grid-cols-3 gap-4 lg:gap-5 w-full px-4 md:px-7 py-8">
<StepLabel number={++stepNumber} className="items-start">
Add the configuration package to read the settings
</StepLabel>
{examplePostInstallCommands?.map((cmd) => (
<CodeBlock
key={`example-post-install-commands-${cmd}`}
className="text-sm"
wrapperClassName="lg:col-span-2"
value={cmd}
hideLineNumbers
language="bash"
>
{cmd}
</CodeBlock>
))}
<div className="grid grid-cols-2 gap-20 w-full px-4 md:px-7 py-10">
<div>
<StepLabel number={++stepNumber} className="mb-4">
Add the configuration package to read the settings
</StepLabel>
{examplePostInstallCommands?.map((cmd, i) => (
<CodeBlock
key={i}
className="text-sm"
value={cmd}
hideLineNumbers
language="bash"
>
{cmd}
</CodeBlock>
))}
</div>
</div>
)}
</div>
@@ -581,43 +536,3 @@ export const DatabaseConnectionString = () => {
</div>
)
}
const ConnectionStringMethodSelectItem = ({
method,
poolerBadge,
}: {
method: ConnectionStringMethod
poolerBadge: string
}) => (
<SelectItem_Shadcn_ value={method} className="[&>span:first-child]:top-3.5">
<div className="flex flex-col w-full py-1">
<div>{connectionStringMethodOptions[method].label}</div>
<div className="text-foreground-lighter text-xs">
{connectionStringMethodOptions[method].description}
</div>
<div className="flex items-center gap-1 mt-2">
{method === 'session' ? (
<Badge variant="outline" size="small" className="flex gap-1 text-foreground-lighter">
<InfoIcon className="w-3 h-3" />
{connectionStringMethodOptions[method].badge}
</Badge>
) : (
<Badge variant="outline" size="small" className="flex gap-1 text-foreground-lighter">
<GlobeIcon className="w-3 h-3" />
{connectionStringMethodOptions[method].badge}
</Badge>
)}
{method === 'transaction' && (
<Badge variant="outline" size="small" className="text-foreground-lighter">
{poolerBadge}
</Badge>
)}
{method === 'session' && (
<Badge variant="outline" size="small" className="text-foreground-lighter">
Shared Pooler
</Badge>
)}
</div>
</div>
</SelectItem_Shadcn_>
)