content.tsx294 lines · main
| 1 | import { useParams } from 'common' |
| 2 | import { useMemo } from 'react' |
| 3 | import { Badge } from 'ui' |
| 4 | import { CodeBlock } from 'ui-patterns/CodeBlock' |
| 5 | import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' |
| 6 | |
| 7 | import { getConnectionStrings } from '../../../DatabaseSettings.utils' |
| 8 | import { IPv4StatusPanel, type IPv4Status } from './IPv4StatusPanel' |
| 9 | import { getAddons } from '@/components/interfaces/Billing/Subscription/Subscription.utils' |
| 10 | import { |
| 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' |
| 17 | import type { |
| 18 | ConnectionStringPooler, |
| 19 | StepContentProps, |
| 20 | } from '@/components/interfaces/ConnectSheet/Connect.types' |
| 21 | import { ConnectionParameters } from '@/components/interfaces/ConnectSheet/ConnectionParameters' |
| 22 | import { |
| 23 | buildConnectionParameters, |
| 24 | buildSafeConnectionString, |
| 25 | parseConnectionParams, |
| 26 | PASSWORD_PLACEHOLDER, |
| 27 | resolveConnectionString, |
| 28 | } from '@/components/interfaces/ConnectSheet/ConnectionString.utils' |
| 29 | import { usePgbouncerConfigQuery } from '@/data/database/pgbouncer-config-query' |
| 30 | import { useSupavisorConfigurationQuery } from '@/data/database/supavisor-configuration-query' |
| 31 | import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query' |
| 32 | import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query' |
| 33 | import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' |
| 34 | import { useIsHighAvailability } from '@/hooks/misc/useSelectedProject' |
| 35 | import { pluckObjectFields } from '@/lib/helpers' |
| 36 | import { useTrack } from '@/lib/telemetry/track' |
| 37 | |
| 38 | const 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 | |
| 41 | const 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 | */ |
| 51 | const 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 | |
| 117 | const 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 | */ |
| 130 | function 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 | |
| 294 | export default DirectConnectionContent |