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