mirror of
https://github.com/supabase/supabase.git
synced 2026-10-17 07:09:07 +03:00
add new page
This commit is contained in:
1 parent
637167a27b
commit
32802ef461
9 files changed
+1635
-33
No files matched your search
@@ -0,0 +1,82 @@
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
|
||||
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui'
|
||||
import { WarningIcon } from 'ui'
|
||||
|
||||
export const IPv4DeprecationNotice = () => {
|
||||
return (
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<WarningIcon />
|
||||
<AlertTitle_Shadcn_>
|
||||
Direct database access is only available via IPv6-compatible networks.
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_ className="space-y-3">
|
||||
<p>
|
||||
We strongly recommend using connection pooling to connect to your database because it's
|
||||
compatible with both IPv4 and IPv6 networks. You'll only need to change the connection
|
||||
credentials that you're using in your application to the pooler's connection credentials.
|
||||
</p>
|
||||
<Button asChild type="default" icon={<ExternalLink />}>
|
||||
<a
|
||||
href="https://github.com/orgs/supabase/discussions/17817"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</Button>
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
)
|
||||
}
|
||||
|
||||
export const IPv4AddonDirectConnectionNotice = () => {
|
||||
return (
|
||||
<Alert_Shadcn_ variant="default">
|
||||
<AlertTitle_Shadcn_>
|
||||
Direct database connections are recommended if you're connecting with session mode.
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_ className="space-y-3">
|
||||
<p>
|
||||
If you are not connecting to the database from serverless environments, we strongly
|
||||
recommend you connect to the database directly instead of using connection pooling for
|
||||
lower latency.
|
||||
</p>
|
||||
<p>
|
||||
Since your project has the IPv4 add on, you are not required to use the connection pooler
|
||||
even if your network only supports IPv4.
|
||||
</p>
|
||||
<Button asChild type="default" icon={<ExternalLink />}>
|
||||
<a
|
||||
href="https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pooler"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</Button>
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
)
|
||||
}
|
||||
|
||||
export const DefaultSessionModeNotice = () => {
|
||||
return (
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<WarningIcon />
|
||||
<AlertTitle_Shadcn_>You are not yet in Transaction mode.</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_ className="space-y-3">
|
||||
<p>
|
||||
You need to set your <span className="text-foreground">Pool Mode</span> to{' '}
|
||||
<span className="text-foreground">Transaction</span> in the{' '}
|
||||
<span className="text-foreground">Connection pooling configuration</span> section below in
|
||||
order to use transaction mode on port 6543.
|
||||
</p>
|
||||
<p>
|
||||
Otherwise, port 6543 will continue to connect in{' '}
|
||||
<span className="text-foreground">Session</span> mode.
|
||||
</p>
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
import { useParams } from 'common'
|
||||
import { getAddons } from 'components/interfaces/Billing/Subscription/Subscription.utils'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import DatabaseSelector from 'components/ui/DatabaseSelector'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { usePoolingConfigurationQuery } from 'data/database/pooling-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 { useSelectedProject } from 'hooks/misc/useSelectedProject'
|
||||
import { pluckObjectFields } from 'lib/helpers'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
||||
import { useDatabaseSettingsStateSnapshot } from 'state/database-settings'
|
||||
import {
|
||||
CollapsibleContent_Shadcn_,
|
||||
CollapsibleTrigger_Shadcn_,
|
||||
Collapsible_Shadcn_,
|
||||
DIALOG_PADDING_X,
|
||||
SelectContent_Shadcn_,
|
||||
SelectItem_Shadcn_,
|
||||
SelectTrigger_Shadcn_,
|
||||
SelectValue_Shadcn_,
|
||||
Select_Shadcn_,
|
||||
Separator,
|
||||
TooltipContent_Shadcn_,
|
||||
TooltipTrigger_Shadcn_,
|
||||
Tooltip_Shadcn_,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import { Input } from 'ui-patterns/DataInputs/Input'
|
||||
import {
|
||||
DefaultSessionModeNotice,
|
||||
IPv4AddonDirectConnectionNotice,
|
||||
IPv4DeprecationNotice,
|
||||
} from './DatabaseConnectionNotices'
|
||||
import DatabaseSettings from './DatabaseSettings'
|
||||
import {
|
||||
constructConnStringSyntax,
|
||||
getConnectionStrings,
|
||||
getPoolerTld,
|
||||
} from './DatabaseSettings.utils'
|
||||
import { UsePoolerCheckbox } from './UsePoolerCheckbox'
|
||||
import { SessionIcon, TransactionIcon } from './pooler-icons'
|
||||
|
||||
const CONNECTION_TYPES = [
|
||||
{ id: 'uri', label: 'URI' },
|
||||
{ id: 'psql', label: 'PSQL' },
|
||||
{ id: 'golang', label: 'Golang' },
|
||||
{ id: 'jdbc', label: 'JDBC' },
|
||||
{ id: 'dotnet', label: '.NET' },
|
||||
{ id: 'nodejs', label: 'Node.js' },
|
||||
{ id: 'php', label: 'PHP' },
|
||||
{ id: 'python', label: 'Python' },
|
||||
]
|
||||
|
||||
type ConnectionType = 'uri' | 'psql' | 'golang' | 'jdbc' | 'dotnet' | 'nodejs' | 'php' | 'python'
|
||||
|
||||
export const DatabaseConnectionString = () => {
|
||||
const project = useSelectedProject()
|
||||
const { ref: projectRef, connectionString } = useParams()
|
||||
const snap = useDatabaseSettingsStateSnapshot()
|
||||
const state = useDatabaseSelectorStateSnapshot()
|
||||
const { project: projectDetails, isLoading: isProjectLoading } = useProjectContext()
|
||||
|
||||
const [poolingMode, setPoolingMode] = useState<'transaction' | 'session'>('transaction')
|
||||
const [selectedTab, setSelectedTab] = useState<ConnectionType>('uri')
|
||||
|
||||
const { data: poolingInfo, isSuccess: isSuccessPoolingInfo } = usePoolingConfigurationQuery({
|
||||
projectRef,
|
||||
})
|
||||
const poolingConfiguration = poolingInfo?.find((x) => x.identifier === state.selectedDatabaseId)
|
||||
const defaultPoolingMode = poolingConfiguration?.pool_mode
|
||||
|
||||
const {
|
||||
data: databases,
|
||||
error: readReplicasError,
|
||||
isLoading: isLoadingReadReplicas,
|
||||
isError: isErrorReadReplicas,
|
||||
isSuccess: isSuccessReadReplicas,
|
||||
} = useReadReplicasQuery({ projectRef })
|
||||
|
||||
const selectedDatabase = (databases ?? []).find(
|
||||
(db) => db.identifier === state.selectedDatabaseId
|
||||
)
|
||||
|
||||
const { data: addons, isSuccess: isSuccessAddons } = 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 connectionTld =
|
||||
projectDetails?.restUrl !== undefined
|
||||
? new URL(projectDetails?.restUrl ?? '').hostname.split('.').pop() ?? 'co'
|
||||
: 'co'
|
||||
|
||||
const handleCopy = (id: string) => {
|
||||
const labelValue = CONNECTION_TYPES.find((type) => type.id === id)?.label
|
||||
sendEvent({
|
||||
category: 'settings',
|
||||
action: 'copy_connection_string',
|
||||
label: labelValue ? labelValue : '',
|
||||
})
|
||||
}
|
||||
|
||||
const connectionStrings =
|
||||
isSuccessPoolingInfo && poolingConfiguration !== undefined
|
||||
? getConnectionStrings(connectionInfo, poolingConfiguration, {
|
||||
projectRef,
|
||||
// usePoolerConnection: snap.usePoolerConnection,
|
||||
})
|
||||
: {
|
||||
direct: {
|
||||
uri: '',
|
||||
psql: '',
|
||||
golang: '',
|
||||
jdbc: '',
|
||||
dotnet: '',
|
||||
nodejs: '',
|
||||
php: '',
|
||||
python: '',
|
||||
},
|
||||
pooler: {
|
||||
uri: '',
|
||||
psql: '',
|
||||
golang: '',
|
||||
jdbc: '',
|
||||
dotnet: '',
|
||||
nodejs: '',
|
||||
php: '',
|
||||
python: '',
|
||||
},
|
||||
}
|
||||
|
||||
const poolerTld =
|
||||
isSuccessPoolingInfo && poolingConfiguration !== undefined
|
||||
? getPoolerTld(poolingConfiguration?.connectionString)
|
||||
: 'com'
|
||||
const poolerConnStringSyntax =
|
||||
isSuccessPoolingInfo && poolingConfiguration !== undefined
|
||||
? constructConnStringSyntax(poolingConfiguration?.connectionString, {
|
||||
selectedTab,
|
||||
usePoolerConnection: snap.usePoolerConnection,
|
||||
ref: projectRef as string,
|
||||
cloudProvider: isProjectLoading ? '' : project?.cloud_provider || '',
|
||||
region: isProjectLoading ? '' : project?.region || '',
|
||||
tld: snap.usePoolerConnection ? poolerTld : connectionTld,
|
||||
portNumber: snap.usePoolerConnection
|
||||
? poolingMode === 'transaction'
|
||||
? poolingConfiguration?.db_port.toString()
|
||||
: '5432'
|
||||
: connectionInfo.db_port.toString(),
|
||||
})
|
||||
: []
|
||||
|
||||
useEffect(() => {
|
||||
if (poolingConfiguration?.pool_mode === 'session') {
|
||||
setPoolingMode(poolingConfiguration.pool_mode)
|
||||
}
|
||||
}, [poolingConfiguration?.pool_mode])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="m-7 p-3 border rounded flex items-center gap-10">
|
||||
<TransactionIcon />
|
||||
<SessionIcon />
|
||||
</div>
|
||||
<div className={cn('flex items-center gap-2', DIALOG_PADDING_X)}>
|
||||
<div className="flex">
|
||||
<span className="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={(connectionType: ConnectionType) => setSelectedTab(connectionType)}
|
||||
>
|
||||
<SelectTrigger_Shadcn_ size="small" className="w-auto rounded-l-none">
|
||||
<SelectValue_Shadcn_ className="[&_svg]:pl-2" />
|
||||
</SelectTrigger_Shadcn_>
|
||||
<SelectContent_Shadcn_>
|
||||
{CONNECTION_TYPES.map((type) => (
|
||||
<SelectItem_Shadcn_ key={type.id} value={type.id}>
|
||||
{type.label}
|
||||
</SelectItem_Shadcn_>
|
||||
))}
|
||||
</SelectContent_Shadcn_>
|
||||
</Select_Shadcn_>
|
||||
</div>
|
||||
<DatabaseSelector />
|
||||
{/* <DocsButton href="https://supabase.com/docs/guides/database/connecting-to-postgres" /> */}
|
||||
</div>
|
||||
<div className={cn(DIALOG_PADDING_X)}>
|
||||
{isLoadingReadReplicas && <ShimmeringLoader className="h-8 w-full" />}
|
||||
{isErrorReadReplicas && (
|
||||
<AlertError error={readReplicasError} subject="Failed to retrieve database settings" />
|
||||
)}
|
||||
{isSuccessReadReplicas && (
|
||||
<>
|
||||
<div className="flex flex-col gap-y-4 pt-2">
|
||||
<h1>Direct Connection</h1>
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
className="input-mono [&>div>div>div>input]:text-xs [&>div>div>div>input]:opacity-100"
|
||||
value={connectionStrings['direct'][selectedTab]}
|
||||
onCopy={() => handleCopy(selectedTab)}
|
||||
/>
|
||||
</div>
|
||||
<DatabaseSettings />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Separator />
|
||||
<div className={cn(DIALOG_PADDING_X)}>
|
||||
{isLoadingReadReplicas && <ShimmeringLoader className="h-8 w-full" />}
|
||||
{isErrorReadReplicas && (
|
||||
<AlertError error={readReplicasError} subject="Failed to retrieve database settings" />
|
||||
)}
|
||||
{isSuccessReadReplicas && (
|
||||
<>
|
||||
<div className="flex flex-row gap-x-4">
|
||||
<div className="flex flex-col gap-y-4 pt-2">
|
||||
<h1>Transaction Pooler</h1>
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
className="input-mono [&>div>div>div>input]:text-xs [&>div>div>div>input]:opacity-100"
|
||||
value={connectionStrings['pooler'][selectedTab]}
|
||||
onCopy={() => handleCopy(selectedTab)}
|
||||
/>
|
||||
<DatabaseSettings />
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-4 pt-2">
|
||||
<h1>Session Pooler</h1>
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
className="input-mono [&>div>div>div>input]:text-xs [&>div>div>div>input]:opacity-100"
|
||||
value={connectionStrings['pooler'][selectedTab].replace('6543', '5432')}
|
||||
onCopy={() => handleCopy(selectedTab)}
|
||||
/>
|
||||
<DatabaseSettings />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{poolerConnStringSyntax.length > 0 && (
|
||||
<>
|
||||
<Separator />
|
||||
<Panel.Content className={cn('!py-3 space-y-2')}>
|
||||
<Collapsible_Shadcn_>
|
||||
<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">
|
||||
How to connect to a different database or switch to another user
|
||||
</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">
|
||||
<p className="text-xs">
|
||||
You can use the following URI format to switch to a different database or user
|
||||
{snap.usePoolerConnection ? ' when using connection pooling' : ''}.
|
||||
</p>
|
||||
<p className="text-sm tracking-tight text-foreground-lighter">
|
||||
{poolerConnStringSyntax.map((x, idx) => {
|
||||
if (x.tooltip) {
|
||||
return (
|
||||
<Tooltip_Shadcn_ key={`syntax-${idx}`}>
|
||||
<TooltipTrigger_Shadcn_ asChild>
|
||||
<span className="text-foreground text-xs font-mono">{x.value}</span>
|
||||
</TooltipTrigger_Shadcn_>
|
||||
<TooltipContent_Shadcn_ side="bottom">
|
||||
{x.tooltip}
|
||||
</TooltipContent_Shadcn_>
|
||||
</Tooltip_Shadcn_>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<span key={`syntax-${idx}`} className="text-xs font-mono">
|
||||
{x.value}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</CollapsibleContent_Shadcn_>
|
||||
</Collapsible_Shadcn_>
|
||||
|
||||
{selectedTab === 'python' && (
|
||||
<Collapsible_Shadcn_>
|
||||
<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_>
|
||||
)}
|
||||
</Panel.Content>
|
||||
<Separator />
|
||||
<div className={cn(DIALOG_PADDING_X)}>
|
||||
<DatabaseSettings />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
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 Panel from 'components/ui/Panel'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { usePoolingConfigurationQuery } from 'data/database/pooling-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 { pluckObjectFields } from 'lib/helpers'
|
||||
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
||||
import { useDatabaseSettingsStateSnapshot } from 'state/database-settings'
|
||||
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Input, WarningIcon } from 'ui'
|
||||
import {
|
||||
DefaultSessionModeNotice,
|
||||
IPv4AddonDirectConnectionNotice,
|
||||
IPv4DeprecationNotice,
|
||||
} from './DatabaseConnectionNotices'
|
||||
import { UsePoolerCheckbox } from './UsePoolerCheckbox'
|
||||
|
||||
const DatabaseSettings = () => {
|
||||
const { ref: projectRef, connectionString } = useParams()
|
||||
const snap = useDatabaseSettingsStateSnapshot()
|
||||
const state = useDatabaseSelectorStateSnapshot()
|
||||
|
||||
const connectionStringsRef = useRef<HTMLDivElement>(null)
|
||||
const [poolingMode, setPoolingMode] = useState<'transaction' | 'session'>('transaction')
|
||||
|
||||
const {
|
||||
data: poolingInfo,
|
||||
error: poolingInfoError,
|
||||
isLoading: isLoadingPoolingInfo,
|
||||
isError: isErrorPoolingInfo,
|
||||
isSuccess: isSuccessPoolingInfo,
|
||||
} = usePoolingConfigurationQuery({
|
||||
projectRef,
|
||||
})
|
||||
const {
|
||||
data: databases,
|
||||
error: readReplicasError,
|
||||
isLoading: isLoadingReadReplicas,
|
||||
isError: isErrorReadReplicas,
|
||||
isSuccess: isSuccessReadReplicas,
|
||||
} = useReadReplicasQuery({ projectRef })
|
||||
const {
|
||||
data: addons,
|
||||
error: addOnsError,
|
||||
isLoading: isLoadingAddons,
|
||||
isError: isErrorAddons,
|
||||
isSuccess: isSuccessAddons,
|
||||
} = useProjectAddonsQuery({ projectRef })
|
||||
const { mutate: sendEvent } = useSendEventMutation()
|
||||
|
||||
const error = readReplicasError || poolingInfoError || addOnsError
|
||||
const isLoading = isLoadingReadReplicas || isLoadingPoolingInfo || isLoadingAddons
|
||||
const isError = isErrorReadReplicas || isErrorPoolingInfo || isErrorAddons
|
||||
const isSuccess = isSuccessReadReplicas && isSuccessPoolingInfo && isSuccessAddons
|
||||
|
||||
const selectedDatabase = (databases ?? []).find(
|
||||
(db) => db.identifier === state.selectedDatabaseId
|
||||
)
|
||||
const primaryConfig = poolingInfo?.find((x) => x.identifier === state.selectedDatabaseId)
|
||||
const defaultPoolingMode = primaryConfig?.pool_mode
|
||||
const { ipv4: ipv4Addon } = getAddons(addons?.selected_addons ?? [])
|
||||
const isMd5 = primaryConfig?.connectionString.includes('?options=reference')
|
||||
|
||||
const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user']
|
||||
const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' }
|
||||
const dbConnectionInfo = pluckObjectFields(selectedDatabase || emptyState, DB_FIELDS)
|
||||
|
||||
const connectionInfo = snap.usePoolerConnection
|
||||
? {
|
||||
db_host: primaryConfig?.db_host,
|
||||
db_name: primaryConfig?.db_name,
|
||||
db_port: primaryConfig?.db_port,
|
||||
db_user: primaryConfig?.db_user,
|
||||
}
|
||||
: dbConnectionInfo
|
||||
|
||||
const handleCopy = (labelValue?: string) =>
|
||||
sendEvent({
|
||||
category: 'settings',
|
||||
action: 'copy_connection_string',
|
||||
label: labelValue ? labelValue : '',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (connectionString !== undefined && connectionStringsRef.current !== undefined) {
|
||||
state.setSelectedDatabaseId(connectionString)
|
||||
connectionStringsRef.current?.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
||||
}
|
||||
}, [connectionString])
|
||||
|
||||
useEffect(() => {
|
||||
if (primaryConfig?.pool_mode === 'session') {
|
||||
setPoolingMode(primaryConfig.pool_mode)
|
||||
}
|
||||
}, [primaryConfig?.pool_mode])
|
||||
|
||||
if (isLoading) {
|
||||
return Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="grid gap-2 items-center md:grid md:grid-cols-12 md:gap-x-4 w-full">
|
||||
<ShimmeringLoader className="h-4 w-1/3 col-span-4" delayIndex={i} />
|
||||
<ShimmeringLoader className="h-8 w-full col-span-8" delayIndex={i} />
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <AlertError error={error} subject="Failed to retrieve databases" />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
{/* {ipv4Addon === undefined && !snap.usePoolerConnection && <IPv4DeprecationNotice />}
|
||||
{isMd5 && (
|
||||
<Alert_Shadcn_>
|
||||
<WarningIcon />
|
||||
<AlertTitle_Shadcn_>
|
||||
If you are connecting to your database via a GUI client, use the{' '}
|
||||
<span tabIndex={0}>connection string</span> above instead
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
GUI clients only support database connections for Postgres 13 via a connection string.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
)} */}
|
||||
</div>
|
||||
<div className="-space-y-px">
|
||||
<div className="text-xs font-mono space-y-1 bg-surface-100 border p-3 rounded-t">
|
||||
<div>
|
||||
<span className="text-foreground-light">Host:</span>{' '}
|
||||
<span>{connectionInfo.db_host}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-foreground-light">Database name:</span>{' '}
|
||||
<span>{connectionInfo.db_name}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-foreground-light">Port:</span>{' '}
|
||||
<span>{poolingMode === 'transaction' ? connectionInfo.db_port : '5432'}</span>
|
||||
</div>
|
||||
{isMd5 && snap.usePoolerConnection && (
|
||||
<div>
|
||||
<span className="text-foreground-light">Options:</span>{' '}
|
||||
<span>reference={projectRef}</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<span className="text-foreground-light">User:</span>{' '}
|
||||
<span>{connectionInfo.db_user}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-foreground-light">Password:</span>{' '}
|
||||
<span>
|
||||
{state.selectedDatabaseId !== projectRef
|
||||
? '[The password for your primary database]'
|
||||
: '[The password for your database]'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-foreground-muted p-3 py-1 bg-surface-75 border rounded-b text-xs">
|
||||
For security reasons, your database password is never shown and is only readable on
|
||||
creation/reset.
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DatabaseSettings
|
||||
@@ -0,0 +1,357 @@
|
||||
import type { PoolingConfiguration } from 'data/database/pooling-configuration-query'
|
||||
|
||||
type ConnectionStrings = {
|
||||
psql: string
|
||||
uri: string
|
||||
golang: string
|
||||
jdbc: string
|
||||
dotnet: string
|
||||
nodejs: string
|
||||
php: string
|
||||
python: string
|
||||
}
|
||||
|
||||
export const getConnectionStrings = (
|
||||
connectionInfo: {
|
||||
db_user: string
|
||||
db_port: number
|
||||
db_host: string
|
||||
db_name: string
|
||||
},
|
||||
poolingInfo: PoolingConfiguration,
|
||||
metadata: {
|
||||
projectRef?: string
|
||||
pgVersion?: string
|
||||
}
|
||||
): {
|
||||
direct: ConnectionStrings
|
||||
pooler: ConnectionStrings
|
||||
} => {
|
||||
const isMd5 = poolingInfo.connectionString.includes('options=reference')
|
||||
const { projectRef } = metadata
|
||||
const password = '[YOUR-PASSWORD]'
|
||||
|
||||
// Direct connection variables
|
||||
const directUser = connectionInfo.db_user
|
||||
const directPort = connectionInfo.db_port
|
||||
const directHost = connectionInfo.db_host
|
||||
const directName = connectionInfo.db_name
|
||||
|
||||
// Pooler connection variables
|
||||
const poolerUser = poolingInfo.db_user
|
||||
const poolerPort = poolingInfo.db_port
|
||||
const poolerHost = poolingInfo.db_host
|
||||
const poolerName = poolingInfo.db_name
|
||||
|
||||
// Direct connection strings
|
||||
const directPsqlString = isMd5
|
||||
? `psql "postgresql://${directUser}:${password}@${directHost}:${directPort}/${directName}"`
|
||||
: `psql -h ${directHost} -p ${directPort} -d ${directName} -U ${directUser}`
|
||||
|
||||
const directUriString = `postgresql://${directUser}:${password}@${directHost}:${directPort}/${directName}`
|
||||
|
||||
const directGolangString = `user=${directUser} password=${password} host=${directHost} port=${directPort} dbname=${directName}`
|
||||
|
||||
const directJdbcString = `jdbc:postgresql://${directHost}:${directPort}/${directName}?user=${directUser}&password=${password}`
|
||||
|
||||
const directDotNetString = `User Id=${directUser};Password=${password};Server=${directHost};Port=${directPort};Database=${directName}`
|
||||
|
||||
// Pooler connection strings
|
||||
const poolerPsqlString = isMd5
|
||||
? `psql "postgresql://${poolerUser}:${password}@${poolerHost}:${poolerPort}/${poolerName}?options=reference%3D${projectRef}"`
|
||||
: `psql -h ${poolerHost} -p ${poolerPort} -d ${poolerName} -U ${poolerUser}.${projectRef}`
|
||||
|
||||
const poolerUriString = poolingInfo.connectionString
|
||||
|
||||
const poolerGolangString = `user=${poolerUser} password=${password} host=${poolerHost} port=${poolerPort} dbname=${poolerName}${isMd5 ? ` options=reference=${projectRef}` : ''}`
|
||||
|
||||
const poolerJdbcString = `jdbc:postgresql://${poolerHost}:${poolerPort}/${poolerName}?user=${poolerUser}${isMd5 ? `&options=reference%3D${projectRef}` : ''}&password=${password}`
|
||||
|
||||
const poolerDotNetString = `User Id=${poolerUser};Password=${password};Server=${poolerHost};Port=${poolerPort};Database=${poolerName}${isMd5 ? `;Options='reference=${projectRef}'` : ''}`
|
||||
|
||||
return {
|
||||
direct: {
|
||||
psql: directPsqlString,
|
||||
uri: directUriString,
|
||||
golang: directGolangString,
|
||||
jdbc: directJdbcString,
|
||||
dotnet: directDotNetString,
|
||||
nodejs: directUriString,
|
||||
php: directGolangString,
|
||||
python: directGolangString,
|
||||
},
|
||||
pooler: {
|
||||
psql: poolerPsqlString,
|
||||
uri: poolerUriString,
|
||||
golang: poolerGolangString,
|
||||
jdbc: poolerJdbcString,
|
||||
dotnet: poolerDotNetString,
|
||||
nodejs: poolerUriString,
|
||||
php: poolerGolangString,
|
||||
python: poolerGolangString,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const DB_USER_DESC = 'Database user (e.g postgres)'
|
||||
const DB_PASS_DESC = 'Database password'
|
||||
const DB_NAME_DESC = 'Database name (e.g postgres)'
|
||||
const PROJECT_REF_DESC = "Project's reference ID"
|
||||
const PORT_NUMBER_DESC = 'Port number (Use 5432 if using prepared statements)'
|
||||
|
||||
// [Joshen] This is to the best of interpreting the syntax from the API response
|
||||
// // There's different format for PG13 (depending on authentication method being md5) and PG14
|
||||
export const constructConnStringSyntax = (
|
||||
connString: string,
|
||||
{
|
||||
selectedTab,
|
||||
usePoolerConnection,
|
||||
ref,
|
||||
cloudProvider,
|
||||
region,
|
||||
tld,
|
||||
portNumber,
|
||||
}: {
|
||||
selectedTab: 'uri' | 'psql' | 'golang' | 'jdbc' | 'dotnet' | 'nodejs' | 'php' | 'python'
|
||||
usePoolerConnection: boolean
|
||||
ref: string
|
||||
cloudProvider: string
|
||||
region: string
|
||||
tld: string
|
||||
portNumber: string
|
||||
}
|
||||
) => {
|
||||
const isMd5 = connString.includes('options=reference')
|
||||
const poolerHostDetails = [
|
||||
{ value: cloudProvider.toLocaleLowerCase(), tooltip: 'Cloud provider' },
|
||||
{ value: '-0-', tooltip: undefined },
|
||||
{ value: region, tooltip: "Project's region" },
|
||||
{ value: `.pooler.supabase.${tld}`, tooltip: undefined },
|
||||
]
|
||||
const dbHostDetails = [
|
||||
{ value: 'db.', tooltip: undefined },
|
||||
{ value: ref, tooltip: PROJECT_REF_DESC },
|
||||
{ value: `.supabase.${tld}`, tooltip: undefined },
|
||||
]
|
||||
|
||||
if (selectedTab === 'uri' || selectedTab === 'nodejs') {
|
||||
if (isMd5) {
|
||||
return [
|
||||
{ value: 'postgresql://', tooltip: undefined },
|
||||
{ value: '[user]', tooltip: DB_USER_DESC },
|
||||
{ value: ':', tooltip: undefined },
|
||||
{ value: '[password]', tooltip: DB_PASS_DESC },
|
||||
{ value: '@', tooltip: undefined },
|
||||
...(usePoolerConnection ? poolerHostDetails : dbHostDetails),
|
||||
{ value: ':', tooltip: undefined },
|
||||
{ value: portNumber, tooltip: PORT_NUMBER_DESC },
|
||||
{ value: '/', tooltip: undefined },
|
||||
{ value: '[db-name]', tooltip: DB_NAME_DESC },
|
||||
...(usePoolerConnection
|
||||
? [
|
||||
{ value: `?options=reference%3D`, tooltip: undefined },
|
||||
{ value: ref, tooltip: PROJECT_REF_DESC },
|
||||
]
|
||||
: []),
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
{ value: 'postgresql://', tooltip: undefined },
|
||||
{ value: '[user]', tooltip: DB_USER_DESC },
|
||||
...(usePoolerConnection
|
||||
? [
|
||||
{ value: '.', tooltip: undefined },
|
||||
{ value: ref, tooltip: PROJECT_REF_DESC },
|
||||
]
|
||||
: []),
|
||||
{ value: ':', tooltip: undefined },
|
||||
{ value: '[password]', tooltip: DB_PASS_DESC },
|
||||
{ value: '@', tooltip: undefined },
|
||||
...(usePoolerConnection ? poolerHostDetails : dbHostDetails),
|
||||
{ value: ':', tooltip: undefined },
|
||||
{ value: portNumber, tooltip: PORT_NUMBER_DESC },
|
||||
{ value: '/', tooltip: undefined },
|
||||
{ value: '[db-name]', tooltip: DB_NAME_DESC },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedTab === 'psql') {
|
||||
if (isMd5) {
|
||||
return [
|
||||
{ value: 'psql "postgresql://', tooltip: undefined },
|
||||
{ value: '[user]', tooltip: DB_USER_DESC },
|
||||
{ value: ':', tooltip: undefined },
|
||||
{ value: '[password]', tooltip: DB_PASS_DESC },
|
||||
{ value: '@', tooltip: undefined },
|
||||
...(usePoolerConnection ? poolerHostDetails : dbHostDetails),
|
||||
{ value: ':', tooltip: undefined },
|
||||
{ value: portNumber, tooltip: PORT_NUMBER_DESC },
|
||||
{ value: '/', tooltip: undefined },
|
||||
{ value: '[db-name]', tooltip: DB_NAME_DESC },
|
||||
...(usePoolerConnection
|
||||
? [
|
||||
{ value: '?options=reference%3D', tooltip: undefined },
|
||||
{ value: ref, tooltip: PROJECT_REF_DESC },
|
||||
]
|
||||
: []),
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
{ value: 'psql -h ', tooltip: undefined },
|
||||
...(usePoolerConnection ? poolerHostDetails : dbHostDetails),
|
||||
{ value: ' -p ', tooltip: undefined },
|
||||
{ value: portNumber, tooltip: PORT_NUMBER_DESC },
|
||||
{ value: ' -d ', tooltip: undefined },
|
||||
{ value: '[db-name]', tooltip: DB_NAME_DESC },
|
||||
{ value: ' -U ', tooltip: undefined },
|
||||
{ value: '[user]', tooltip: DB_USER_DESC },
|
||||
...(usePoolerConnection
|
||||
? [
|
||||
{ value: '.', tooltip: undefined },
|
||||
{ value: ref, tooltip: PROJECT_REF_DESC },
|
||||
]
|
||||
: []),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedTab === 'golang' || selectedTab === 'php' || selectedTab === 'python') {
|
||||
if (isMd5) {
|
||||
return [
|
||||
{ value: 'user=', tooltip: undefined },
|
||||
{ value: '[user]', tooltip: DB_USER_DESC },
|
||||
{ value: ' password=', tooltip: undefined },
|
||||
{ value: '[password]', tooltip: DB_PASS_DESC },
|
||||
{ value: ' host=', tooltip: undefined },
|
||||
...(usePoolerConnection ? poolerHostDetails : dbHostDetails),
|
||||
{ value: ' port=', tooltip: undefined },
|
||||
{ value: portNumber, tooltip: PORT_NUMBER_DESC },
|
||||
{ value: ' dbname=', tooltip: undefined },
|
||||
{ value: '[db-name]', tooltip: DB_NAME_DESC },
|
||||
...(usePoolerConnection
|
||||
? [
|
||||
{ value: ' options=reference=', tooltip: undefined },
|
||||
{ value: ref, tooltip: PROJECT_REF_DESC },
|
||||
]
|
||||
: []),
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
{ value: 'user=', tooltip: undefined },
|
||||
{ value: '[user]', tooltip: DB_USER_DESC },
|
||||
...(usePoolerConnection
|
||||
? [
|
||||
{ value: '.', tooltip: undefined },
|
||||
{ value: ref, tooltip: PROJECT_REF_DESC },
|
||||
]
|
||||
: []),
|
||||
{ value: ' password=', tooltip: undefined },
|
||||
{ value: '[password]', tooltip: DB_PASS_DESC },
|
||||
{ value: ' host=', tooltip: undefined },
|
||||
...(usePoolerConnection ? poolerHostDetails : dbHostDetails),
|
||||
{ value: ' port=', tooltip: undefined },
|
||||
{ value: portNumber, tooltip: PORT_NUMBER_DESC },
|
||||
{ value: ' dbname=', tooltip: undefined },
|
||||
{ value: '[db-name]', tooltip: DB_NAME_DESC },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedTab === 'jdbc') {
|
||||
if (isMd5) {
|
||||
return [
|
||||
{ value: 'jdbc:postgresql://', tooltip: undefined },
|
||||
...(usePoolerConnection ? poolerHostDetails : dbHostDetails),
|
||||
{ value: ':', tooltip: undefined },
|
||||
{ value: portNumber, tooltip: PORT_NUMBER_DESC },
|
||||
{ value: '/', tooltip: undefined },
|
||||
{ value: '[db-name]', tooltip: DB_NAME_DESC },
|
||||
{ value: '?user=', tooltip: undefined },
|
||||
{ value: '[user]', tooltip: DB_USER_DESC },
|
||||
{ value: '&password=', tooltip: undefined },
|
||||
{ value: '[password]', tooltip: DB_PASS_DESC },
|
||||
...(usePoolerConnection
|
||||
? [
|
||||
{ value: '&options=reference%3D', tooltip: undefined },
|
||||
{ value: ref, tooltip: PROJECT_REF_DESC },
|
||||
]
|
||||
: []),
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
{ value: 'jdbc:postgresql://', tooltip: undefined },
|
||||
...(usePoolerConnection ? poolerHostDetails : dbHostDetails),
|
||||
{ value: `:`, tooltip: undefined },
|
||||
{ value: portNumber, tooltip: PORT_NUMBER_DESC },
|
||||
{ value: '/', tooltip: undefined },
|
||||
{ value: '[db-name]', tooltip: DB_NAME_DESC },
|
||||
{ value: '?user=', tooltip: undefined },
|
||||
{ value: '[user]', tooltip: DB_USER_DESC },
|
||||
...(usePoolerConnection
|
||||
? [
|
||||
{ value: '.', tooltip: undefined },
|
||||
{ value: ref, tooltip: PROJECT_REF_DESC },
|
||||
]
|
||||
: []),
|
||||
{ value: '&password=', tooltip: undefined },
|
||||
{ value: '[password]', tooltip: DB_PASS_DESC },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedTab === 'dotnet') {
|
||||
if (isMd5) {
|
||||
return [
|
||||
{ value: 'User Id=', tooltip: undefined },
|
||||
{ value: '[user]', tooltip: DB_USER_DESC },
|
||||
{ value: ';Password=', tooltip: undefined },
|
||||
{ value: '[password]', tooltip: DB_PASS_DESC },
|
||||
{ value: ';Server=', tooltip: undefined },
|
||||
...(usePoolerConnection ? poolerHostDetails : dbHostDetails),
|
||||
{ value: ';Port=', tooltip: undefined },
|
||||
{ value: portNumber, tooltip: PORT_NUMBER_DESC },
|
||||
{ value: ';Database=', tooltip: undefined },
|
||||
{ value: '[db-name]', tooltip: DB_NAME_DESC },
|
||||
...(usePoolerConnection
|
||||
? [
|
||||
{ value: ";Options='reference=", tooltip: undefined },
|
||||
{ value: ref, tooltip: PROJECT_REF_DESC },
|
||||
{ value: "'", tooltip: undefined },
|
||||
]
|
||||
: []),
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
{ value: 'User Id=', tooltip: undefined },
|
||||
{ value: '[user]', tooltip: DB_USER_DESC },
|
||||
...(usePoolerConnection
|
||||
? [
|
||||
{ value: '.', tooltip: undefined },
|
||||
{ value: ref, tooltip: PROJECT_REF_DESC },
|
||||
]
|
||||
: []),
|
||||
{ value: ';Password=', tooltip: undefined },
|
||||
{ value: '[password]', tooltip: DB_PASS_DESC },
|
||||
{ value: ';Server=', tooltip: undefined },
|
||||
...(usePoolerConnection ? poolerHostDetails : dbHostDetails),
|
||||
{ value: ';Port=', tooltip: undefined },
|
||||
{ value: portNumber, tooltip: PORT_NUMBER_DESC },
|
||||
{ value: ';Database=', tooltip: undefined },
|
||||
{ value: '[db-name]', tooltip: DB_NAME_DESC },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export const getPoolerTld = (connString: string) => {
|
||||
try {
|
||||
const segment = connString.split('pooler.supabase.')[1]
|
||||
const tld = segment.split(':6543')[0]
|
||||
return tld
|
||||
} catch {
|
||||
return 'com'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { AlertTriangleIcon } from 'lucide-react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
import { DocsButton } from 'components/ui/DocsButton'
|
||||
import { usePoolingConfigurationQuery } from 'data/database/pooling-configuration-query'
|
||||
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
||||
import { useDatabaseSettingsStateSnapshot } from 'state/database-settings'
|
||||
import {
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Alert_Shadcn_,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogSection,
|
||||
DialogSectionSeparator,
|
||||
DialogTitle,
|
||||
} from 'ui'
|
||||
|
||||
export const PoolingModesDialog = () => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const snap = useDatabaseSettingsStateSnapshot()
|
||||
const state = useDatabaseSelectorStateSnapshot()
|
||||
|
||||
const { data } = usePoolingConfigurationQuery({ projectRef: projectRef })
|
||||
const primaryConfig = data?.find((x) => x.identifier === state.selectedDatabaseId)
|
||||
|
||||
const navigateToPoolerSettings = () => {
|
||||
const el = document.getElementById('connection-pooler')
|
||||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={snap.showPoolingModeHelper} onOpenChange={snap.setShowPoolingModeHelper}>
|
||||
<DialogContent hideClose className="sm:max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<p className="max-w-2xl">Which pooling mode should I use?</p>
|
||||
<DocsButton href="https://supabase.com/docs/guides/database/connecting-to-postgres#how-connection-pooling-works" />
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogDescription className="max-w-2xl">
|
||||
A connection pooler is a system (external to Postgres) which manages Postgres
|
||||
connections by allocating connections whenever clients make requests.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogSectionSeparator />
|
||||
<DialogSection>
|
||||
<Markdown
|
||||
className="max-w-full [&>h3]:text-sm"
|
||||
content={`
|
||||
Each pooling mode handles connections differently.
|
||||
|
||||
### Transaction mode
|
||||
This mode is recommended if you are connecting from *serverless environments*. A connection is assigned to the client for the duration of a transaction. Two consecutive transactions from the same client could be executed over two different connections. Some session-based Postgres features such as prepared statements are *not available* with this option.
|
||||
|
||||
### Session mode
|
||||
This mode is similar to connecting to your database directly. There is full support for prepared statements in this mode. When a new client connects, a connection is assigned to the client until it disconnects. You *might run into pooler connection limits* since the connection is held till the client disconnects.
|
||||
|
||||
### Using session and transaction modes at the same time
|
||||
${
|
||||
primaryConfig?.pool_mode === 'transaction'
|
||||
? 'You can use the session mode connection string (port 5432) and transaction mode connection string (port 6543) in your application.'
|
||||
: 'To get the best of both worlds, as a starting point, we recommend using session mode just when you need support for prepared statements and transaction mode in other cases.'
|
||||
}
|
||||
`}
|
||||
/>
|
||||
</DialogSection>
|
||||
{primaryConfig?.pool_mode === 'session' && (
|
||||
<div className="px-6">
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<AlertTriangleIcon strokeWidth={2} />
|
||||
<AlertTitle_Shadcn_>
|
||||
Pooling mode is currently configured to use session mode
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
To use transaction mode concurrently with session mode, change the pooling mode to
|
||||
transaction first in the{' '}
|
||||
<span
|
||||
tabIndex={0}
|
||||
className="text-foreground cursor-pointer underline underline-offset-2"
|
||||
onClick={() => {
|
||||
snap.setShowPoolingModeHelper(false)
|
||||
navigateToPoolerSettings()
|
||||
}}
|
||||
>
|
||||
connection pooling settings
|
||||
</span>
|
||||
. After this, you can use transaction mode on port 6543 and session mode on port
|
||||
5432.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<DialogClose onClick={() => snap.setShowPoolingModeHelper(false)}>
|
||||
<Button type="default">Close</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { usePgBouncerStatus } from 'data/config/pgbouncer-enabled-query'
|
||||
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
|
||||
import { usePoolingConfigurationQuery } from 'data/database/pooling-configuration-query'
|
||||
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
||||
import { useDatabaseSettingsStateSnapshot } from 'state/database-settings'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Checkbox_Shadcn_,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
Label_Shadcn_,
|
||||
Separator,
|
||||
} from 'ui'
|
||||
import {
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
} from 'ui/src/components/shadcn/ui/dropdown-menu'
|
||||
|
||||
interface UsePoolerCheckboxInterface {
|
||||
id: string
|
||||
checked: boolean
|
||||
poolingMode: 'transaction' | 'session'
|
||||
ipv4AddonAdded: boolean
|
||||
onCheckedChange: (value: boolean) => void
|
||||
onSelectPoolingMode: (mode: 'transaction' | 'session') => void
|
||||
}
|
||||
|
||||
export const UsePoolerCheckbox = ({
|
||||
id,
|
||||
checked,
|
||||
poolingMode = 'transaction',
|
||||
ipv4AddonAdded,
|
||||
onCheckedChange,
|
||||
onSelectPoolingMode,
|
||||
}: UsePoolerCheckboxInterface) => {
|
||||
const router = useRouter()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { ref: projectRef } = useParams()
|
||||
const snap = useDatabaseSettingsStateSnapshot()
|
||||
const state = useDatabaseSelectorStateSnapshot()
|
||||
|
||||
const { data, isLoading, isSuccess } = usePoolingConfigurationQuery({ projectRef })
|
||||
const { data: settings, isSuccess: isSuccessSettings } = useProjectSettingsV2Query({ projectRef })
|
||||
const { data: pgBouncerStatus } = usePgBouncerStatus({ projectRef: projectRef })
|
||||
|
||||
const isDatabaseSettingsPage = router.pathname.endsWith('/settings/database')
|
||||
const poolingConfiguration = data?.find((x) => x.identifier === state.selectedDatabaseId)
|
||||
const resolvesToIpV6 = settings?.db_ip_addr_config === 'ipv6'
|
||||
|
||||
const onSelectOption = (value: 'session' | 'transaction') => {
|
||||
onSelectPoolingMode(value)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<div className="flex gap-x-3 items-center">
|
||||
<Checkbox_Shadcn_
|
||||
id={`use-pooler-${id}`}
|
||||
checked={checked}
|
||||
onCheckedChange={() => onCheckedChange(!checked)}
|
||||
/>
|
||||
<div className="-mt-[2px] flex flex-col gap-y-1 w-full">
|
||||
<div className="flex items-center gap-x-4">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Label_Shadcn_ htmlFor={`use-pooler-${id}`} className="text-sm cursor-pointer">
|
||||
Display connection pooler
|
||||
</Label_Shadcn_>
|
||||
{checked && (
|
||||
<>
|
||||
<div className="flex items-center gap-x-2">
|
||||
{isLoading && <ShimmeringLoader className="w-[100px] py-2.5" />}
|
||||
{isSuccess && (
|
||||
<div className="flex items-center gap-x-1">
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="outline"
|
||||
className="py-0.5 pr-1.5 capitalize"
|
||||
iconRight={<ChevronDown strokeWidth={1} />}
|
||||
>
|
||||
Mode: {poolingMode}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="start" className="w-[280px]">
|
||||
<DropdownMenuRadioGroup
|
||||
value={poolingMode}
|
||||
onValueChange={(value) =>
|
||||
onSelectOption(value as 'session' | 'transaction')
|
||||
}
|
||||
>
|
||||
<DropdownMenuRadioItem value="session">
|
||||
Session mode
|
||||
</DropdownMenuRadioItem>
|
||||
{poolingConfiguration?.pool_mode === 'transaction' && (
|
||||
<DropdownMenuRadioItem value="transaction">
|
||||
Transaction mode
|
||||
</DropdownMenuRadioItem>
|
||||
)}
|
||||
</DropdownMenuRadioGroup>
|
||||
|
||||
{poolingConfiguration?.pool_mode === 'session' && (
|
||||
<>
|
||||
<DropdownMenuSeparator className="my-1" />
|
||||
<div className="px-2 text-xs flex flex-col gap-y-2">
|
||||
<p>
|
||||
To use transaction mode, change the pool mode in the{' '}
|
||||
{isDatabaseSettingsPage ? (
|
||||
<span
|
||||
tabIndex={0}
|
||||
className="underline hover:text-foreground cursor-pointer transition"
|
||||
onClick={() => {
|
||||
const el = document.getElementById('connection-pooler')
|
||||
if (el) {
|
||||
setOpen(false)
|
||||
el.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
pooling configuration settings
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
href={`/project/${projectRef}/settings/database#connection-pooler`}
|
||||
className="underline hover:text-foreground"
|
||||
>
|
||||
pooling configuration settings
|
||||
</Link>
|
||||
)}{' '}
|
||||
to use transaction mode first.
|
||||
</p>
|
||||
<p>
|
||||
You can then connect to session mode on port 5432 and
|
||||
transaction mode on port 6543.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Separator className="my-1" />
|
||||
<DropdownMenuItem
|
||||
key="more-info"
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
snap.setShowPoolingModeHelper(true)
|
||||
}}
|
||||
>
|
||||
How to choose pooling modes?
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-x-1">
|
||||
{isSuccess && checked && <Badge>Supavisor</Badge>}
|
||||
{isSuccessSettings && (
|
||||
<Badge>
|
||||
{checked || ipv4AddonAdded
|
||||
? 'Resolves to IPv4'
|
||||
: resolvesToIpV6
|
||||
? 'Resolves to IPv6'
|
||||
: 'Will resolve to IPv6'}
|
||||
</Badge>
|
||||
)}
|
||||
{pgBouncerStatus?.active && (
|
||||
<Badge variant="warning">PgBouncer pending removal</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { cn } from 'ui'
|
||||
|
||||
const FlowingLine = ({
|
||||
x,
|
||||
y1,
|
||||
y2,
|
||||
isActive,
|
||||
}: {
|
||||
x: number
|
||||
y1: number
|
||||
y2: number
|
||||
isActive: boolean
|
||||
}) => {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isActive && (
|
||||
<svg
|
||||
width="4"
|
||||
height={y2 - y1 + 6}
|
||||
viewBox={`0 0 4 ${y2 - y1 + 6}`}
|
||||
style={{ position: 'absolute', left: x - 2, top: y1 - 3 }}
|
||||
>
|
||||
<motion.line
|
||||
x1="2"
|
||||
y1="3"
|
||||
x2="2"
|
||||
y2={y2 - y1 + 3}
|
||||
stroke="hsl(var(--foreground-default) / 0.6)"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
initial={{ pathLength: 0, opacity: 0 }}
|
||||
animate={{ pathLength: 1, opacity: 1 }}
|
||||
exit={{ pathLength: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
<defs>
|
||||
<motion.linearGradient
|
||||
id={`lineGradient-${x}-${y1}-${y2}`}
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="2"
|
||||
y1="-20"
|
||||
x2="2"
|
||||
y2={y2 - y1 + 26}
|
||||
animate={{
|
||||
y1: [-20, y2 - y1 + 26],
|
||||
y2: [0, y2 - y1 + 46],
|
||||
}}
|
||||
transition={{
|
||||
duration: 1,
|
||||
repeat: Infinity,
|
||||
ease: 'linear',
|
||||
repeatDelay: 0,
|
||||
}}
|
||||
>
|
||||
<stop offset="0" stopColor="hsl(var(--foreground-default) / 0.6)" stopOpacity="0" />
|
||||
<stop offset="0.8" stopColor="hsl(var(--foreground-default))" />
|
||||
<stop offset="1" stopColor="hsl(var(--foreground-default) / 0.6)" stopOpacity="0" />
|
||||
</motion.linearGradient>
|
||||
</defs>
|
||||
<motion.line
|
||||
x1="2"
|
||||
y1="3"
|
||||
x2="2"
|
||||
y2={y2 - y1 + 3}
|
||||
stroke={`url(#lineGradient-${x}-${y1}-${y2})`}
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
const TopRect = ({ isActive }: { isActive: boolean }) => (
|
||||
<motion.rect
|
||||
className={cn(
|
||||
`fill-indigo-500 stroke-indigo-700 stroke-[1] w-[${RECT_WIDTH}px] h-[3px]`,
|
||||
isActive && 'stroke-indigo-900'
|
||||
)}
|
||||
x={RECT_X}
|
||||
y={TOP_RECT_Y}
|
||||
rx="1.5"
|
||||
animate={{
|
||||
boxShadow: isActive ? '0 0 8px #86efac' : '0 0 0px #86efac',
|
||||
}}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
)
|
||||
|
||||
const BottomRect = ({ isActive }: { isActive: boolean }) => (
|
||||
<motion.rect
|
||||
className={cn(
|
||||
`stroke-brand-500 fill-brand-300 stroke-[1] w-[${RECT_WIDTH}px] h-[8px] transition-all`,
|
||||
isActive && 'fill-brand-400 stroke-brand'
|
||||
)}
|
||||
x={RECT_X}
|
||||
y={BOTTOM_RECT_Y}
|
||||
rx="4"
|
||||
animate={{
|
||||
fill: isActive ? '#2563eb' : '#1e40af',
|
||||
stroke: '#93c5fd',
|
||||
boxShadow: isActive ? '0 0 8px #93c5fd' : '0 0 0px #93c5fd',
|
||||
}}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
)
|
||||
|
||||
// Common y-coordinates for both icons
|
||||
const TOP_LINE_START = 24 // Start below dots
|
||||
const TOP_LINE_END = 36 // End above top rect
|
||||
const BOTTOM_LINE_START = 50 // Start below top rect
|
||||
const BOTTOM_LINE_END = 56 // End above bottom rect
|
||||
|
||||
// Common rect positions and dimensions
|
||||
const TOP_RECT_Y = 42
|
||||
const BOTTOM_RECT_Y = 60
|
||||
const RECT_X = 8
|
||||
const RECT_WIDTH = 48
|
||||
|
||||
// Add to existing constants
|
||||
const CIRCLE_Y = 12
|
||||
const CIRCLE_SPACING = 16
|
||||
const CIRCLE_START_X = 16
|
||||
const CIRCLE_RADIUS = 4
|
||||
|
||||
// Static circle for SessionIcon
|
||||
const ConnectionDot = ({ index, isActive }: { index: number; isActive: boolean }) => (
|
||||
<circle
|
||||
cx={CIRCLE_START_X + index * CIRCLE_SPACING}
|
||||
cy={CIRCLE_Y}
|
||||
r={CIRCLE_RADIUS}
|
||||
className={isActive ? 'fill-foreground' : 'fill-foreground-lighter'}
|
||||
/>
|
||||
)
|
||||
|
||||
export const TransactionIcon = () => {
|
||||
const [dots, setDots] = useState([false, false, false])
|
||||
const [lines, setLines] = useState([false, false, false])
|
||||
const [bottomLineActive, setBottomLineActive] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const animateDot = (index: number) => {
|
||||
// Start with dot inactive during animation in
|
||||
setDots((prev) => {
|
||||
const newDots = [...prev]
|
||||
newDots[index] = false
|
||||
return newDots
|
||||
})
|
||||
|
||||
// After animation in completes, set dot to active
|
||||
setTimeout(() => {
|
||||
setDots((prev) => {
|
||||
const newDots = [...prev]
|
||||
newDots[index] = true
|
||||
return newDots
|
||||
})
|
||||
setLines((prev) => {
|
||||
const newLines = [...prev]
|
||||
newLines[index] = true
|
||||
return newLines
|
||||
})
|
||||
setBottomLineActive(true)
|
||||
}, 200)
|
||||
|
||||
// Start exit animation, set dot inactive first
|
||||
setTimeout(() => {
|
||||
setDots((prev) => {
|
||||
const newDots = [...prev]
|
||||
newDots[index] = false
|
||||
return newDots
|
||||
})
|
||||
setLines((prev) => {
|
||||
const newLines = [...prev]
|
||||
newLines[index] = false
|
||||
if (!newLines.some(Boolean)) {
|
||||
setBottomLineActive(false)
|
||||
}
|
||||
return newLines
|
||||
})
|
||||
}, 700)
|
||||
}
|
||||
|
||||
const intervals = [0, 1, 2].map((index) =>
|
||||
setInterval(() => animateDot(index), Math.random() * 2000 + 1500)
|
||||
)
|
||||
|
||||
return () => intervals.forEach(clearInterval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', width: 64, height: 72 }}>
|
||||
<svg width="64" height="72" viewBox="0 0 64 72">
|
||||
{[0, 1, 2].map((index) => (
|
||||
<React.Fragment key={index}>
|
||||
<AnimatePresence>
|
||||
{dots[index] && (
|
||||
<motion.circle
|
||||
cx={CIRCLE_START_X + index * CIRCLE_SPACING}
|
||||
cy={CIRCLE_Y}
|
||||
r={CIRCLE_RADIUS}
|
||||
className={'fill-foreground'}
|
||||
initial={{
|
||||
scale: 0,
|
||||
x: [-8, -5, -2][index],
|
||||
y: -10,
|
||||
}}
|
||||
animate={{
|
||||
scale: 1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
}}
|
||||
exit={{
|
||||
scale: 0,
|
||||
x: [8, 5, 2][index],
|
||||
y: -10,
|
||||
}}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 500,
|
||||
damping: 30,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<TopRect isActive={bottomLineActive} />
|
||||
<BottomRect isActive={bottomLineActive} />
|
||||
</svg>
|
||||
{[0, 1, 2].map((index) => (
|
||||
<FlowingLine
|
||||
key={index}
|
||||
x={CIRCLE_START_X + index * CIRCLE_SPACING}
|
||||
y1={TOP_LINE_START}
|
||||
y2={TOP_LINE_END}
|
||||
isActive={lines[index]}
|
||||
/>
|
||||
))}
|
||||
<FlowingLine
|
||||
x={CIRCLE_START_X + 1 * CIRCLE_SPACING}
|
||||
y1={BOTTOM_LINE_START}
|
||||
y2={BOTTOM_LINE_END}
|
||||
isActive={bottomLineActive}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const SessionIcon = () => {
|
||||
const [topLineStates, setTopLineStates] = useState([false, false, false])
|
||||
const [bottomLineStates, setBottomLineStates] = useState([false, false, false])
|
||||
|
||||
useEffect(() => {
|
||||
// Function to animate a single dot
|
||||
const animateDot = (index: number) => {
|
||||
setTopLineStates((prev) => {
|
||||
const newState = [...prev]
|
||||
newState[index] = true
|
||||
return newState
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
setBottomLineStates((prev) => {
|
||||
const newState = [...prev]
|
||||
newState[index] = true
|
||||
return newState
|
||||
})
|
||||
}, 300)
|
||||
|
||||
setTimeout(() => {
|
||||
setTopLineStates((prev) => {
|
||||
const newState = [...prev]
|
||||
newState[index] = false
|
||||
return newState
|
||||
})
|
||||
setBottomLineStates((prev) => {
|
||||
const newState = [...prev]
|
||||
newState[index] = false
|
||||
return newState
|
||||
})
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
// Start initial animations immediately with slight delays
|
||||
setTimeout(() => animateDot(0), 100)
|
||||
setTimeout(() => animateDot(1), 1500)
|
||||
setTimeout(() => animateDot(2), 3000)
|
||||
|
||||
// Set up intervals for subsequent animations
|
||||
const intervals = [0, 1, 2].map((index) =>
|
||||
setInterval(() => animateDot(index), Math.random() * 3000 + 8000)
|
||||
)
|
||||
|
||||
return () => intervals.forEach(clearInterval)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', width: 64, height: 72 }}>
|
||||
<svg width="64" height="72" viewBox="0 0 64 72">
|
||||
{[0, 1, 2].map((index) => (
|
||||
<g key={index}>
|
||||
<ConnectionDot index={index} isActive={topLineStates[index]} />
|
||||
</g>
|
||||
))}
|
||||
<TopRect isActive={topLineStates.some((state) => state)} />
|
||||
<BottomRect isActive={bottomLineStates.some((state) => state)} />
|
||||
</svg>
|
||||
{[0, 1, 2].map((index) => (
|
||||
<React.Fragment key={index}>
|
||||
<FlowingLine
|
||||
x={CIRCLE_START_X + index * CIRCLE_SPACING}
|
||||
y1={TOP_LINE_START}
|
||||
y2={TOP_LINE_END}
|
||||
isActive={topLineStates[index]}
|
||||
/>
|
||||
<FlowingLine
|
||||
x={CIRCLE_START_X + index * CIRCLE_SPACING}
|
||||
y1={BOTTOM_LINE_START}
|
||||
y2={BOTTOM_LINE_END}
|
||||
isActive={bottomLineStates[index]}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { useParams } from 'common'
|
||||
import { ExternalLink, Plug } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { DatabaseConnectionString } from 'components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString'
|
||||
import { DatabaseConnectionString } from 'components/interfaces/Connect/DatabaseConnectionString'
|
||||
import { PoolingModesModal } from 'components/interfaces/Settings/Database/PoolingModesModal'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
|
||||
@@ -29,6 +29,7 @@ import { CONNECTION_TYPES, ConnectionType, FRAMEWORKS, MOBILES, ORMS } from './C
|
||||
import { getContentFilePath } from './Connect.utils'
|
||||
import ConnectDropdown from './ConnectDropdown'
|
||||
import ConnectTabContent from './ConnectTabContent'
|
||||
import { PoolingModesDialog } from 'components/interfaces/Connect/PoolingModesDialog'
|
||||
|
||||
const Connect = () => {
|
||||
const { ref: projectRef } = useParams()
|
||||
@@ -256,14 +257,14 @@ const Connect = () => {
|
||||
<TabsContent_Shadcn_
|
||||
key="direct"
|
||||
value="direct"
|
||||
className={cn(DIALOG_PADDING_X_SMALL, DIALOG_PADDING_Y, '!mt-0')}
|
||||
className={cn('!mt-0', 'p-0', 'flex flex-col gap-6', DIALOG_PADDING_Y)}
|
||||
>
|
||||
<DatabaseConnectionString appearance="minimal" />
|
||||
<DatabaseConnectionString />
|
||||
</TabsContent_Shadcn_>
|
||||
</Tabs_Shadcn_>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<PoolingModesModal />
|
||||
<PoolingModesDialog />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,35 +60,42 @@ const DatabaseSelector = ({
|
||||
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<div className="flex items-center space-x-2 cursor-pointer">
|
||||
<Button
|
||||
type="default"
|
||||
className={cn(
|
||||
'pr-2',
|
||||
variant === 'connected-on-right' && 'rounded-r-none',
|
||||
variant === 'connected-on-left' && 'rounded-l-none border-l-0',
|
||||
variant === 'connected-on-both' && 'rounded-none border-x-0'
|
||||
)}
|
||||
icon={isLoading && <Loader2 className="animate-spin" />}
|
||||
iconRight={<ChevronDown className="text-foreground-light" strokeWidth={2} size={12} />}
|
||||
>
|
||||
<span className="text-foreground-muted mr-1">source</span>
|
||||
{selectedAdditionalOption ? (
|
||||
<span>{selectedAdditionalOption.name}</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="capitalize">
|
||||
{isLoading || selectedDatabase?.identifier === projectRef
|
||||
? 'Primary database'
|
||||
: 'Read replica'}
|
||||
</span>{' '}
|
||||
{isSuccess && selectedDatabase?.identifier !== projectRef && (
|
||||
<span>
|
||||
({selectedDatabaseRegion} - {formattedDatabaseId})
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<div className="flex">
|
||||
<span className="flex items-center text-foreground-lighter px-3 rounded-lg rounded-r-none text-xs border border-button border-r-0">
|
||||
Source
|
||||
</span>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
className={cn(
|
||||
'pr-2 rounded-l-none',
|
||||
variant === 'connected-on-right' && 'rounded-r-none',
|
||||
variant === 'connected-on-left' && 'rounded-l-none border-l-0',
|
||||
variant === 'connected-on-both' && 'rounded-none border-x-0'
|
||||
)}
|
||||
icon={isLoading && <Loader2 className="animate-spin" />}
|
||||
iconRight={
|
||||
<ChevronDown className="text-foreground-light" strokeWidth={2} size={12} />
|
||||
}
|
||||
>
|
||||
{selectedAdditionalOption ? (
|
||||
<span>{selectedAdditionalOption.name}</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="capitalize">
|
||||
{isLoading || selectedDatabase?.identifier === projectRef
|
||||
? 'Primary database'
|
||||
: 'Read replica'}
|
||||
</span>{' '}
|
||||
{isSuccess && selectedDatabase?.identifier !== projectRef && (
|
||||
<span>
|
||||
({selectedDatabaseRegion} - {formattedDatabaseId})
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_ className="p-0 w-64" side="bottom" align="end">
|
||||
|
||||
Reference in new issue
Block a user