content.tsx 11 KB

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