From 32802ef46174d0bb92a30bc2cab590e22af3336c Mon Sep 17 00:00:00 2001 From: Jonathan Summers-Muir Date: Fri, 15 Nov 2024 01:37:09 +0800 Subject: [PATCH] add new page --- .../Connect/DatabaseConnectionNotices.tsx | 82 ++++ .../Connect/DatabaseConnectionString.tsx | 344 +++++++++++++++++ .../interfaces/Connect/DatabaseSettings.tsx | 175 +++++++++ .../Connect/DatabaseSettings.utils.ts | 357 ++++++++++++++++++ .../interfaces/Connect/PoolingModesDialog.tsx | 109 ++++++ .../interfaces/Connect/UsePoolerCheckbox.tsx | 192 ++++++++++ .../interfaces/Connect/pooler-icons.tsx | 335 ++++++++++++++++ .../interfaces/Home/Connect/Connect.tsx | 9 +- .../studio/components/ui/DatabaseSelector.tsx | 65 ++-- 9 files changed, 1635 insertions(+), 33 deletions(-) create mode 100644 apps/studio/components/interfaces/Connect/DatabaseConnectionNotices.tsx create mode 100644 apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx create mode 100644 apps/studio/components/interfaces/Connect/DatabaseSettings.tsx create mode 100644 apps/studio/components/interfaces/Connect/DatabaseSettings.utils.ts create mode 100644 apps/studio/components/interfaces/Connect/PoolingModesDialog.tsx create mode 100644 apps/studio/components/interfaces/Connect/UsePoolerCheckbox.tsx create mode 100644 apps/studio/components/interfaces/Connect/pooler-icons.tsx diff --git a/apps/studio/components/interfaces/Connect/DatabaseConnectionNotices.tsx b/apps/studio/components/interfaces/Connect/DatabaseConnectionNotices.tsx new file mode 100644 index 00000000000..0bd005cebb1 --- /dev/null +++ b/apps/studio/components/interfaces/Connect/DatabaseConnectionNotices.tsx @@ -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 ( + + + + Direct database access is only available via IPv6-compatible networks. + + +

+ 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. +

+ +
+
+ ) +} + +export const IPv4AddonDirectConnectionNotice = () => { + return ( + + + Direct database connections are recommended if you're connecting with session mode. + + +

+ 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. +

+

+ Since your project has the IPv4 add on, you are not required to use the connection pooler + even if your network only supports IPv4. +

+ +
+
+ ) +} + +export const DefaultSessionModeNotice = () => { + return ( + + + You are not yet in Transaction mode. + +

+ You need to set your Pool Mode to{' '} + Transaction in the{' '} + Connection pooling configuration section below in + order to use transaction mode on port 6543. +

+

+ Otherwise, port 6543 will continue to connect in{' '} + Session mode. +

+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx b/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx new file mode 100644 index 00000000000..26deeb1fe35 --- /dev/null +++ b/apps/studio/components/interfaces/Connect/DatabaseConnectionString.tsx @@ -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('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 ( + <> +
+ + +
+
+
+ + Type + + setSelectedTab(connectionType)} + > + + + + + {CONNECTION_TYPES.map((type) => ( + + {type.label} + + ))} + + +
+ + {/* */} +
+
+ {isLoadingReadReplicas && } + {isErrorReadReplicas && ( + + )} + {isSuccessReadReplicas && ( + <> +
+

Direct Connection

+ handleCopy(selectedTab)} + /> +
+ + + )} +
+ +
+ {isLoadingReadReplicas && } + {isErrorReadReplicas && ( + + )} + {isSuccessReadReplicas && ( + <> +
+
+

Transaction Pooler

+ handleCopy(selectedTab)} + /> + +
+
+

Session Pooler

+ handleCopy(selectedTab)} + /> + +
+
+ + )} +
+ {poolerConnStringSyntax.length > 0 && ( + <> + + + + +
+

+ How to connect to a different database or switch to another user +

+ +
+
+ +
+

+ You can use the following URI format to switch to a different database or user + {snap.usePoolerConnection ? ' when using connection pooling' : ''}. +

+

+ {poolerConnStringSyntax.map((x, idx) => { + if (x.tooltip) { + return ( + + + {x.value} + + + {x.tooltip} + + + ) + } else { + return ( + + {x.value} + + ) + } + })} +

+
+
+
+ + {selectedTab === 'python' && ( + + +
+

+ Connecting to SQL Alchemy +

+ +
+
+ +
+

+ Please use postgresql:// instead of postgres:// as + your dialect when connecting via SQLAlchemy. +

+

+ Example: + create_engine("postgresql+psycopg2://...") +

+

+
+
+
+ )} +
+ +
+ +
+ + )} + + ) +} diff --git a/apps/studio/components/interfaces/Connect/DatabaseSettings.tsx b/apps/studio/components/interfaces/Connect/DatabaseSettings.tsx new file mode 100644 index 00000000000..d5c104b5835 --- /dev/null +++ b/apps/studio/components/interfaces/Connect/DatabaseSettings.tsx @@ -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(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) => ( +
+ + +
+ )) + } + + if (isError) { + return + } + + return ( + <> +
+ {/* {ipv4Addon === undefined && !snap.usePoolerConnection && } + {isMd5 && ( + + + + If you are connecting to your database via a GUI client, use the{' '} + connection string above instead + + + GUI clients only support database connections for Postgres 13 via a connection string. + + + )} */} +
+
+
+
+ Host:{' '} + {connectionInfo.db_host} +
+
+ Database name:{' '} + {connectionInfo.db_name} +
+
+ Port:{' '} + {poolingMode === 'transaction' ? connectionInfo.db_port : '5432'} +
+ {isMd5 && snap.usePoolerConnection && ( +
+ Options:{' '} + reference={projectRef} +
+ )} +
+ User:{' '} + {connectionInfo.db_user} +
+
+ Password:{' '} + + {state.selectedDatabaseId !== projectRef + ? '[The password for your primary database]' + : '[The password for your database]'} + +
+
+
+ For security reasons, your database password is never shown and is only readable on + creation/reset. +
+
+ + ) +} + +export default DatabaseSettings diff --git a/apps/studio/components/interfaces/Connect/DatabaseSettings.utils.ts b/apps/studio/components/interfaces/Connect/DatabaseSettings.utils.ts new file mode 100644 index 00000000000..073efc8f163 --- /dev/null +++ b/apps/studio/components/interfaces/Connect/DatabaseSettings.utils.ts @@ -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' + } +} diff --git a/apps/studio/components/interfaces/Connect/PoolingModesDialog.tsx b/apps/studio/components/interfaces/Connect/PoolingModesDialog.tsx new file mode 100644 index 00000000000..4146ff2de7e --- /dev/null +++ b/apps/studio/components/interfaces/Connect/PoolingModesDialog.tsx @@ -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 ( + + + + +
+

