DatabaseConnectionString.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679
  1. // @ts-nocheck
  2. import { useParams } from 'common'
  3. import { BookOpen, ChevronDown, ExternalLink } from 'lucide-react'
  4. import { parseAsString, useQueryState } from 'nuqs'
  5. import { HTMLAttributes, ReactNode, useEffect, useState } from 'react'
  6. import {
  7. Badge,
  8. Button,
  9. cn,
  10. Collapsible,
  11. CollapsibleContent,
  12. CollapsibleTrigger,
  13. DIALOG_PADDING_X,
  14. Select,
  15. SelectContent,
  16. SelectItem,
  17. SelectTrigger,
  18. SelectValue,
  19. Separator,
  20. } from 'ui'
  21. import { CodeBlock } from 'ui-patterns/CodeBlock'
  22. import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
  23. import {
  24. CONNECTION_PARAMETERS,
  25. connectionStringMethodOptions,
  26. DATABASE_CONNECTION_TYPES,
  27. DatabaseConnectionType,
  28. IPV4_ADDON_TEXT,
  29. PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT,
  30. type ConnectionStringMethod,
  31. } from './Connect.constants'
  32. import { CodeBlockFileHeader, ConnectionPanel } from './ConnectionPanel'
  33. import { getConnectionStrings } from './DatabaseSettings.utils'
  34. import { examples, type Example } from './DirectConnectionExamples'
  35. import { getAddons } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
  36. import AlertError from '@/components/ui/AlertError'
  37. import { DatabaseSelector } from '@/components/ui/DatabaseSelector'
  38. import { InlineLink } from '@/components/ui/InlineLink'
  39. import { usePgbouncerConfigQuery } from '@/data/database/pgbouncer-config-query'
  40. import { useSupavisorConfigurationQuery } from '@/data/database/supavisor-configuration-query'
  41. import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
  42. import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
  43. import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
  44. import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
  45. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  46. import { DOCS_URL, IS_PLATFORM } from '@/lib/constants'
  47. import { pluckObjectFields } from '@/lib/helpers'
  48. import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
  49. const StepLabel = ({
  50. number,
  51. children,
  52. ...props
  53. }: { number: number; children: ReactNode } & HTMLAttributes<HTMLDivElement>) => (
  54. <div {...props} className={cn('flex items-center gap-2', props.className)}>
  55. <div className="flex font-mono text-xs items-center justify-center min-w-6 w-6 h-6 border border-strong rounded-md bg-surface-100">
  56. {number}
  57. </div>
  58. <span>{children}</span>
  59. </div>
  60. )
  61. /**
  62. * [Joshen] For paid projects - Dedicated pooler is always in transaction mode
  63. * So session mode connection details are always using the shared pooler (Supavisor)
  64. */
  65. export const DatabaseConnectionString = () => {
  66. const { ref: projectRef } = useParams()
  67. const { data: org } = useSelectedOrganizationQuery()
  68. const state = useDatabaseSelectorStateSnapshot()
  69. const {
  70. hasAccess: hasDedicatedPooler,
  71. isLoading: isLoadingEntitlement,
  72. isSuccess: isSuccessEntitlement,
  73. } = useCheckEntitlements('dedicated_pooler')
  74. const sharedPoolerPreferred = !hasDedicatedPooler
  75. // URL state management
  76. const [queryType, setQueryType] = useQueryState('type', parseAsString.withDefault('uri'))
  77. const [querySource, setQuerySource] = useQueryState('source', parseAsString)
  78. const [queryMethod, setQueryMethod] = useQueryState('method', parseAsString.withDefault('direct'))
  79. const [selectedTab, setSelectedTab] = useState<DatabaseConnectionType>('uri')
  80. const [selectedMethod, setSelectedMethod] = useState<ConnectionStringMethod>('direct')
  81. // Sync URL state with component state on mount and when URL changes
  82. useEffect(() => {
  83. const validTypes = DATABASE_CONNECTION_TYPES.map((t) => t.id)
  84. if (queryType && validTypes.includes(queryType as DatabaseConnectionType)) {
  85. setSelectedTab(queryType as DatabaseConnectionType)
  86. } else if (queryType && !validTypes.includes(queryType as DatabaseConnectionType)) {
  87. setQueryType('uri')
  88. setSelectedTab('uri')
  89. }
  90. const validMethods: ConnectionStringMethod[] = ['direct', 'transaction', 'session']
  91. if (queryMethod && validMethods.includes(queryMethod as ConnectionStringMethod)) {
  92. setSelectedMethod(queryMethod as ConnectionStringMethod)
  93. } else if (queryMethod && !validMethods.includes(queryMethod as ConnectionStringMethod)) {
  94. setQueryMethod('direct')
  95. setSelectedMethod('direct')
  96. }
  97. if (querySource && querySource !== state.selectedDatabaseId) {
  98. state.setSelectedDatabaseId(querySource)
  99. } else if (!querySource && state.selectedDatabaseId !== projectRef) {
  100. state.setSelectedDatabaseId(projectRef)
  101. }
  102. }, [queryType, queryMethod, querySource, state])
  103. // Sync component state changes back to URL
  104. const handleTabChange = (connectionType: DatabaseConnectionType) => {
  105. setSelectedTab(connectionType)
  106. setQueryType(connectionType)
  107. }
  108. const handleMethodChange = (method: ConnectionStringMethod) => {
  109. setSelectedMethod(method)
  110. setQueryMethod(method)
  111. }
  112. const handleDatabaseChange = (databaseId: string) => {
  113. if (databaseId === projectRef) {
  114. setQuerySource(null)
  115. } else {
  116. setQuerySource(databaseId)
  117. }
  118. }
  119. // Sync database selector state changes back to URL
  120. useEffect(() => {
  121. if (state.selectedDatabaseId && state.selectedDatabaseId !== querySource) {
  122. // Only set source in URL if it's not the primary database
  123. if (state.selectedDatabaseId === projectRef) {
  124. setQuerySource(null)
  125. } else {
  126. setQuerySource(state.selectedDatabaseId)
  127. }
  128. }
  129. }, [state.selectedDatabaseId, querySource, projectRef])
  130. const {
  131. data: pgbouncerConfig,
  132. error: pgbouncerError,
  133. isPending: isLoadingPgbouncerConfig,
  134. isError: isErrorPgbouncerConfig,
  135. isSuccess: isSuccessPgBouncerConfig,
  136. } = usePgbouncerConfigQuery({ projectRef })
  137. const {
  138. data: supavisorConfig,
  139. error: supavisorConfigError,
  140. isPending: isLoadingSupavisorConfig,
  141. isError: isErrorSupavisorConfig,
  142. isSuccess: isSuccessSupavisorConfig,
  143. } = useSupavisorConfigurationQuery({ projectRef })
  144. const {
  145. data: databases,
  146. error: readReplicasError,
  147. isPending: isLoadingReadReplicas,
  148. isError: isErrorReadReplicas,
  149. isSuccess: isSuccessReadReplicas,
  150. } = useReadReplicasQuery({ projectRef })
  151. const poolerError = sharedPoolerPreferred ? pgbouncerError : supavisorConfigError
  152. const isLoadingPoolerConfig = !IS_PLATFORM
  153. ? false
  154. : sharedPoolerPreferred
  155. ? isLoadingPgbouncerConfig
  156. : isLoadingSupavisorConfig
  157. const isErrorPoolerConfig = !IS_PLATFORM
  158. ? undefined
  159. : sharedPoolerPreferred
  160. ? isErrorPgbouncerConfig
  161. : isErrorSupavisorConfig
  162. const isSuccessPoolerConfig = !IS_PLATFORM
  163. ? true
  164. : sharedPoolerPreferred
  165. ? isSuccessPgBouncerConfig
  166. : isSuccessSupavisorConfig
  167. const error = poolerError || readReplicasError
  168. const isLoading = isLoadingPoolerConfig || isLoadingReadReplicas || isLoadingEntitlement
  169. const isError = isErrorPoolerConfig || isErrorReadReplicas
  170. const isSuccess = isSuccessPoolerConfig && isSuccessReadReplicas && isSuccessEntitlement
  171. const sharedPoolerConfig = supavisorConfig?.find((x) => x.identifier === state.selectedDatabaseId)
  172. const poolingConfiguration = sharedPoolerPreferred ? sharedPoolerConfig : pgbouncerConfig
  173. const selectedDatabase = (databases ?? []).find(
  174. (db) => db.identifier === state.selectedDatabaseId
  175. )
  176. const isReplicaSelected = selectedDatabase?.identifier !== projectRef
  177. const { data: addons } = useProjectAddonsQuery({ projectRef })
  178. const { ipv4: ipv4Addon } = getAddons(addons?.selected_addons ?? [])
  179. const { mutate: sendEvent } = useSendEventMutation()
  180. const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at']
  181. const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' }
  182. const connectionInfo = pluckObjectFields(selectedDatabase || emptyState, DB_FIELDS)
  183. const handleCopy = (
  184. connectionTypeId: string,
  185. connectionStringMethod: 'direct' | 'transaction_pooler' | 'session_pooler'
  186. ) => {
  187. const connectionInfo = DATABASE_CONNECTION_TYPES.find((type) => type.id === connectionTypeId)
  188. const connectionType = connectionInfo?.label ?? 'Unknown'
  189. const lang = connectionInfo?.lang ?? 'Unknown'
  190. sendEvent({
  191. action: 'connection_string_copied',
  192. properties: {
  193. connectionType,
  194. lang,
  195. connectionMethod: connectionStringMethod,
  196. connectionTab: 'Connection String',
  197. },
  198. groups: { project: projectRef ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
  199. })
  200. }
  201. const supavisorConnectionStrings = getConnectionStrings({
  202. connectionInfo,
  203. poolingInfo: {
  204. connectionString: sharedPoolerConfig?.connection_string ?? '',
  205. db_host: isReplicaSelected ? connectionInfo.db_host : (sharedPoolerConfig?.db_host ?? ''),
  206. db_name: sharedPoolerConfig?.db_name ?? '',
  207. db_port: sharedPoolerConfig?.db_port ?? 0,
  208. db_user: sharedPoolerConfig?.db_user ?? '',
  209. },
  210. metadata: { projectRef },
  211. })
  212. const connectionStrings = getConnectionStrings({
  213. connectionInfo,
  214. poolingInfo: {
  215. connectionString: isReplicaSelected
  216. ? (poolingConfiguration?.connection_string.replace(
  217. poolingConfiguration?.db_host,
  218. connectionInfo.db_host
  219. ) ?? '')
  220. : (poolingConfiguration?.connection_string ?? ''),
  221. db_host: isReplicaSelected ? connectionInfo.db_host : poolingConfiguration?.db_host,
  222. db_name: poolingConfiguration?.db_name ?? '',
  223. db_port: poolingConfiguration?.db_port ?? 0,
  224. db_user: poolingConfiguration?.db_user ?? '',
  225. },
  226. metadata: { projectRef },
  227. })
  228. const lang = DATABASE_CONNECTION_TYPES.find((type) => type.id === selectedTab)?.lang ?? 'bash'
  229. const contentType =
  230. DATABASE_CONNECTION_TYPES.find((type) => type.id === selectedTab)?.contentType ?? 'input'
  231. const example: Example | undefined = examples[selectedTab as keyof typeof examples]
  232. const exampleFiles = example?.files
  233. const exampleInstallCommands = example?.installCommands
  234. const examplePostInstallCommands = example?.postInstallCommands
  235. const hasCodeExamples = exampleFiles || exampleInstallCommands
  236. const fileTitle = DATABASE_CONNECTION_TYPES.find((type) => type.id === selectedTab)?.fileTitle
  237. // [Refactor] See if we can do this in an immutable way, technically not a good practice to do this
  238. let stepNumber = 0
  239. const ipv4AddOnUrl = {
  240. text: 'IPv4 add-on',
  241. url: `/project/${projectRef}/settings/addons?panel=ipv4`,
  242. }
  243. const ipv4SettingsUrl = {
  244. text: 'IPv4 settings',
  245. url: `/project/${projectRef}/settings/addons?panel=ipv4`,
  246. }
  247. const poolerSettingsUrl = {
  248. text: 'Pooler settings',
  249. url: `/project/${projectRef}/database/settings#connection-pooling`,
  250. }
  251. const buttonLinks = !ipv4Addon
  252. ? [ipv4AddOnUrl, ...(sharedPoolerPreferred ? [poolerSettingsUrl] : [])]
  253. : [ipv4SettingsUrl, ...(sharedPoolerPreferred ? [poolerSettingsUrl] : [])]
  254. const poolerBadge = sharedPoolerPreferred ? 'Shared Pooler' : 'Dedicated Pooler'
  255. return (
  256. <div className="flex flex-col">
  257. <div className={cn('w-full flex flex-col items-start gap-2 lg:gap-3', DIALOG_PADDING_X)}>
  258. <div className="flex w-full flex-col md:flex-row items-stretch md:items-center gap-2 md:gap-3">
  259. <div className="flex">
  260. <span className="w-1/2 md:w-auto flex items-center text-foreground-lighter px-3 rounded-lg rounded-r-none text-xs border border-button border-r-0">
  261. Type
  262. </span>
  263. <Select value={selectedTab} onValueChange={handleTabChange}>
  264. <SelectTrigger size="small" className="w-full md:w-auto rounded-l-none">
  265. <SelectValue />
  266. </SelectTrigger>
  267. <SelectContent>
  268. {DATABASE_CONNECTION_TYPES.map((type) => (
  269. <SelectItem key={type.id} value={type.id}>
  270. {type.label}
  271. </SelectItem>
  272. ))}
  273. </SelectContent>
  274. </Select>
  275. </div>
  276. <DatabaseSelector
  277. align="start"
  278. buttonProps={{
  279. size: 'small',
  280. className: 'w-full pr-2.5 [&_svg]:h-4',
  281. }}
  282. className="w-full md:w-auto [&>span]:w-1/2 [&>span]:md:w-auto"
  283. onSelectId={handleDatabaseChange}
  284. />
  285. <div className="flex">
  286. <span className="w-1/2 md:w-auto flex items-center text-foreground-lighter px-3 rounded-lg rounded-r-none text-xs border border-button border-r-0">
  287. Method
  288. </span>
  289. <Select value={selectedMethod} onValueChange={handleMethodChange}>
  290. <SelectTrigger size="small" className="w-full md:w-auto rounded-l-none">
  291. <SelectValue size="tiny">
  292. {connectionStringMethodOptions[selectedMethod].label}
  293. </SelectValue>
  294. </SelectTrigger>
  295. <SelectContent className="max-w-sm">
  296. {Object.keys(connectionStringMethodOptions).map((method) => (
  297. <ConnectionStringMethodSelectItem
  298. key={method}
  299. method={method as ConnectionStringMethod}
  300. poolerBadge={method === 'transaction' ? poolerBadge : undefined}
  301. />
  302. ))}
  303. </SelectContent>
  304. </Select>
  305. </div>
  306. </div>
  307. <p className="text-xs inline-flex items-center gap-1 text-foreground-lighter">
  308. <BookOpen size={12} strokeWidth={1.5} className="-mb-px" /> Learn how to connect to your
  309. Postgres databases.
  310. <InlineLink
  311. title="Read docs"
  312. className="flex items-center gap-x-1"
  313. href={`${DOCS_URL}/guides/database/connecting-to-postgres`}
  314. >
  315. Read docs <ExternalLink size={12} strokeWidth={1.5} />
  316. </InlineLink>
  317. </p>
  318. </div>
  319. {isLoading && (
  320. <div className="p-7">
  321. <ShimmeringLoader className="h-8 w-full" />
  322. </div>
  323. )}
  324. {isError && (
  325. <div className="p-7">
  326. <AlertError error={error} subject="Failed to retrieve database settings" />
  327. </div>
  328. )}
  329. {isSuccess && (
  330. <div className="flex flex-col divide-y divide-border">
  331. {/* // handle non terminal examples */}
  332. {hasCodeExamples && (
  333. <div className="flex flex-col w-full">
  334. <div className="grid lg:grid-cols-3 gap-4 lg:gap-5 py-8 px-4 md:px-7">
  335. <StepLabel number={++stepNumber} className="items-start">
  336. Install the following
  337. </StepLabel>
  338. {exampleInstallCommands?.map((cmd) => (
  339. <CodeBlock
  340. key={`example-install-command-${cmd}`}
  341. className="[&_code]:text-[12px] [&_code]:text-foreground"
  342. wrapperClassName="lg:col-span-2"
  343. value={cmd}
  344. hideLineNumbers
  345. language="bash"
  346. >
  347. {cmd}
  348. </CodeBlock>
  349. ))}
  350. </div>
  351. {exampleFiles && exampleFiles?.length > 0 && (
  352. <div className="grid lg:grid-cols-3 gap-4 lg:gap-5 border-t py-8 px-4 md:px-7">
  353. <StepLabel number={++stepNumber} className="items-start">
  354. Add file to project
  355. </StepLabel>
  356. {exampleFiles?.map((file) => (
  357. <div key={`example-files-${file.name}`} className="lg:col-span-2">
  358. <CodeBlockFileHeader title={file.name} />
  359. <CodeBlock
  360. wrapperClassName="[&_pre]:max-h-40 [&_pre]:px-4 [&_pre]:py-3 [&_pre]:rounded-t-none"
  361. value={file.content}
  362. hideLineNumbers
  363. language={lang}
  364. className="[&_code]:text-[12px] [&_code]:text-foreground"
  365. />
  366. </div>
  367. ))}
  368. </div>
  369. )}
  370. </div>
  371. )}
  372. <div>
  373. {hasCodeExamples && (
  374. <div className="px-4 md:px-7 pt-8">
  375. <StepLabel number={++stepNumber}>Connect to your database</StepLabel>
  376. </div>
  377. )}
  378. <div className="px-4 md:px-7 py-8">
  379. {selectedMethod === 'direct' && (
  380. <ConnectionPanel
  381. type="direct"
  382. title={connectionStringMethodOptions.direct.label}
  383. contentType={contentType}
  384. lang={lang}
  385. fileTitle={fileTitle}
  386. description={connectionStringMethodOptions.direct.description}
  387. connectionString={connectionStrings['direct'][selectedTab]}
  388. ipv4Status={{
  389. type: !ipv4Addon ? 'error' : 'success',
  390. title: !ipv4Addon ? 'Not IPv4 compatible' : 'IPv4 compatible',
  391. description:
  392. !sharedPoolerPreferred && !ipv4Addon
  393. ? PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT
  394. : sharedPoolerPreferred
  395. ? 'Use Session Pooler if on a IPv4 network or purchase IPv4 add-on'
  396. : IPV4_ADDON_TEXT,
  397. links: buttonLinks,
  398. }}
  399. parameters={[
  400. { ...CONNECTION_PARAMETERS.host, value: connectionInfo.db_host },
  401. { ...CONNECTION_PARAMETERS.port, value: connectionInfo.db_port },
  402. { ...CONNECTION_PARAMETERS.database, value: connectionInfo.db_name },
  403. { ...CONNECTION_PARAMETERS.user, value: connectionInfo.db_user },
  404. ]}
  405. onCopyCallback={() => handleCopy(selectedTab, 'direct')}
  406. />
  407. )}
  408. {selectedMethod === 'transaction' && IS_PLATFORM && (
  409. <ConnectionPanel
  410. type="transaction"
  411. title={connectionStringMethodOptions.transaction.label}
  412. contentType={contentType}
  413. lang={lang}
  414. badge={poolerBadge}
  415. fileTitle={fileTitle}
  416. description={connectionStringMethodOptions.transaction.description}
  417. connectionString={connectionStrings['pooler'][selectedTab]}
  418. ipv4Status={{
  419. type: !sharedPoolerPreferred && !ipv4Addon ? 'error' : 'success',
  420. title:
  421. !sharedPoolerPreferred && !ipv4Addon
  422. ? 'Not IPv4 compatible'
  423. : 'IPv4 compatible',
  424. description:
  425. !sharedPoolerPreferred && !ipv4Addon
  426. ? PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT
  427. : sharedPoolerPreferred
  428. ? 'Transaction pooler connections are IPv4 proxied for free.'
  429. : IPV4_ADDON_TEXT,
  430. links: !sharedPoolerPreferred ? buttonLinks : undefined,
  431. }}
  432. notice={['Does not support PREPARE statements']}
  433. parameters={[
  434. {
  435. ...CONNECTION_PARAMETERS.host,
  436. value: isReplicaSelected
  437. ? connectionInfo.db_host
  438. : (poolingConfiguration?.db_host ?? ''),
  439. },
  440. {
  441. ...CONNECTION_PARAMETERS.port,
  442. value: poolingConfiguration?.db_port.toString() ?? '6543',
  443. },
  444. {
  445. ...CONNECTION_PARAMETERS.database,
  446. value: poolingConfiguration?.db_name ?? '',
  447. },
  448. { ...CONNECTION_PARAMETERS.user, value: poolingConfiguration?.db_user ?? '' },
  449. { ...CONNECTION_PARAMETERS.pool_mode, value: 'transaction' },
  450. ]}
  451. onCopyCallback={() => handleCopy(selectedTab, 'transaction_pooler')}
  452. >
  453. {!sharedPoolerPreferred && !ipv4Addon && (
  454. <>
  455. <Separator className="w-full" />
  456. <Collapsible className="group">
  457. <CollapsibleTrigger
  458. asChild
  459. className="w-full justify-start !last:rounded-b group-data-open:rounded-b-none px-3"
  460. >
  461. <Button
  462. type="default"
  463. size="large"
  464. iconRight={
  465. <ChevronDown className="transition group-data-open:rotate-180" />
  466. }
  467. className="text-foreground bg-dash-sidebar! justify-between"
  468. >
  469. <div className="text-xs flex items-center gap-x-2 py-2 px-1">
  470. <span>Using the Shared Pooler</span>
  471. <Badge variant="success">IPv4 compatible</Badge>
  472. </div>
  473. </Button>
  474. </CollapsibleTrigger>
  475. <CollapsibleContent className="bg-dash-sidebar rounded-b border text-xs">
  476. <CodeBlock
  477. wrapperClassName={cn(
  478. '[&_pre]:border-x-0 [&_pre]:border-t-0 [&_pre]:px-4 [&_pre]:py-3',
  479. '[&_pre]:rounded-t-none'
  480. )}
  481. language={lang}
  482. value={supavisorConnectionStrings['pooler'][selectedTab]}
  483. className="[&_code]:text-[12px] [&_code]:text-foreground"
  484. hideLineNumbers
  485. onCopyCallback={() => handleCopy(selectedTab, 'transaction_pooler')}
  486. />
  487. <p className="px-3 py-2 text-foreground-light">
  488. Only recommended when your network does not support IPv6. Added latency
  489. compared to dedicated pooler.
  490. </p>
  491. </CollapsibleContent>
  492. </Collapsible>
  493. </>
  494. )}
  495. </ConnectionPanel>
  496. )}
  497. {selectedMethod === 'session' && IS_PLATFORM && (
  498. <ConnectionPanel
  499. type="session"
  500. title={connectionStringMethodOptions.session.label}
  501. contentType={contentType}
  502. lang={lang}
  503. badge="Shared Pooler"
  504. fileTitle={fileTitle}
  505. description={connectionStringMethodOptions.session.description}
  506. connectionString={supavisorConnectionStrings['pooler'][selectedTab].replace(
  507. '6543',
  508. '5432'
  509. )}
  510. ipv4Status={{
  511. type: 'success',
  512. title: 'IPv4 compatible',
  513. description: 'Session pooler connections are IPv4 proxied for free',
  514. links: undefined,
  515. }}
  516. parameters={[
  517. { ...CONNECTION_PARAMETERS.host, value: sharedPoolerConfig?.db_host ?? '' },
  518. { ...CONNECTION_PARAMETERS.port, value: '5432' },
  519. {
  520. ...CONNECTION_PARAMETERS.database,
  521. value: sharedPoolerConfig?.db_name ?? '',
  522. },
  523. { ...CONNECTION_PARAMETERS.user, value: sharedPoolerConfig?.db_user ?? '' },
  524. { ...CONNECTION_PARAMETERS.pool_mode, value: 'session' },
  525. ]}
  526. onCopyCallback={() => handleCopy(selectedTab, 'session_pooler')}
  527. />
  528. )}
  529. </div>
  530. </div>
  531. {examplePostInstallCommands && (
  532. <div className="grid lg:grid-cols-3 gap-4 lg:gap-5 w-full px-4 md:px-7 py-8">
  533. <StepLabel number={++stepNumber} className="items-start">
  534. Add the configuration package to read the settings
  535. </StepLabel>
  536. {examplePostInstallCommands?.map((cmd) => (
  537. <CodeBlock
  538. key={`example-post-install-commands-${cmd}`}
  539. className="text-sm"
  540. wrapperClassName="lg:col-span-2"
  541. value={cmd}
  542. hideLineNumbers
  543. language="bash"
  544. >
  545. {cmd}
  546. </CodeBlock>
  547. ))}
  548. </div>
  549. )}
  550. </div>
  551. )}
  552. {selectedTab === 'python' && (
  553. <>
  554. <Separator />
  555. <Collapsible className="px-8 py-5">
  556. <CollapsibleTrigger className="group [&[data-state=open]>div>svg]:-rotate-180!">
  557. <div className="flex items-center gap-x-2 w-full">
  558. <p className="text-xs text-foreground-light group-hover:text-foreground transition">
  559. Connecting to SQL Alchemy
  560. </p>
  561. <ChevronDown
  562. className="transition-transform duration-200"
  563. strokeWidth={1.5}
  564. size={14}
  565. />
  566. </div>
  567. </CollapsibleTrigger>
  568. <CollapsibleContent className="my-2">
  569. <div className="text-foreground-light text-xs grid gap-2">
  570. <p>
  571. Please use <code>postgresql://</code> instead of <code>postgres://</code> as your
  572. dialect when connecting via SQLAlchemy.
  573. </p>
  574. <p>
  575. Example:
  576. <code>create_engine("postgresql+psycopg2://...")</code>
  577. </p>
  578. <p className="text-sm font-mono tracking-tight text-foreground-lighter"></p>
  579. </div>
  580. </CollapsibleContent>
  581. </Collapsible>
  582. </>
  583. )}
  584. <Separator />
  585. <div className="px-8 pt-5 flex flex-col gap-y-1">
  586. <p className="text-sm">Reset your database password</p>
  587. <p className="text-sm text-foreground-lighter">
  588. You may reset your database password in your project's{' '}
  589. <InlineLink
  590. href={`/project/${projectRef}/database/settings`}
  591. className="text-foreground-lighter hover:text-foreground"
  592. >
  593. Database Settings
  594. </InlineLink>
  595. </p>
  596. </div>
  597. </div>
  598. )
  599. }
  600. const ConnectionStringMethodSelectItem = ({
  601. method,
  602. poolerBadge,
  603. }: {
  604. method: ConnectionStringMethod
  605. poolerBadge?: string
  606. }) => {
  607. const badges: ReactNode[] = []
  608. if (method !== 'direct') {
  609. badges.push(
  610. <Badge key="direct" className="flex gap-x-1">
  611. Shared Pooler
  612. </Badge>
  613. )
  614. }
  615. if (poolerBadge === 'Dedicated Pooler') {
  616. badges.push(
  617. <Badge key="dedicated" className="flex gap-x-1">
  618. {poolerBadge}
  619. </Badge>
  620. )
  621. }
  622. return (
  623. <SelectItem value={method} className="[&>span:first-child]:top-3.5">
  624. <div className="flex flex-col w-full py-1">
  625. <div className="flex gap-x-2 items-center">
  626. {connectionStringMethodOptions[method].label}
  627. </div>
  628. <div className="text-foreground-lighter text-xs">
  629. {connectionStringMethodOptions[method].description}
  630. </div>
  631. <div className="flex items-center gap-0.5 flex-wrap mt-1.5">
  632. {badges.map((badge) => badge)}
  633. </div>
  634. </div>
  635. </SelectItem>
  636. )
  637. }