ConnectStepsSection.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import { useParams } from 'common'
  2. import dynamic from 'next/dynamic'
  3. import Link from 'next/link'
  4. import { useMemo, useRef } from 'react'
  5. import { Button } from 'ui'
  6. import { Admonition } from 'ui-patterns'
  7. import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
  8. import type {
  9. ConnectionStringPooler,
  10. ConnectState,
  11. ProjectKeys,
  12. ResolvedStep,
  13. StepContentProps,
  14. } from './Connect.types'
  15. import { ConnectSheetStep } from './ConnectSheetStep'
  16. import { CopyPromptAdmonition } from './CopyPromptAdmonition'
  17. import { getConnectionStrings } from './DatabaseSettings.utils'
  18. import { getAddons } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
  19. import { DocsButton } from '@/components/ui/DocsButton'
  20. import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
  21. import { usePgbouncerConfigQuery } from '@/data/database/pgbouncer-config-query'
  22. import { useSupavisorConfigurationQuery } from '@/data/database/supavisor-configuration-query'
  23. import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
  24. import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
  25. import { DOCS_URL } from '@/lib/constants'
  26. import { pluckObjectFields } from '@/lib/helpers'
  27. interface ConnectStepsSectionProps {
  28. steps: ResolvedStep[]
  29. state: ConnectState
  30. projectKeys: ProjectKeys
  31. }
  32. /**
  33. * Resolves a content path template by replacing {{key}} placeholders with state values.
  34. * Empty segments are filtered out to handle optional state values like frameworkVariant.
  35. *
  36. * Examples:
  37. * - '{{framework}}/{{frameworkVariant}}/{{library}}' with state {framework: 'nextjs', frameworkVariant: 'app', library: 'brivenjs'}
  38. * → 'nextjs/app/brivenjs'
  39. * - '{{orm}}' with state {orm: 'prisma'}
  40. * → 'prisma'
  41. * - 'steps/install' (no templates)
  42. * → 'steps/install'
  43. */
  44. function resolveContentPath(template: string, state: ConnectState): string {
  45. return template
  46. .replace(/\{\{(\w+)\}\}/g, (_, key) => String(state[key] ?? ''))
  47. .split('/')
  48. .filter(Boolean)
  49. .join('/')
  50. }
  51. /**
  52. * Hook to fetch and prepare connection strings for step content.
  53. */
  54. function useConnectionStringPooler(): ConnectionStringPooler {
  55. const { ref: projectRef } = useParams()
  56. const { hasAccess: allowPgBouncerSelection } = useCheckEntitlements('dedicated_pooler')
  57. const { data: settings } = useProjectSettingsV2Query({ projectRef })
  58. const { data: pgbouncerConfig } = usePgbouncerConfigQuery({ projectRef })
  59. const { data: supavisorConfig } = useSupavisorConfigurationQuery({ projectRef })
  60. const { data: addons } = useProjectAddonsQuery({ projectRef })
  61. const { ipv4: ipv4Addon } = getAddons(addons?.selected_addons ?? [])
  62. const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at']
  63. const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' }
  64. const connectionInfo = pluckObjectFields(settings || emptyState, DB_FIELDS)
  65. const poolingConfigurationShared = supavisorConfig?.find((x) => x.database_type === 'PRIMARY')
  66. const poolingConfigurationDedicated = allowPgBouncerSelection ? pgbouncerConfig : undefined
  67. const connectionStringsShared = getConnectionStrings({
  68. connectionInfo,
  69. poolingInfo: {
  70. connectionString: poolingConfigurationShared?.connection_string ?? '',
  71. db_host: poolingConfigurationShared?.db_host ?? '',
  72. db_name: poolingConfigurationShared?.db_name ?? '',
  73. db_port: poolingConfigurationShared?.db_port ?? 0,
  74. db_user: poolingConfigurationShared?.db_user ?? '',
  75. },
  76. metadata: { projectRef },
  77. })
  78. const connectionStringsDedicated =
  79. poolingConfigurationDedicated !== undefined
  80. ? getConnectionStrings({
  81. connectionInfo,
  82. poolingInfo: {
  83. connectionString: poolingConfigurationDedicated.connection_string,
  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 },
  90. })
  91. : undefined
  92. return useMemo(
  93. () => ({
  94. transactionShared: connectionStringsShared.pooler.uri,
  95. sessionShared: connectionStringsShared.pooler.uri.replace('6543', '5432'),
  96. transactionDedicated: connectionStringsDedicated?.pooler.uri,
  97. sessionDedicated: connectionStringsDedicated?.pooler.uri.replace('6543', '5432'),
  98. ipv4SupportedForDedicatedPooler: !!ipv4Addon,
  99. direct: connectionStringsShared.direct.uri,
  100. }),
  101. [connectionStringsShared, connectionStringsDedicated, ipv4Addon]
  102. )
  103. }
  104. /**
  105. * Dynamically loads and renders a content component from the content directory.
  106. * All step content uses this unified loader - no built-in component registry needed.
  107. */
  108. function StepContent({
  109. contentId,
  110. state,
  111. projectKeys,
  112. connectionStringPooler,
  113. }: {
  114. contentId: string
  115. state: ConnectState
  116. projectKeys: ProjectKeys
  117. connectionStringPooler: ConnectionStringPooler
  118. }) {
  119. // Resolve any template placeholders in the content path
  120. const filePath = useMemo(() => resolveContentPath(contentId, state), [contentId, state])
  121. // Dynamically import the content component
  122. const ContentComponent = useMemo(() => {
  123. return dynamic<StepContentProps>(() => import(`./content/${filePath}/content`), {
  124. loading: () => (
  125. <div className="p-4 min-h-[200px]">
  126. <GenericSkeletonLoader />
  127. </div>
  128. ),
  129. })
  130. }, [filePath])
  131. return (
  132. <ContentComponent
  133. state={state}
  134. projectKeys={projectKeys}
  135. connectionStringPooler={connectionStringPooler}
  136. />
  137. )
  138. }
  139. export function ConnectStepsSection({ steps, state, projectKeys }: ConnectStepsSectionProps) {
  140. const { ref } = useParams()
  141. const stepsContainerRef = useRef<HTMLDivElement | null>(null)
  142. const connectionStringPooler = useConnectionStringPooler()
  143. const { data: ipv4Addon } = useProjectAddonsQuery(
  144. { projectRef: ref },
  145. {
  146. select: (data) => {
  147. const selectedAddons = data?.selected_addons ?? []
  148. return selectedAddons.find((addon) => addon.type === 'ipv4')
  149. },
  150. }
  151. )
  152. const showIpv4AddonNotice =
  153. state.mode === 'direct' &&
  154. !ipv4Addon &&
  155. (state.connectionMethod === 'direct' ||
  156. (state.connectionMethod === 'transaction' && !state.useSharedPooler))
  157. if (steps.length === 0) return null
  158. return (
  159. <div className="bg-muted/50 flex-1">
  160. <div className="p-8 flex flex-col gap-y-6">
  161. <h3>Connect your app</h3>
  162. <CopyPromptAdmonition stepsContainerRef={stepsContainerRef} />
  163. {showIpv4AddonNotice && (
  164. <Admonition
  165. type="default"
  166. title={`${state.connectionMethod === 'direct' ? 'Direct connections use' : 'Transaction pooler uses'} IPv6 by default`}
  167. description="Enable the dedicated IPv4 address add-on to connect from IPv4-only networks"
  168. actions={[
  169. <Button asChild key="addon" type="default">
  170. <Link href={`/project/${ref}/settings/addons?panel=ipv4`}>Enable IPv4 add-on</Link>
  171. </Button>,
  172. <DocsButton key="docs" href={`${DOCS_URL}/guides/platform/ipv4-address`} />,
  173. ]}
  174. />
  175. )}
  176. <div className="mt-6" ref={stepsContainerRef}>
  177. {steps.map((step, index) => (
  178. <ConnectSheetStep
  179. key={step.id}
  180. number={index + 1}
  181. title={step.title}
  182. description={step.description}
  183. >
  184. <StepContent
  185. contentId={step.content}
  186. state={state}
  187. projectKeys={projectKeys}
  188. connectionStringPooler={connectionStringPooler}
  189. />
  190. </ConnectSheetStep>
  191. ))}
  192. </div>
  193. </div>
  194. </div>
  195. )
  196. }