mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
## Context Related to dashboard scalability Previous PR: https://github.com/supabase/supabase/pull/42856 Note: Changes are all feature flagged still and I'm still not entirely convinced with the current UX Will iterate as as go along, and only make this publicly available when we're satisfied with the behaviour Adds a "Dashboard preference" section to the project settings <img width="265" height="740" alt="image" src="https://github.com/user-attachments/assets/6ce1aa19-26c2-47c6-a9c4-595137266631" /> In which users can then select which database they'd like to use for read queries run from the dashboard Note: Everything is local storage for now, but we'd need middleware support if we want to make this setting persist for all users on the project <img width="791" height="434" alt="image" src="https://github.com/user-attachments/assets/e651d6d9-fed4-4da4-b552-c9f93f8d46d3" /> Added a dialog as well to further explain what this implies <img width="610" height="312" alt="image" src="https://github.com/user-attachments/assets/0aa957af-cb51-476f-aa79-8948a7cbe5ae" /> ## To test - Choosing a replica in dashboard preferences will only affect the table editor as thats the only place that's set up so far to use a replica for read queries (I'll need to follow up for other parts of the dashboard in subsequent PRs)
679 lines
28 KiB
TypeScript
679 lines
28 KiB
TypeScript
import { useParams } from 'common'
|
|
import { getAddons } from 'components/interfaces/Billing/Subscription/Subscription.utils'
|
|
import AlertError from 'components/ui/AlertError'
|
|
import { DatabaseSelector } from 'components/ui/DatabaseSelector'
|
|
import { InlineLink } from 'components/ui/InlineLink'
|
|
import { usePgbouncerConfigQuery } from 'data/database/pgbouncer-config-query'
|
|
import { useSupavisorConfigurationQuery } from 'data/database/supavisor-configuration-query'
|
|
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
|
|
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
|
|
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
|
|
import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
|
|
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
|
|
import { DOCS_URL, IS_PLATFORM } from 'lib/constants'
|
|
import { pluckObjectFields } from 'lib/helpers'
|
|
import { BookOpen, ChevronDown, ExternalLink } from 'lucide-react'
|
|
import { parseAsString, useQueryState } from 'nuqs'
|
|
import { HTMLAttributes, ReactNode, useEffect, useState } from 'react'
|
|
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
|
import {
|
|
Badge,
|
|
Button,
|
|
cn,
|
|
CodeBlock,
|
|
Collapsible_Shadcn_,
|
|
CollapsibleContent_Shadcn_,
|
|
CollapsibleTrigger_Shadcn_,
|
|
DIALOG_PADDING_X,
|
|
Select_Shadcn_,
|
|
SelectContent_Shadcn_,
|
|
SelectItem_Shadcn_,
|
|
SelectTrigger_Shadcn_,
|
|
SelectValue_Shadcn_,
|
|
Separator,
|
|
} from 'ui'
|
|
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
|
|
|
import {
|
|
CONNECTION_PARAMETERS,
|
|
connectionStringMethodOptions,
|
|
DATABASE_CONNECTION_TYPES,
|
|
DatabaseConnectionType,
|
|
IPV4_ADDON_TEXT,
|
|
PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT,
|
|
type ConnectionStringMethod,
|
|
} from './Connect.constants'
|
|
import { CodeBlockFileHeader, ConnectionPanel } from './ConnectionPanel'
|
|
import { getConnectionStrings } from './DatabaseSettings.utils'
|
|
import { examples, type Example } from './DirectConnectionExamples'
|
|
|
|
const StepLabel = ({
|
|
number,
|
|
children,
|
|
...props
|
|
}: { number: number; children: ReactNode } & HTMLAttributes<HTMLDivElement>) => (
|
|
<div {...props} className={cn('flex items-center gap-2', props.className)}>
|
|
<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">
|
|
{number}
|
|
</div>
|
|
<span>{children}</span>
|
|
</div>
|
|
)
|
|
|
|
/**
|
|
* [Joshen] For paid projects - Dedicated pooler is always in transaction mode
|
|
* So session mode connection details are always using the shared pooler (Supavisor)
|
|
*/
|
|
export const DatabaseConnectionString = () => {
|
|
const { ref: projectRef } = useParams()
|
|
const { data: org } = useSelectedOrganizationQuery()
|
|
const state = useDatabaseSelectorStateSnapshot()
|
|
const {
|
|
hasAccess: hasDedicatedPooler,
|
|
isLoading: isLoadingEntitlement,
|
|
isSuccess: isSuccessEntitlement,
|
|
} = useCheckEntitlements('dedicated_pooler')
|
|
const sharedPoolerPreferred = !hasDedicatedPooler
|
|
|
|
// URL state management
|
|
const [queryType, setQueryType] = useQueryState('type', parseAsString.withDefault('uri'))
|
|
const [querySource, setQuerySource] = useQueryState('source', parseAsString)
|
|
const [queryMethod, setQueryMethod] = useQueryState('method', parseAsString.withDefault('direct'))
|
|
|
|
const [selectedTab, setSelectedTab] = useState<DatabaseConnectionType>('uri')
|
|
const [selectedMethod, setSelectedMethod] = useState<ConnectionStringMethod>('direct')
|
|
|
|
// Sync URL state with component state on mount and when URL changes
|
|
useEffect(() => {
|
|
const validTypes = DATABASE_CONNECTION_TYPES.map((t) => t.id)
|
|
if (queryType && validTypes.includes(queryType as DatabaseConnectionType)) {
|
|
setSelectedTab(queryType as DatabaseConnectionType)
|
|
} else if (queryType && !validTypes.includes(queryType as DatabaseConnectionType)) {
|
|
setQueryType('uri')
|
|
setSelectedTab('uri')
|
|
}
|
|
|
|
const validMethods: ConnectionStringMethod[] = ['direct', 'transaction', 'session']
|
|
if (queryMethod && validMethods.includes(queryMethod as ConnectionStringMethod)) {
|
|
setSelectedMethod(queryMethod as ConnectionStringMethod)
|
|
} else if (queryMethod && !validMethods.includes(queryMethod as ConnectionStringMethod)) {
|
|
setQueryMethod('direct')
|
|
setSelectedMethod('direct')
|
|
}
|
|
|
|
if (querySource && querySource !== state.selectedDatabaseId) {
|
|
state.setSelectedDatabaseId(querySource)
|
|
} else if (!querySource && state.selectedDatabaseId !== projectRef) {
|
|
state.setSelectedDatabaseId(projectRef)
|
|
}
|
|
}, [queryType, queryMethod, querySource, state])
|
|
|
|
// Sync component state changes back to URL
|
|
const handleTabChange = (connectionType: DatabaseConnectionType) => {
|
|
setSelectedTab(connectionType)
|
|
setQueryType(connectionType)
|
|
}
|
|
|
|
const handleMethodChange = (method: ConnectionStringMethod) => {
|
|
setSelectedMethod(method)
|
|
setQueryMethod(method)
|
|
}
|
|
|
|
const handleDatabaseChange = (databaseId: string) => {
|
|
if (databaseId === projectRef) {
|
|
setQuerySource(null)
|
|
} else {
|
|
setQuerySource(databaseId)
|
|
}
|
|
}
|
|
|
|
// Sync database selector state changes back to URL
|
|
useEffect(() => {
|
|
if (state.selectedDatabaseId && state.selectedDatabaseId !== querySource) {
|
|
// Only set source in URL if it's not the primary database
|
|
if (state.selectedDatabaseId === projectRef) {
|
|
setQuerySource(null)
|
|
} else {
|
|
setQuerySource(state.selectedDatabaseId)
|
|
}
|
|
}
|
|
}, [state.selectedDatabaseId, querySource, projectRef])
|
|
|
|
const {
|
|
data: pgbouncerConfig,
|
|
error: pgbouncerError,
|
|
isPending: isLoadingPgbouncerConfig,
|
|
isError: isErrorPgbouncerConfig,
|
|
isSuccess: isSuccessPgBouncerConfig,
|
|
} = usePgbouncerConfigQuery({ projectRef })
|
|
const {
|
|
data: supavisorConfig,
|
|
error: supavisorConfigError,
|
|
isPending: isLoadingSupavisorConfig,
|
|
isError: isErrorSupavisorConfig,
|
|
isSuccess: isSuccessSupavisorConfig,
|
|
} = useSupavisorConfigurationQuery({ projectRef })
|
|
|
|
const {
|
|
data: databases,
|
|
error: readReplicasError,
|
|
isPending: isLoadingReadReplicas,
|
|
isError: isErrorReadReplicas,
|
|
isSuccess: isSuccessReadReplicas,
|
|
} = useReadReplicasQuery({ projectRef })
|
|
|
|
const poolerError = sharedPoolerPreferred ? pgbouncerError : supavisorConfigError
|
|
const isLoadingPoolerConfig = !IS_PLATFORM
|
|
? false
|
|
: sharedPoolerPreferred
|
|
? isLoadingPgbouncerConfig
|
|
: isLoadingSupavisorConfig
|
|
const isErrorPoolerConfig = !IS_PLATFORM
|
|
? undefined
|
|
: sharedPoolerPreferred
|
|
? isErrorPgbouncerConfig
|
|
: isErrorSupavisorConfig
|
|
const isSuccessPoolerConfig = !IS_PLATFORM
|
|
? true
|
|
: sharedPoolerPreferred
|
|
? isSuccessPgBouncerConfig
|
|
: isSuccessSupavisorConfig
|
|
|
|
const error = poolerError || readReplicasError
|
|
const isLoading = isLoadingPoolerConfig || isLoadingReadReplicas || isLoadingEntitlement
|
|
const isError = isErrorPoolerConfig || isErrorReadReplicas
|
|
const isSuccess = isSuccessPoolerConfig && isSuccessReadReplicas && isSuccessEntitlement
|
|
|
|
const sharedPoolerConfig = supavisorConfig?.find((x) => x.identifier === state.selectedDatabaseId)
|
|
const poolingConfiguration = sharedPoolerPreferred ? sharedPoolerConfig : pgbouncerConfig
|
|
|
|
const selectedDatabase = (databases ?? []).find(
|
|
(db) => db.identifier === state.selectedDatabaseId
|
|
)
|
|
const isReplicaSelected = selectedDatabase?.identifier !== projectRef
|
|
|
|
const { data: addons } = useProjectAddonsQuery({ projectRef })
|
|
const { ipv4: ipv4Addon } = getAddons(addons?.selected_addons ?? [])
|
|
|
|
const { mutate: sendEvent } = useSendEventMutation()
|
|
|
|
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(selectedDatabase || emptyState, DB_FIELDS)
|
|
|
|
const handleCopy = (
|
|
connectionTypeId: string,
|
|
connectionStringMethod: 'direct' | 'transaction_pooler' | 'session_pooler'
|
|
) => {
|
|
const connectionInfo = DATABASE_CONNECTION_TYPES.find((type) => type.id === connectionTypeId)
|
|
const connectionType = connectionInfo?.label ?? 'Unknown'
|
|
const lang = connectionInfo?.lang ?? 'Unknown'
|
|
sendEvent({
|
|
action: 'connection_string_copied',
|
|
properties: {
|
|
connectionType,
|
|
lang,
|
|
connectionMethod: connectionStringMethod,
|
|
connectionTab: 'Connection String',
|
|
},
|
|
groups: { project: projectRef ?? 'Unknown', organization: org?.slug ?? 'Unknown' },
|
|
})
|
|
}
|
|
|
|
const supavisorConnectionStrings = getConnectionStrings({
|
|
connectionInfo,
|
|
poolingInfo: {
|
|
connectionString: sharedPoolerConfig?.connection_string ?? '',
|
|
db_host: isReplicaSelected ? connectionInfo.db_host : sharedPoolerConfig?.db_host ?? '',
|
|
db_name: sharedPoolerConfig?.db_name ?? '',
|
|
db_port: sharedPoolerConfig?.db_port ?? 0,
|
|
db_user: sharedPoolerConfig?.db_user ?? '',
|
|
},
|
|
metadata: { projectRef },
|
|
})
|
|
|
|
const connectionStrings = getConnectionStrings({
|
|
connectionInfo,
|
|
poolingInfo: {
|
|
connectionString: isReplicaSelected
|
|
? poolingConfiguration?.connection_string.replace(
|
|
poolingConfiguration?.db_host,
|
|
connectionInfo.db_host
|
|
) ?? ''
|
|
: poolingConfiguration?.connection_string ?? '',
|
|
db_host: isReplicaSelected ? connectionInfo.db_host : poolingConfiguration?.db_host,
|
|
db_name: poolingConfiguration?.db_name ?? '',
|
|
db_port: poolingConfiguration?.db_port ?? 0,
|
|
db_user: poolingConfiguration?.db_user ?? '',
|
|
},
|
|
metadata: { projectRef },
|
|
})
|
|
|
|
const lang = DATABASE_CONNECTION_TYPES.find((type) => type.id === selectedTab)?.lang ?? 'bash'
|
|
const contentType =
|
|
DATABASE_CONNECTION_TYPES.find((type) => type.id === selectedTab)?.contentType ?? 'input'
|
|
|
|
const example: Example | undefined = examples[selectedTab as keyof typeof examples]
|
|
|
|
const exampleFiles = example?.files
|
|
const exampleInstallCommands = example?.installCommands
|
|
const examplePostInstallCommands = example?.postInstallCommands
|
|
const hasCodeExamples = exampleFiles || exampleInstallCommands
|
|
const fileTitle = DATABASE_CONNECTION_TYPES.find((type) => type.id === selectedTab)?.fileTitle
|
|
|
|
// [Refactor] See if we can do this in an immutable way, technically not a good practice to do this
|
|
let stepNumber = 0
|
|
|
|
const ipv4AddOnUrl = {
|
|
text: 'IPv4 add-on',
|
|
url: `/project/${projectRef}/settings/addons?panel=ipv4`,
|
|
}
|
|
const ipv4SettingsUrl = {
|
|
text: 'IPv4 settings',
|
|
url: `/project/${projectRef}/settings/addons?panel=ipv4`,
|
|
}
|
|
const poolerSettingsUrl = {
|
|
text: 'Pooler settings',
|
|
url: `/project/${projectRef}/database/settings#connection-pooling`,
|
|
}
|
|
const buttonLinks = !ipv4Addon
|
|
? [ipv4AddOnUrl, ...(sharedPoolerPreferred ? [poolerSettingsUrl] : [])]
|
|
: [ipv4SettingsUrl, ...(sharedPoolerPreferred ? [poolerSettingsUrl] : [])]
|
|
const poolerBadge = sharedPoolerPreferred ? 'Shared Pooler' : 'Dedicated Pooler'
|
|
|
|
return (
|
|
<div className="flex flex-col">
|
|
<div className={cn('w-full flex flex-col items-start gap-2 lg:gap-3', DIALOG_PADDING_X)}>
|
|
<div className="flex w-full flex-col md:flex-row items-stretch md:items-center gap-2 md:gap-3">
|
|
<div className="flex">
|
|
<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">
|
|
Type
|
|
</span>
|
|
<Select_Shadcn_ value={selectedTab} onValueChange={handleTabChange}>
|
|
<SelectTrigger_Shadcn_ size="small" className="w-full md:w-auto rounded-l-none">
|
|
<SelectValue_Shadcn_ />
|
|
</SelectTrigger_Shadcn_>
|
|
<SelectContent_Shadcn_>
|
|
{DATABASE_CONNECTION_TYPES.map((type) => (
|
|
<SelectItem_Shadcn_ key={type.id} value={type.id}>
|
|
{type.label}
|
|
</SelectItem_Shadcn_>
|
|
))}
|
|
</SelectContent_Shadcn_>
|
|
</Select_Shadcn_>
|
|
</div>
|
|
<DatabaseSelector
|
|
align="start"
|
|
buttonProps={{
|
|
size: 'small',
|
|
className: 'w-full pr-2.5 [&_svg]:h-4',
|
|
}}
|
|
className="w-full md:w-auto [&>span]:w-1/2 [&>span]:md:w-auto"
|
|
onSelectId={handleDatabaseChange}
|
|
/>
|
|
<div className="flex">
|
|
<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">
|
|
Method
|
|
</span>
|
|
<Select_Shadcn_ value={selectedMethod} onValueChange={handleMethodChange}>
|
|
<SelectTrigger_Shadcn_ size="small" className="w-full md:w-auto rounded-l-none">
|
|
<SelectValue_Shadcn_ size="tiny">
|
|
{connectionStringMethodOptions[selectedMethod].label}
|
|
</SelectValue_Shadcn_>
|
|
</SelectTrigger_Shadcn_>
|
|
<SelectContent_Shadcn_ className="max-w-sm">
|
|
{Object.keys(connectionStringMethodOptions).map((method) => (
|
|
<ConnectionStringMethodSelectItem
|
|
key={method}
|
|
method={method as ConnectionStringMethod}
|
|
poolerBadge={method === 'transaction' ? poolerBadge : undefined}
|
|
/>
|
|
))}
|
|
</SelectContent_Shadcn_>
|
|
</Select_Shadcn_>
|
|
</div>
|
|
</div>
|
|
<p className="text-xs inline-flex items-center gap-1 text-foreground-lighter">
|
|
<BookOpen size={12} strokeWidth={1.5} className="-mb-px" /> Learn how to connect to your
|
|
Postgres databases.
|
|
<InlineLink
|
|
title="Read docs"
|
|
className="flex items-center gap-x-1"
|
|
href={`${DOCS_URL}/guides/database/connecting-to-postgres`}
|
|
>
|
|
Read docs <ExternalLink size={12} strokeWidth={1.5} />
|
|
</InlineLink>
|
|
</p>
|
|
</div>
|
|
|
|
{isLoading && (
|
|
<div className="p-7">
|
|
<ShimmeringLoader className="h-8 w-full" />
|
|
</div>
|
|
)}
|
|
|
|
{isError && (
|
|
<div className="p-7">
|
|
<AlertError error={error} subject="Failed to retrieve database settings" />
|
|
</div>
|
|
)}
|
|
|
|
{isSuccess && (
|
|
<div className="flex flex-col divide-y divide-border">
|
|
{/* // handle non terminal examples */}
|
|
{hasCodeExamples && (
|
|
<div className="flex flex-col w-full">
|
|
<div className="grid lg:grid-cols-3 gap-4 lg:gap-5 py-8 px-4 md:px-7">
|
|
<StepLabel number={++stepNumber} className="items-start">
|
|
Install the following
|
|
</StepLabel>
|
|
{exampleInstallCommands?.map((cmd) => (
|
|
<CodeBlock
|
|
key={`example-install-command-${cmd}`}
|
|
className="[&_code]:text-[12px] [&_code]:text-foreground"
|
|
wrapperClassName="lg:col-span-2"
|
|
value={cmd}
|
|
hideLineNumbers
|
|
language="bash"
|
|
>
|
|
{cmd}
|
|
</CodeBlock>
|
|
))}
|
|
</div>
|
|
{exampleFiles && exampleFiles?.length > 0 && (
|
|
<div className="grid lg:grid-cols-3 gap-4 lg:gap-5 border-t py-8 px-4 md:px-7">
|
|
<StepLabel number={++stepNumber} className="items-start">
|
|
Add file to project
|
|
</StepLabel>
|
|
{exampleFiles?.map((file) => (
|
|
<div key={`example-files-${file.name}`} className="lg:col-span-2">
|
|
<CodeBlockFileHeader title={file.name} />
|
|
<CodeBlock
|
|
wrapperClassName="[&_pre]:max-h-40 [&_pre]:px-4 [&_pre]:py-3 [&_pre]:rounded-t-none"
|
|
value={file.content}
|
|
hideLineNumbers
|
|
language={lang}
|
|
className="[&_code]:text-[12px] [&_code]:text-foreground"
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
{hasCodeExamples && (
|
|
<div className="px-4 md:px-7 pt-8">
|
|
<StepLabel number={++stepNumber}>Connect to your database</StepLabel>
|
|
</div>
|
|
)}
|
|
<div className="px-4 md:px-7 py-8">
|
|
{selectedMethod === 'direct' && (
|
|
<ConnectionPanel
|
|
type="direct"
|
|
title={connectionStringMethodOptions.direct.label}
|
|
contentType={contentType}
|
|
lang={lang}
|
|
fileTitle={fileTitle}
|
|
description={connectionStringMethodOptions.direct.description}
|
|
connectionString={connectionStrings['direct'][selectedTab]}
|
|
ipv4Status={{
|
|
type: !ipv4Addon ? 'error' : 'success',
|
|
title: !ipv4Addon ? 'Not IPv4 compatible' : 'IPv4 compatible',
|
|
description:
|
|
!sharedPoolerPreferred && !ipv4Addon
|
|
? PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT
|
|
: sharedPoolerPreferred
|
|
? 'Use Session Pooler if on a IPv4 network or purchase IPv4 add-on'
|
|
: IPV4_ADDON_TEXT,
|
|
links: buttonLinks,
|
|
}}
|
|
parameters={[
|
|
{ ...CONNECTION_PARAMETERS.host, value: connectionInfo.db_host },
|
|
{ ...CONNECTION_PARAMETERS.port, value: connectionInfo.db_port },
|
|
{ ...CONNECTION_PARAMETERS.database, value: connectionInfo.db_name },
|
|
{ ...CONNECTION_PARAMETERS.user, value: connectionInfo.db_user },
|
|
]}
|
|
onCopyCallback={() => handleCopy(selectedTab, 'direct')}
|
|
/>
|
|
)}
|
|
|
|
{selectedMethod === 'transaction' && IS_PLATFORM && (
|
|
<ConnectionPanel
|
|
type="transaction"
|
|
title={connectionStringMethodOptions.transaction.label}
|
|
contentType={contentType}
|
|
lang={lang}
|
|
badge={poolerBadge}
|
|
fileTitle={fileTitle}
|
|
description={connectionStringMethodOptions.transaction.description}
|
|
connectionString={connectionStrings['pooler'][selectedTab]}
|
|
ipv4Status={{
|
|
type: !sharedPoolerPreferred && !ipv4Addon ? 'error' : 'success',
|
|
title:
|
|
!sharedPoolerPreferred && !ipv4Addon
|
|
? 'Not IPv4 compatible'
|
|
: 'IPv4 compatible',
|
|
description:
|
|
!sharedPoolerPreferred && !ipv4Addon
|
|
? PGBOUNCER_ENABLED_BUT_NO_IPV4_ADDON_TEXT
|
|
: sharedPoolerPreferred
|
|
? 'Transaction pooler connections are IPv4 proxied for free.'
|
|
: IPV4_ADDON_TEXT,
|
|
links: !sharedPoolerPreferred ? buttonLinks : undefined,
|
|
}}
|
|
notice={['Does not support PREPARE statements']}
|
|
parameters={[
|
|
{
|
|
...CONNECTION_PARAMETERS.host,
|
|
value: isReplicaSelected
|
|
? connectionInfo.db_host
|
|
: poolingConfiguration?.db_host ?? '',
|
|
},
|
|
{
|
|
...CONNECTION_PARAMETERS.port,
|
|
value: poolingConfiguration?.db_port.toString() ?? '6543',
|
|
},
|
|
{
|
|
...CONNECTION_PARAMETERS.database,
|
|
value: poolingConfiguration?.db_name ?? '',
|
|
},
|
|
{ ...CONNECTION_PARAMETERS.user, value: poolingConfiguration?.db_user ?? '' },
|
|
{ ...CONNECTION_PARAMETERS.pool_mode, value: 'transaction' },
|
|
]}
|
|
onCopyCallback={() => handleCopy(selectedTab, 'transaction_pooler')}
|
|
>
|
|
{!sharedPoolerPreferred && !ipv4Addon && (
|
|
<>
|
|
<Separator className="w-full" />
|
|
<Collapsible_Shadcn_ className="group">
|
|
<CollapsibleTrigger_Shadcn_
|
|
asChild
|
|
className="w-full justify-start !last:rounded-b group-data-[state=open]:rounded-b-none border-light px-3"
|
|
>
|
|
<Button
|
|
type="default"
|
|
size="large"
|
|
iconRight={
|
|
<ChevronDown className="transition group-data-[state=open]:rotate-180" />
|
|
}
|
|
className="text-foreground !bg-dash-sidebar justify-between"
|
|
>
|
|
<div className="text-xs flex items-center gap-x-2 py-2 px-1">
|
|
<span>Using the Shared Pooler</span>
|
|
<Badge variant="success">IPv4 compatible</Badge>
|
|
</div>
|
|
</Button>
|
|
</CollapsibleTrigger_Shadcn_>
|
|
<CollapsibleContent_Shadcn_ className="bg-dash-sidebar rounded-b border text-xs">
|
|
<CodeBlock
|
|
wrapperClassName={cn(
|
|
'[&_pre]:border-x-0 [&_pre]:border-t-0 [&_pre]:px-4 [&_pre]:py-3',
|
|
'[&_pre]:rounded-t-none'
|
|
)}
|
|
language={lang}
|
|
value={supavisorConnectionStrings['pooler'][selectedTab]}
|
|
className="[&_code]:text-[12px] [&_code]:text-foreground"
|
|
hideLineNumbers
|
|
onCopyCallback={() => handleCopy(selectedTab, 'transaction_pooler')}
|
|
/>
|
|
<p className="px-3 py-2 text-foreground-light">
|
|
Only recommended when your network does not support IPv6. Added latency
|
|
compared to dedicated pooler.
|
|
</p>
|
|
</CollapsibleContent_Shadcn_>
|
|
</Collapsible_Shadcn_>
|
|
</>
|
|
)}
|
|
</ConnectionPanel>
|
|
)}
|
|
|
|
{selectedMethod === 'session' && IS_PLATFORM && (
|
|
<ConnectionPanel
|
|
type="session"
|
|
title={connectionStringMethodOptions.session.label}
|
|
contentType={contentType}
|
|
lang={lang}
|
|
badge="Shared Pooler"
|
|
fileTitle={fileTitle}
|
|
description={connectionStringMethodOptions.session.description}
|
|
connectionString={supavisorConnectionStrings['pooler'][selectedTab].replace(
|
|
'6543',
|
|
'5432'
|
|
)}
|
|
ipv4Status={{
|
|
type: 'success',
|
|
title: 'IPv4 compatible',
|
|
description: 'Session pooler connections are IPv4 proxied for free',
|
|
links: undefined,
|
|
}}
|
|
parameters={[
|
|
{ ...CONNECTION_PARAMETERS.host, value: sharedPoolerConfig?.db_host ?? '' },
|
|
{ ...CONNECTION_PARAMETERS.port, value: '5432' },
|
|
{
|
|
...CONNECTION_PARAMETERS.database,
|
|
value: sharedPoolerConfig?.db_name ?? '',
|
|
},
|
|
{ ...CONNECTION_PARAMETERS.user, value: sharedPoolerConfig?.db_user ?? '' },
|
|
{ ...CONNECTION_PARAMETERS.pool_mode, value: 'session' },
|
|
]}
|
|
onCopyCallback={() => handleCopy(selectedTab, 'session_pooler')}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{examplePostInstallCommands && (
|
|
<div className="grid lg:grid-cols-3 gap-4 lg:gap-5 w-full px-4 md:px-7 py-8">
|
|
<StepLabel number={++stepNumber} className="items-start">
|
|
Add the configuration package to read the settings
|
|
</StepLabel>
|
|
{examplePostInstallCommands?.map((cmd) => (
|
|
<CodeBlock
|
|
key={`example-post-install-commands-${cmd}`}
|
|
className="text-sm"
|
|
wrapperClassName="lg:col-span-2"
|
|
value={cmd}
|
|
hideLineNumbers
|
|
language="bash"
|
|
>
|
|
{cmd}
|
|
</CodeBlock>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{selectedTab === 'python' && (
|
|
<>
|
|
<Separator />
|
|
<Collapsible_Shadcn_ className="px-8 py-5">
|
|
<CollapsibleTrigger_Shadcn_ className="group [&[data-state=open]>div>svg]:!-rotate-180">
|
|
<div className="flex items-center gap-x-2 w-full">
|
|
<p className="text-xs text-foreground-light group-hover:text-foreground transition">
|
|
Connecting to SQL Alchemy
|
|
</p>
|
|
<ChevronDown
|
|
className="transition-transform duration-200"
|
|
strokeWidth={1.5}
|
|
size={14}
|
|
/>
|
|
</div>
|
|
</CollapsibleTrigger_Shadcn_>
|
|
<CollapsibleContent_Shadcn_ className="my-2">
|
|
<div className="text-foreground-light text-xs grid gap-2">
|
|
<p>
|
|
Please use <code>postgresql://</code> instead of <code>postgres://</code> as your
|
|
dialect when connecting via SQLAlchemy.
|
|
</p>
|
|
<p>
|
|
Example:
|
|
<code>create_engine("postgresql+psycopg2://...")</code>
|
|
</p>
|
|
<p className="text-sm font-mono tracking-tight text-foreground-lighter"></p>
|
|
</div>
|
|
</CollapsibleContent_Shadcn_>
|
|
</Collapsible_Shadcn_>
|
|
</>
|
|
)}
|
|
|
|
<Separator />
|
|
<div className="px-8 pt-5 flex flex-col gap-y-1">
|
|
<p className="text-sm">Reset your database password</p>
|
|
<p className="text-sm text-foreground-lighter">
|
|
You may reset your database password in your project's{' '}
|
|
<InlineLink
|
|
href={`/project/${projectRef}/database/settings`}
|
|
className="text-foreground-lighter hover:text-foreground"
|
|
>
|
|
Database Settings
|
|
</InlineLink>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const ConnectionStringMethodSelectItem = ({
|
|
method,
|
|
poolerBadge,
|
|
}: {
|
|
method: ConnectionStringMethod
|
|
poolerBadge?: string
|
|
}) => {
|
|
const badges: ReactNode[] = []
|
|
|
|
if (method !== 'direct') {
|
|
badges.push(
|
|
<Badge key="direct" className="flex gap-x-1">
|
|
Shared Pooler
|
|
</Badge>
|
|
)
|
|
}
|
|
if (poolerBadge === 'Dedicated Pooler') {
|
|
badges.push(
|
|
<Badge key="dedicated" className="flex gap-x-1">
|
|
{poolerBadge}
|
|
</Badge>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<SelectItem_Shadcn_ value={method} className="[&>span:first-child]:top-3.5">
|
|
<div className="flex flex-col w-full py-1">
|
|
<div className="flex gap-x-2 items-center">
|
|
{connectionStringMethodOptions[method].label}
|
|
</div>
|
|
<div className="text-foreground-lighter text-xs">
|
|
{connectionStringMethodOptions[method].description}
|
|
</div>
|
|
<div className="flex items-center gap-0.5 flex-wrap mt-1.5">
|
|
{badges.map((badge) => badge)}
|
|
</div>
|
|
</div>
|
|
</SelectItem_Shadcn_>
|
|
)
|
|
}
|