Which pooling mode should I use?

+ +
+
+ + A connection pooler is a system (external to Postgres) which manages Postgres + connections by allocating connections whenever clients make requests. + +
+ + + + + {primaryConfig?.pool_mode === 'session' && ( +
+ + + + Pooling mode is currently configured to use session mode + + + To use transaction mode concurrently with session mode, change the pooling mode to + transaction first in the{' '} + { + snap.setShowPoolingModeHelper(false) + navigateToPoolerSettings() + }} + > + connection pooling settings + + . After this, you can use transaction mode on port 6543 and session mode on port + 5432. + + +
+ )} + + snap.setShowPoolingModeHelper(false)}> + + + +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Connect/UsePoolerCheckbox.tsx b/apps/studio/components/interfaces/Connect/UsePoolerCheckbox.tsx new file mode 100644 index 00000000000..6990f9f60d0 --- /dev/null +++ b/apps/studio/components/interfaces/Connect/UsePoolerCheckbox.tsx @@ -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 ( +
+
+ onCheckedChange(!checked)} + /> +
+
+
+ + Display connection pooler + + {checked && ( + <> +
+ {isLoading && } + {isSuccess && ( +
+ + + + + + + onSelectOption(value as 'session' | 'transaction') + } + > + + Session mode + + {poolingConfiguration?.pool_mode === 'transaction' && ( + + Transaction mode + + )} + + + {poolingConfiguration?.pool_mode === 'session' && ( + <> + +
+

+ To use transaction mode, change the pool mode in the{' '} + {isDatabaseSettingsPage ? ( + { + const el = document.getElementById('connection-pooler') + if (el) { + setOpen(false) + el.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }) + } + }} + > + pooling configuration settings + + ) : ( + + pooling configuration settings + + )}{' '} + to use transaction mode first. +

+

+ You can then connect to session mode on port 5432 and + transaction mode on port 6543. +

+
+ + )} + + + { + setOpen(false) + snap.setShowPoolingModeHelper(true) + }} + > + How to choose pooling modes? + +
+
+
+ )} +
+ + )} +
+
+ {isSuccess && checked && Supavisor} + {isSuccessSettings && ( + + {checked || ipv4AddonAdded + ? 'Resolves to IPv4' + : resolvesToIpV6 + ? 'Resolves to IPv6' + : 'Will resolve to IPv6'} + + )} + {pgBouncerStatus?.active && ( + PgBouncer pending removal + )} +
+
+
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Connect/pooler-icons.tsx b/apps/studio/components/interfaces/Connect/pooler-icons.tsx new file mode 100644 index 00000000000..04b8d1b5fb9 --- /dev/null +++ b/apps/studio/components/interfaces/Connect/pooler-icons.tsx @@ -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 ( + + {isActive && ( + + + + + + + + + + + + )} + + ) +} + +const TopRect = ({ isActive }: { isActive: boolean }) => ( + +) + +const BottomRect = ({ isActive }: { isActive: boolean }) => ( + +) + +// 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 }) => ( + +) + +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 ( +
+ + {[0, 1, 2].map((index) => ( + + + {dots[index] && ( + + )} + + + ))} + + + + {[0, 1, 2].map((index) => ( + + ))} + +
+ ) +} + +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 ( +
+ + {[0, 1, 2].map((index) => ( + + + + ))} + state)} /> + state)} /> + + {[0, 1, 2].map((index) => ( + + + + + ))} +
+ ) +} diff --git a/apps/studio/components/interfaces/Home/Connect/Connect.tsx b/apps/studio/components/interfaces/Home/Connect/Connect.tsx index 71517f29d5f..ceea1ac740b 100644 --- a/apps/studio/components/interfaces/Home/Connect/Connect.tsx +++ b/apps/studio/components/interfaces/Home/Connect/Connect.tsx @@ -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 = () => { - + - + ) } diff --git a/apps/studio/components/ui/DatabaseSelector.tsx b/apps/studio/components/ui/DatabaseSelector.tsx index 832bdae9b11..765d337923b 100644 --- a/apps/studio/components/ui/DatabaseSelector.tsx +++ b/apps/studio/components/ui/DatabaseSelector.tsx @@ -60,35 +60,42 @@ const DatabaseSelector = ({
- +
+ + Source + + +