content.tsx294 lines · main
1import { useParams } from 'common'
2import { useMemo } from 'react'
3import { Badge } from 'ui'
4import { CodeBlock } from 'ui-patterns/CodeBlock'
5import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
6
7import { getConnectionStrings } from '../../../DatabaseSettings.utils'
8import { IPv4StatusPanel, type IPv4Status } from './IPv4StatusPanel'
9import { getAddons } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
10import {
11 DATABASE_CONNECTION_TYPES,
12 IPV4_ADDON_TEXT,
13 PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT,
14 type ConnectionStringMethod,
15 type DatabaseConnectionType,
16} from '@/components/interfaces/ConnectSheet/Connect.constants'
17import type {
18 ConnectionStringPooler,
19 StepContentProps,
20} from '@/components/interfaces/ConnectSheet/Connect.types'
21import { ConnectionParameters } from '@/components/interfaces/ConnectSheet/ConnectionParameters'
22import {
23 buildConnectionParameters,
24 buildSafeConnectionString,
25 parseConnectionParams,
26 PASSWORD_PLACEHOLDER,
27 resolveConnectionString,
28} from '@/components/interfaces/ConnectSheet/ConnectionString.utils'
29import { usePgbouncerConfigQuery } from '@/data/database/pgbouncer-config-query'
30import { useSupavisorConfigurationQuery } from '@/data/database/supavisor-configuration-query'
31import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
32import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
33import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
34import { useIsHighAvailability } from '@/hooks/misc/useSelectedProject'
35import { pluckObjectFields } from '@/lib/helpers'
36import { useTrack } from '@/lib/telemetry/track'
37
38const buildPsqlCommand = (params: { host: string; port: string; database: string; user: string }) =>
39 `psql -h ${params.host} -p ${params.port} -d ${params.database} -U ${params.user}`
40
41const buildJdbcString = (params: { host: string; port: string; database: string; user: string }) =>
42 `jdbc:postgresql://${params.host}:${params.port}/${params.database}?user=${params.user}&password=${PASSWORD_PLACEHOLDER}`
43
44/**
45 * [Joshen] ConnectStepsSection does something similar but since only this page needs to consider connection strings
46 * from all databases (including read replicas), am opting to separate the logic for retrieving connection strings here
47 *
48 * We can however, consider to shift this logic into ConnectStepsSection, such that we can consider read replicas for
49 * the other tabs like "Framework" and "ORM" too. However, leaving them out for now and only updating "Direct"
50 */
51const useConnectionStringDatabases = () => {
52 const { ref: projectRef } = useParams()
53 const { hasAccess: allowPgBouncerSelection } = useCheckEntitlements('dedicated_pooler')
54
55 const { data: databases = [] } = useReadReplicasQuery({ projectRef })
56 const { data: pgbouncerConfig } = usePgbouncerConfigQuery({ projectRef })
57 const { data: supavisorConfig } = useSupavisorConfigurationQuery({ projectRef })
58 const { data: addons } = useProjectAddonsQuery({ projectRef })
59 const { ipv4: ipv4Addon } = getAddons(addons?.selected_addons ?? [])
60
61 const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at']
62 const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' }
63
64 return Object.fromEntries(
65 databases.map((db) => {
66 const connectionInfo = pluckObjectFields(db || emptyState, DB_FIELDS)
67 const poolingConfigurationShared = supavisorConfig?.find(
68 (x) => x.identifier === db.identifier
69 )
70 const poolingConfigurationDedicated = allowPgBouncerSelection ? pgbouncerConfig : undefined
71
72 const connectionStringsShared = getConnectionStrings({
73 connectionInfo,
74 poolingInfo: {
75 connectionString: poolingConfigurationShared?.connection_string ?? '',
76 db_host: poolingConfigurationShared?.db_host ?? '',
77 db_name: poolingConfigurationShared?.db_name ?? '',
78 db_port: poolingConfigurationShared?.db_port ?? 0,
79 db_user: poolingConfigurationShared?.db_user ?? '',
80 },
81 metadata: { projectRef: db.identifier },
82 })
83
84 const connectionStringsDedicated =
85 poolingConfigurationDedicated !== undefined
86 ? getConnectionStrings({
87 connectionInfo,
88 poolingInfo: {
89 connectionString: poolingConfigurationDedicated.connection_string.replace(
90 projectRef ?? '_',
91 db.identifier
92 ),
93 db_host: poolingConfigurationDedicated.db_host,
94 db_name: poolingConfigurationDedicated.db_name,
95 db_port: poolingConfigurationDedicated.db_port,
96 db_user: poolingConfigurationDedicated.db_user,
97 },
98 metadata: { projectRef: db.identifier },
99 })
100 : undefined
101
102 return [
103 db.identifier,
104 {
105 transactionShared: connectionStringsShared.pooler.uri,
106 sessionShared: connectionStringsShared.pooler.uri.replace('6543', '5432'),
107 transactionDedicated: connectionStringsDedicated?.pooler.uri,
108 sessionDedicated: connectionStringsDedicated?.pooler.uri.replace('6543', '5432'),
109 ipv4SupportedForDedicatedPooler: !!ipv4Addon,
110 direct: connectionStringsShared.direct.uri,
111 },
112 ]
113 })
114 )
115}
116
117const CONNECTION_METHOD_TO_TELEMETRY: Record<
118 ConnectionStringMethod,
119 'direct' | 'transaction_pooler' | 'session_pooler'
120> = {
121 direct: 'direct',
122 transaction: 'transaction_pooler',
123 session: 'session_pooler',
124}
125
126/**
127 * Step component for direct database connections.
128 * Uses state to determine which connection string to show.
129 */
130function DirectConnectionContent({ state }: StepContentProps) {
131 const track = useTrack()
132 const { ref: projectRef } = useParams()
133 const { hasAccess: hasDedicatedPooler } = useCheckEntitlements('dedicated_pooler')
134 const isHighAvailability = useIsHighAvailability()
135
136 const connectionSource = state.connectionSource
137 const connectionType = (state.connectionType as DatabaseConnectionType) ?? 'uri'
138 const connectionMethod = (state.connectionMethod as ConnectionStringMethod) ?? 'direct'
139 const useSharedPooler = Boolean(state.useSharedPooler)
140
141 const connectionStrings = useConnectionStringDatabases()
142 const connectionStringPooler: ConnectionStringPooler | undefined =
143 connectionStrings[connectionSource as keyof typeof connectionStrings]
144 const hasIPv4Addon = connectionStringPooler?.ipv4SupportedForDedicatedPooler ?? false
145
146 // Determine which connection string to use
147 const resolvedConnectionString = useMemo(
148 () =>
149 resolveConnectionString({
150 connectionMethod,
151 useSharedPooler,
152 connectionStringPooler,
153 }),
154 [connectionMethod, useSharedPooler, connectionStringPooler]
155 )
156
157 const connectionParams = useMemo(
158 () => parseConnectionParams(resolvedConnectionString),
159 [resolvedConnectionString]
160 )
161
162 const safeConnectionString = useMemo(
163 () => buildSafeConnectionString(resolvedConnectionString, connectionParams),
164 [resolvedConnectionString, connectionParams]
165 )
166
167 const connectionString = useMemo(() => {
168 switch (connectionType) {
169 case 'psql':
170 return buildPsqlCommand(connectionParams)
171 case 'jdbc':
172 return buildJdbcString(connectionParams)
173 case 'php':
174 return `DATABASE_URL=${safeConnectionString}`
175 case 'uri':
176 default:
177 return safeConnectionString
178 }
179 }, [connectionType, connectionParams, safeConnectionString])
180
181 const trackCopy = () => {
182 const typeConfig = DATABASE_CONNECTION_TYPES.find((t) => t.id === connectionType)
183 track('connection_string_copied', {
184 connectionType: typeConfig?.label ?? connectionType,
185 lang: typeConfig?.lang ?? 'bash',
186 connectionMethod: CONNECTION_METHOD_TO_TELEMETRY[connectionMethod],
187 connectionTab: 'Connection String',
188 source: 'studio',
189 })
190 }
191
192 if (!resolvedConnectionString) {
193 return (
194 <div className="p-4">
195 <GenericSkeletonLoader />
196 </div>
197 )
198 }
199
200 const sharedPoolerPreferred = !hasDedicatedPooler
201 const ipv4AddOnUrl = {
202 text: 'IPv4 add-on',
203 url: `/project/${projectRef}/settings/addons?panel=ipv4`,
204 }
205 const ipv4SettingsUrl = {
206 text: 'IPv4 settings',
207 url: `/project/${projectRef}/settings/addons?panel=ipv4`,
208 }
209 const poolerSettingsUrl = {
210 text: 'Pooler settings',
211 url: `/project/${projectRef}/database/settings#connection-pooling`,
212 }
213 const buttonLinks = !hasIPv4Addon
214 ? [ipv4AddOnUrl, ...(sharedPoolerPreferred ? [poolerSettingsUrl] : [])]
215 : [ipv4SettingsUrl, ...(sharedPoolerPreferred ? [poolerSettingsUrl] : [])]
216
217 let ipv4Status: IPv4Status
218 if (connectionMethod === 'direct') {
219 ipv4Status = {
220 type: !hasIPv4Addon ? 'error' : 'success',
221 title: !hasIPv4Addon ? 'Not IPv4 compatible' : 'IPv4 compatible',
222 description:
223 !sharedPoolerPreferred && !hasIPv4Addon
224 ? PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT
225 : sharedPoolerPreferred
226 ? 'Use Session Pooler if on a IPv4 network or purchase IPv4 add-on'
227 : IPV4_ADDON_TEXT,
228 links: buttonLinks,
229 }
230 } else if (connectionMethod === 'transaction') {
231 const isUsingSharedPooler = useSharedPooler || !hasDedicatedPooler
232 ipv4Status = {
233 type: !isUsingSharedPooler && !hasIPv4Addon ? 'error' : 'success',
234 title: !isUsingSharedPooler && !hasIPv4Addon ? 'Not IPv4 compatible' : 'IPv4 compatible',
235 description:
236 !isUsingSharedPooler && !hasIPv4Addon
237 ? PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT
238 : isUsingSharedPooler
239 ? 'Transaction pooler connections are IPv4 proxied for free.'
240 : IPV4_ADDON_TEXT,
241 links: !isUsingSharedPooler ? buttonLinks : undefined,
242 }
243 } else {
244 ipv4Status = {
245 type: 'success',
246 title: 'IPv4 compatible',
247 description: 'Session pooler connections are IPv4 proxied for free',
248 }
249 }
250
251 const poolerBadge =
252 connectionMethod === 'transaction'
253 ? useSharedPooler || !hasDedicatedPooler
254 ? 'Shared Pooler'
255 : 'Dedicated Pooler'
256 : connectionMethod === 'session'
257 ? 'Shared Pooler'
258 : null
259
260 return (
261 <div className="flex flex-col gap-2">
262 {poolerBadge && !isHighAvailability && (
263 <div className="flex items-center gap-x-2">
264 <Badge>{poolerBadge}</Badge>
265 </div>
266 )}
267 <CodeBlock
268 className="[&_code]:text-foreground"
269 wrapperClassName="lg:col-span-2"
270 value={connectionString}
271 hideLineNumbers
272 language="bash"
273 onCopyCallback={trackCopy}
274 >
275 {connectionString}
276 </CodeBlock>
277 {projectRef && !isHighAvailability && (
278 <div className="mt-2">
279 <IPv4StatusPanel
280 method={connectionMethod}
281 ipv4Status={ipv4Status}
282 projectRef={projectRef}
283 />
284 </div>
285 )}
286 <ConnectionParameters
287 parameters={buildConnectionParameters(connectionParams)}
288 onCopy={trackCopy}
289 />
290 </div>
291 )
292}
293
294export default DirectConnectionContent