import { useParams } from 'common' import { BookOpen, ChevronDown, ExternalLink } from 'lucide-react' import { parseAsString, useQueryState } from 'nuqs' import { HTMLAttributes, ReactNode, useEffect, useState } from 'react' import { Badge, Button, cn, Collapsible_Shadcn_, CollapsibleContent_Shadcn_, CollapsibleTrigger_Shadcn_, DIALOG_PADDING_X, Select_Shadcn_, SelectContent_Shadcn_, SelectItem_Shadcn_, SelectTrigger_Shadcn_, SelectValue_Shadcn_, Separator, } from 'ui' import { CodeBlock } from 'ui-patterns/CodeBlock' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { CONNECTION_PARAMETERS, connectionStringMethodOptions, DATABASE_CONNECTION_TYPES, DatabaseConnectionType, IPV4_ADDON_TEXT, PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT, type ConnectionStringMethod, } from './Connect.constants' import { CodeBlockFileHeader, ConnectionPanel } from './ConnectionPanel' import { getConnectionStrings } from './DatabaseSettings.utils' import { examples, type Example } from './DirectConnectionExamples' import { getAddons } from '@/components/interfaces/Billing/Subscription/Subscription.utils' import AlertError from '@/components/ui/AlertError' import { DatabaseSelector } from '@/components/ui/DatabaseSelector' import { InlineLink } from '@/components/ui/InlineLink' import { usePgbouncerConfigQuery } from '@/data/database/pgbouncer-config-query' import { useSupavisorConfigurationQuery } from '@/data/database/supavisor-configuration-query' import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query' import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query' import { useSendEventMutation } from '@/data/telemetry/send-event-mutation' import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { DOCS_URL, IS_PLATFORM } from '@/lib/constants' import { pluckObjectFields } from '@/lib/helpers' import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector' const StepLabel = ({ number, children, ...props }: { number: number; children: ReactNode } & HTMLAttributes) => (
{number}
{children}
) /** * [Joshen] For paid projects - Dedicated pooler is always in transaction mode * So session mode connection details are always using the shared pooler (Supavisor) */ export const DatabaseConnectionString = () => { const { ref: projectRef } = useParams() const { data: org } = useSelectedOrganizationQuery() const state = useDatabaseSelectorStateSnapshot() const { hasAccess: hasDedicatedPooler, isLoading: isLoadingEntitlement, isSuccess: isSuccessEntitlement, } = useCheckEntitlements('dedicated_pooler') const sharedPoolerPreferred = !hasDedicatedPooler // 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('uri') const [selectedMethod, setSelectedMethod] = useState('direct') // 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, isPending: isLoadingPgbouncerConfig, isError: isErrorPgbouncerConfig, isSuccess: isSuccessPgBouncerConfig, } = usePgbouncerConfigQuery({ projectRef }) const { data: supavisorConfig, error: supavisorConfigError, isPending: isLoadingSupavisorConfig, isError: isErrorSupavisorConfig, isSuccess: isSuccessSupavisorConfig, } = useSupavisorConfigurationQuery({ projectRef }) const { data: databases, error: readReplicasError, isPending: isLoadingReadReplicas, isError: isErrorReadReplicas, isSuccess: isSuccessReadReplicas, } = useReadReplicasQuery({ projectRef }) const poolerError = sharedPoolerPreferred ? pgbouncerError : supavisorConfigError const isLoadingPoolerConfig = !IS_PLATFORM ? false : sharedPoolerPreferred ? isLoadingPgbouncerConfig : isLoadingSupavisorConfig const isErrorPoolerConfig = !IS_PLATFORM ? undefined : sharedPoolerPreferred ? isErrorPgbouncerConfig : isErrorSupavisorConfig const isSuccessPoolerConfig = !IS_PLATFORM ? true : sharedPoolerPreferred ? isSuccessPgBouncerConfig : isSuccessSupavisorConfig const error = poolerError || readReplicasError const isLoading = isLoadingPoolerConfig || isLoadingReadReplicas || isLoadingEntitlement const isError = isErrorPoolerConfig || isErrorReadReplicas const isSuccess = isSuccessPoolerConfig && isSuccessReadReplicas && isSuccessEntitlement const sharedPoolerConfig = supavisorConfig?.find((x) => x.identifier === state.selectedDatabaseId) const poolingConfiguration = sharedPoolerPreferred ? sharedPoolerConfig : pgbouncerConfig const selectedDatabase = (databases ?? []).find( (db) => db.identifier === state.selectedDatabaseId ) const isReplicaSelected = selectedDatabase?.identifier !== projectRef const { data: addons } = useProjectAddonsQuery({ projectRef }) const { ipv4: ipv4Addon } = getAddons(addons?.selected_addons ?? []) const { mutate: sendEvent } = useSendEventMutation() const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at'] const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' } const connectionInfo = pluckObjectFields(selectedDatabase || emptyState, DB_FIELDS) const handleCopy = ( connectionTypeId: string, connectionStringMethod: '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, connectionTab: 'Connection String', }, groups: { project: projectRef ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, }) } const supavisorConnectionStrings = getConnectionStrings({ connectionInfo, poolingInfo: { connectionString: sharedPoolerConfig?.connection_string ?? '', db_host: isReplicaSelected ? connectionInfo.db_host : (sharedPoolerConfig?.db_host ?? ''), db_name: sharedPoolerConfig?.db_name ?? '', db_port: sharedPoolerConfig?.db_port ?? 0, db_user: sharedPoolerConfig?.db_user ?? '', }, metadata: { projectRef }, }) const connectionStrings = getConnectionStrings({ connectionInfo, poolingInfo: { connectionString: isReplicaSelected ? (poolingConfiguration?.connection_string.replace( poolingConfiguration?.db_host, connectionInfo.db_host ) ?? '') : (poolingConfiguration?.connection_string ?? ''), db_host: isReplicaSelected ? connectionInfo.db_host : poolingConfiguration?.db_host, db_name: poolingConfiguration?.db_name ?? '', db_port: poolingConfiguration?.db_port ?? 0, db_user: poolingConfiguration?.db_user ?? '', }, metadata: { projectRef }, }) const lang = DATABASE_CONNECTION_TYPES.find((type) => type.id === selectedTab)?.lang ?? 'bash' const contentType = DATABASE_CONNECTION_TYPES.find((type) => type.id === selectedTab)?.contentType ?? 'input' const example: Example | undefined = examples[selectedTab as keyof typeof examples] const exampleFiles = example?.files const exampleInstallCommands = example?.installCommands const examplePostInstallCommands = example?.postInstallCommands const hasCodeExamples = exampleFiles || exampleInstallCommands const fileTitle = DATABASE_CONNECTION_TYPES.find((type) => type.id === selectedTab)?.fileTitle // [Refactor] See if we can do this in an immutable way, technically not a good practice to do this let stepNumber = 0 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 = !ipv4Addon ? [ipv4AddOnUrl, ...(sharedPoolerPreferred ? [poolerSettingsUrl] : [])] : [ipv4SettingsUrl, ...(sharedPoolerPreferred ? [poolerSettingsUrl] : [])] const poolerBadge = sharedPoolerPreferred ? 'Shared Pooler' : 'Dedicated Pooler' return (
Type {DATABASE_CONNECTION_TYPES.map((type) => ( {type.label} ))}
Method {connectionStringMethodOptions[selectedMethod].label} {Object.keys(connectionStringMethodOptions).map((method) => ( ))}

Learn how to connect to your Postgres databases. Read docs

{isLoading && (
)} {isError && (
)} {isSuccess && (
{/* // handle non terminal examples */} {hasCodeExamples && (
Install the following {exampleInstallCommands?.map((cmd) => ( {cmd} ))}
{exampleFiles && exampleFiles?.length > 0 && (
Add file to project {exampleFiles?.map((file) => (
))}
)}
)}
{hasCodeExamples && (
Connect to your database
)}
{selectedMethod === 'direct' && ( handleCopy(selectedTab, 'direct')} /> )} {selectedMethod === 'transaction' && IS_PLATFORM && ( handleCopy(selectedTab, 'transaction_pooler')} > {!sharedPoolerPreferred && !ipv4Addon && ( <> handleCopy(selectedTab, 'transaction_pooler')} />

Only recommended when your network does not support IPv6. Added latency compared to dedicated pooler.

)}
)} {selectedMethod === 'session' && IS_PLATFORM && ( handleCopy(selectedTab, 'session_pooler')} /> )}
{examplePostInstallCommands && (
Add the configuration package to read the settings {examplePostInstallCommands?.map((cmd) => ( {cmd} ))}
)}
)} {selectedTab === 'python' && ( <>

Connecting to SQL Alchemy

Please use postgresql:// instead of postgres:// as your dialect when connecting via SQLAlchemy.

Example: create_engine("postgresql+psycopg2://...")

)}

Reset your database password

You may reset your database password in your project's{' '} Database Settings

) } const ConnectionStringMethodSelectItem = ({ method, poolerBadge, }: { method: ConnectionStringMethod poolerBadge?: string }) => { const badges: ReactNode[] = [] if (method !== 'direct') { badges.push( Shared Pooler ) } if (poolerBadge === 'Dedicated Pooler') { badges.push( {poolerBadge} ) } return (
{connectionStringMethodOptions[method].label}
{connectionStringMethodOptions[method].description}
{badges.map((badge) => badge)}
) }