add new page

This commit is contained in:
Jonathan Summers-Muir committed 2024-11-15 01:37:09 +08:00
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 />
</>
)
}
+36 -29
View File
@@ -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">