| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- import { useParams } from 'common'
- import dynamic from 'next/dynamic'
- import Link from 'next/link'
- import { useMemo, useRef } from 'react'
- import { Button } from 'ui'
- import { Admonition } from 'ui-patterns'
- import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
- import type {
- ConnectionStringPooler,
- ConnectState,
- ProjectKeys,
- ResolvedStep,
- StepContentProps,
- } from './Connect.types'
- import { ConnectSheetStep } from './ConnectSheetStep'
- import { CopyPromptAdmonition } from './CopyPromptAdmonition'
- import { getConnectionStrings } from './DatabaseSettings.utils'
- import { getAddons } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
- import { DocsButton } from '@/components/ui/DocsButton'
- import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
- import { usePgbouncerConfigQuery } from '@/data/database/pgbouncer-config-query'
- import { useSupavisorConfigurationQuery } from '@/data/database/supavisor-configuration-query'
- import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
- import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
- import { DOCS_URL } from '@/lib/constants'
- import { pluckObjectFields } from '@/lib/helpers'
- interface ConnectStepsSectionProps {
- steps: ResolvedStep[]
- state: ConnectState
- projectKeys: ProjectKeys
- }
- /**
- * Resolves a content path template by replacing {{key}} placeholders with state values.
- * Empty segments are filtered out to handle optional state values like frameworkVariant.
- *
- * Examples:
- * - '{{framework}}/{{frameworkVariant}}/{{library}}' with state {framework: 'nextjs', frameworkVariant: 'app', library: 'brivenjs'}
- * → 'nextjs/app/brivenjs'
- * - '{{orm}}' with state {orm: 'prisma'}
- * → 'prisma'
- * - 'steps/install' (no templates)
- * → 'steps/install'
- */
- function resolveContentPath(template: string, state: ConnectState): string {
- return template
- .replace(/\{\{(\w+)\}\}/g, (_, key) => String(state[key] ?? ''))
- .split('/')
- .filter(Boolean)
- .join('/')
- }
- /**
- * Hook to fetch and prepare connection strings for step content.
- */
- function useConnectionStringPooler(): ConnectionStringPooler {
- const { ref: projectRef } = useParams()
- const { hasAccess: allowPgBouncerSelection } = useCheckEntitlements('dedicated_pooler')
- const { data: settings } = useProjectSettingsV2Query({ projectRef })
- const { data: pgbouncerConfig } = usePgbouncerConfigQuery({ projectRef })
- const { data: supavisorConfig } = useSupavisorConfigurationQuery({ projectRef })
- const { data: addons } = useProjectAddonsQuery({ projectRef })
- const { ipv4: ipv4Addon } = getAddons(addons?.selected_addons ?? [])
- 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(settings || emptyState, DB_FIELDS)
- const poolingConfigurationShared = supavisorConfig?.find((x) => x.database_type === 'PRIMARY')
- const poolingConfigurationDedicated = allowPgBouncerSelection ? pgbouncerConfig : undefined
- const connectionStringsShared = getConnectionStrings({
- connectionInfo,
- poolingInfo: {
- connectionString: poolingConfigurationShared?.connection_string ?? '',
- db_host: poolingConfigurationShared?.db_host ?? '',
- db_name: poolingConfigurationShared?.db_name ?? '',
- db_port: poolingConfigurationShared?.db_port ?? 0,
- db_user: poolingConfigurationShared?.db_user ?? '',
- },
- metadata: { projectRef },
- })
- const connectionStringsDedicated =
- poolingConfigurationDedicated !== undefined
- ? getConnectionStrings({
- connectionInfo,
- poolingInfo: {
- connectionString: poolingConfigurationDedicated.connection_string,
- db_host: poolingConfigurationDedicated.db_host,
- db_name: poolingConfigurationDedicated.db_name,
- db_port: poolingConfigurationDedicated.db_port,
- db_user: poolingConfigurationDedicated.db_user,
- },
- metadata: { projectRef },
- })
- : undefined
- return useMemo(
- () => ({
- transactionShared: connectionStringsShared.pooler.uri,
- sessionShared: connectionStringsShared.pooler.uri.replace('6543', '5432'),
- transactionDedicated: connectionStringsDedicated?.pooler.uri,
- sessionDedicated: connectionStringsDedicated?.pooler.uri.replace('6543', '5432'),
- ipv4SupportedForDedicatedPooler: !!ipv4Addon,
- direct: connectionStringsShared.direct.uri,
- }),
- [connectionStringsShared, connectionStringsDedicated, ipv4Addon]
- )
- }
- /**
- * Dynamically loads and renders a content component from the content directory.
- * All step content uses this unified loader - no built-in component registry needed.
- */
- function StepContent({
- contentId,
- state,
- projectKeys,
- connectionStringPooler,
- }: {
- contentId: string
- state: ConnectState
- projectKeys: ProjectKeys
- connectionStringPooler: ConnectionStringPooler
- }) {
- // Resolve any template placeholders in the content path
- const filePath = useMemo(() => resolveContentPath(contentId, state), [contentId, state])
- // Dynamically import the content component
- const ContentComponent = useMemo(() => {
- return dynamic<StepContentProps>(() => import(`./content/${filePath}/content`), {
- loading: () => (
- <div className="p-4 min-h-[200px]">
- <GenericSkeletonLoader />
- </div>
- ),
- })
- }, [filePath])
- return (
- <ContentComponent
- state={state}
- projectKeys={projectKeys}
- connectionStringPooler={connectionStringPooler}
- />
- )
- }
- export function ConnectStepsSection({ steps, state, projectKeys }: ConnectStepsSectionProps) {
- const { ref } = useParams()
- const stepsContainerRef = useRef<HTMLDivElement | null>(null)
- const connectionStringPooler = useConnectionStringPooler()
- const { data: ipv4Addon } = useProjectAddonsQuery(
- { projectRef: ref },
- {
- select: (data) => {
- const selectedAddons = data?.selected_addons ?? []
- return selectedAddons.find((addon) => addon.type === 'ipv4')
- },
- }
- )
- const showIpv4AddonNotice =
- state.mode === 'direct' &&
- !ipv4Addon &&
- (state.connectionMethod === 'direct' ||
- (state.connectionMethod === 'transaction' && !state.useSharedPooler))
- if (steps.length === 0) return null
- return (
- <div className="bg-muted/50 flex-1">
- <div className="p-8 flex flex-col gap-y-6">
- <h3>Connect your app</h3>
- <CopyPromptAdmonition stepsContainerRef={stepsContainerRef} />
- {showIpv4AddonNotice && (
- <Admonition
- type="default"
- title={`${state.connectionMethod === 'direct' ? 'Direct connections use' : 'Transaction pooler uses'} IPv6 by default`}
- description="Enable the dedicated IPv4 address add-on to connect from IPv4-only networks"
- actions={[
- <Button asChild key="addon" type="default">
- <Link href={`/project/${ref}/settings/addons?panel=ipv4`}>Enable IPv4 add-on</Link>
- </Button>,
- <DocsButton key="docs" href={`${DOCS_URL}/guides/platform/ipv4-address`} />,
- ]}
- />
- )}
- <div className="mt-6" ref={stepsContainerRef}>
- {steps.map((step, index) => (
- <ConnectSheetStep
- key={step.id}
- number={index + 1}
- title={step.title}
- description={step.description}
- >
- <StepContent
- contentId={step.content}
- state={state}
- projectKeys={projectKeys}
- connectionStringPooler={connectionStringPooler}
- />
- </ConnectSheetStep>
- ))}
- </div>
- </div>
- </div>
- )
- }
|