diff --git a/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx b/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx index 37f9a2865b..3c6a46adff 100644 --- a/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx +++ b/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.tsx @@ -1,5 +1,4 @@ import { zodResolver } from '@hookform/resolvers/zod' -import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import { Fragment, useEffect } from 'react' @@ -32,7 +31,6 @@ import { usePoolingConfigurationUpdateMutation } from 'data/database/pooling-con import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' import { useCheckPermissions, useStore } from 'hooks' import { POOLING_OPTIMIZATIONS } from './ConnectionPooling.constants' -import { constructConnStringSyntax, getPoolerTld } from './ConnectionPooling.utils' const formId = 'connection-pooling-form' @@ -73,21 +71,9 @@ export const ConnectionPooling = () => { isSuccess, } = usePoolingConfigurationQuery({ projectRef: projectRef }) - const poolerTld = isSuccess ? getPoolerTld(poolingInfo.connectionString) : 'com' - const connectionPoolingUnavailable = !poolingInfo?.pgbouncer_enabled && poolingInfo?.pool_mode === null - const poolerConnStringSyntax = isSuccess - ? constructConnStringSyntax(poolingInfo?.connectionString, { - ref: projectRef as string, - cloudProvider: projectIsLoading ? '' : project?.cloud_provider || '', - region: projectIsLoading ? '' : project?.region || '', - tld: poolerTld, - portNumber: poolingInfo.db_port.toString(), - }) - : [] - // [Joshen] TODO this needs to be obtained from BE as 26th Jan is when we'll start - projects will be affected at different rates const resolvesToIpV6 = !poolingInfo?.supavisor_enabled && false // Number(new Date()) > Number(dayjs.utc('01-26-2024', 'MM-DD-YYYY').toDate()) @@ -166,21 +152,22 @@ export const ConnectionPooling = () => {

{connectionPoolingUnavailable ? 'Connection Pooling is not available for this project' - : 'Connect to your database via connection pooling'} + : 'Connection pooling configuration'}

{isSuccess && (
- - With {poolingInfo?.supavisor_enabled ? 'Supavisor' : 'PGBouncer'} - - - {resolvesToIpV6 ? 'Resolves to IPv6' : 'Resolves to IPv4'} + + {poolingInfo?.supavisor_enabled ? 'Supavisor' : 'PGBouncer'}
)} @@ -256,10 +243,20 @@ export const ConnectionPooling = () => { label="Transaction" value="transaction" > - Transaction +

Transaction mode

+

+ Connection is assigned to the client for the duration of a + transaction. Some session-based Postgres features such as prepared + statements are not available with this option. +

- Session +

Session mode

+

+ When a new client connects, a connection is assigned to the client + until it disconnects. All Postgres features can be used with this + option. +

@@ -406,70 +403,6 @@ export const ConnectionPooling = () => {
)} - - -
- - -

- You may also connect to another database or with another user via Supavisor - with the following URI format: -

- - {poolerConnStringSyntax.length > 0 && ( -

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

- {x.tooltip} -
- - - - - ) - } else { - return x.value - } - })} -

- )} - - ) - } - /> )} diff --git a/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.utils.ts b/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.utils.ts deleted file mode 100644 index a4975c39fb..0000000000 --- a/apps/studio/components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling.utils.ts +++ /dev/null @@ -1,59 +0,0 @@ -// [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, - { - ref, - cloudProvider, - region, - tld, - portNumber, - }: { ref: string; cloudProvider: string; region: string; tld: string; portNumber: string } -) => { - if (connString.includes('postgres:[YOUR-PASSWORD]')) { - // PG 13 + Authentication MD5 - return [ - { value: 'postgres://', tooltip: undefined }, - { value: '[user]', tooltip: 'Database user (e.g postgres)' }, - { value: ':', tooltip: undefined }, - { value: '[password]', tooltip: 'Database password' }, - { value: '@', tooltip: undefined }, - { value: cloudProvider.toLocaleLowerCase(), tooltip: 'Cloud provider' }, - { value: '-0-', tooltip: undefined }, - { value: region, tooltip: "Project's region" }, - { value: `.pooler.supabase.${tld}:`, tooltip: undefined }, - { value: portNumber, tooltip: 'Port number (Use 5432 if using prepared statements)' }, - { value: '/', tooltip: undefined }, - { value: '[db-name]', tooltip: 'Database name (e.g postgres)' }, - { value: `?options=reference%3D`, tooltip: undefined }, - { value: ref, tooltip: "Project's reference ID" }, - ] - } else { - return [ - { value: 'postgres://', tooltip: undefined }, - { value: '[user]', tooltip: 'Database user (e.g postgres)' }, - { value: '.', tooltip: undefined }, - { value: ref, tooltip: "Project's reference ID" }, - { value: ':', tooltip: undefined }, - { value: '[password]', tooltip: 'Database password' }, - { value: '@', tooltip: undefined }, - { value: cloudProvider.toLocaleLowerCase(), tooltip: 'Cloud provider' }, - { value: '-0-', tooltip: undefined }, - { value: region, tooltip: "Project's region" }, - { value: `.pooler.supabase.${tld}:`, tooltip: undefined }, - { value: portNumber, tooltip: 'Port number (Use 5432 if using prepared statements)' }, - { value: '/', tooltip: undefined }, - { value: '[db-name]', tooltip: 'Database name (e.g postgres)' }, - ] - } -} - -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/Settings/Database/DatabaseReadOnlyAlert.tsx b/apps/studio/components/interfaces/Settings/Database/DatabaseReadOnlyAlert.tsx new file mode 100644 index 0000000000..78bf375cf4 --- /dev/null +++ b/apps/studio/components/interfaces/Settings/Database/DatabaseReadOnlyAlert.tsx @@ -0,0 +1,85 @@ +import { useParams } from 'common' +import Link from 'next/link' +import { + Alert_Shadcn_, + IconAlertTriangle, + AlertTitle_Shadcn_, + AlertDescription_Shadcn_, + Button, + IconExternalLink, +} from 'ui' + +import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query' +import { useSelectedOrganization } from 'hooks' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' +import { useState } from 'react' +import ConfirmDisableReadOnlyModeModal from './DatabaseSettings/ConfirmDisableReadOnlyModal' + +export const DatabaseReadOnlyAlert = () => { + const { ref: projectRef } = useParams() + const organization = useSelectedOrganization() + const [showConfirmationModal, setShowConfirmationModal] = useState(false) + + const { data: resourceWarnings } = useResourceWarningsQuery() + const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) + + const isReadOnlyMode = + (resourceWarnings ?? [])?.find((warning) => warning.project === projectRef) + ?.is_readonly_mode_enabled ?? false + + return ( + <> + {isReadOnlyMode && ( + + + + Project is in read-only mode and database is no longer accepting write requests + + + You have reached 95% of your project's disk space, and read-only mode has been enabled + to preserve your database's stability and prevent your project from exceeding its + current billing plan. To resolve this, you may: +
    +
  • + Temporarily disable read-only mode to free up space and reduce your database size +
  • + {subscription?.plan.id === 'free' ? ( +
  • + + Upgrade to the Pro plan + {' '} + to increase your database size limit to 8GB. +
  • + ) : subscription?.plan.id === 'pro' && subscription?.usage_billing_enabled ? ( +
  • + + Disable your Spend Cap + {' '} + to allow your project to auto-scale and expand beyond the 8GB database size limit +
  • + ) : null} +
+
+
+ + +
+
+ )} + setShowConfirmationModal(false)} + /> + + ) +} diff --git a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString.tsx b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString.tsx index a4f406f1e5..d728f2e229 100644 --- a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString.tsx +++ b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString.tsx @@ -1,23 +1,32 @@ +import * as Tooltip from '@radix-ui/react-tooltip' import { useParams, useTelemetryProps } from 'common' import { useRouter } from 'next/router' import { useEffect, useRef, useState } from 'react' -import { Input, Separator, Tabs } from 'ui' +import { Button, IconExternalLink, Input, Separator, Tabs } from 'ui' 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 { useProjectSettingsQuery } from 'data/config/project-settings-query' +import { usePoolingConfigurationQuery } from 'data/database/pooling-configuration-query' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { useFlag } from 'hooks' import { pluckObjectFields } from 'lib/helpers' import Telemetry from 'lib/telemetry' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' -import { getConnectionStrings } from './DatabaseSettings.utils' +import { IPv4DeprecationNotice } from '../IPv4DeprecationNotice' +import { UsePoolerCheckbox } from '../UsePoolerCheckbox' +import { + constructConnStringSyntax, + getConnectionStrings, + getPoolerTld, +} from './DatabaseSettings.utils' const CONNECTION_TYPES = [ - { id: 'psql', label: 'PSQL' }, { id: 'uri', label: 'URI' }, + { id: 'psql', label: 'PSQL' }, { id: 'golang', label: 'Golang' }, { id: 'jdbc', label: 'JDBC' }, { id: 'dotnet', label: '.NET' }, @@ -28,16 +37,22 @@ const CONNECTION_TYPES = [ export const DatabaseConnectionString = () => { const router = useRouter() - const { project: projectDetails } = useProjectContext() + const { project: projectDetails, isLoading: isProjectLoading } = useProjectContext() const { ref: projectRef, connectionString } = useParams() const telemetryProps = useTelemetryProps() + const readReplicasEnabled = useFlag('readReplicas') && projectDetails?.is_read_replicas_enabled + const state = useDatabaseSelectorStateSnapshot() - const readReplicasEnabled = useFlag('readReplicas') && projectDetails?.is_read_replicas_enabled const connectionStringsRef = useRef(null) + const [usePoolerConnection, setUsePoolerConnection] = useState(true) const [selectedTab, setSelectedTab] = useState< - 'psql' | 'uri' | 'golang' | 'jdbc' | 'dotnet' | 'nodejs' | 'php' | 'python' - >('psql') + 'uri' | 'psql' | 'golang' | 'jdbc' | 'dotnet' | 'nodejs' | 'php' | 'python' + >('uri') + + const { data: poolingInfo, isSuccess: isSuccessPoolingInfo } = usePoolingConfigurationQuery({ + projectRef, + }) const { data, @@ -70,6 +85,10 @@ export const DatabaseConnectionString = () => { const connectionInfo = readReplicasEnabled ? pluckObjectFields(selectedDatabase || emptyState, DB_FIELDS) : pluckObjectFields(project || 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 @@ -84,7 +103,26 @@ export const DatabaseConnectionString = () => { ) } - const connectionStrings = getConnectionStrings(connectionInfo) + const connectionStrings = isSuccessPoolingInfo + ? getConnectionStrings(connectionInfo, poolingInfo, { + projectRef, + usePoolerConnection, + }) + : { uri: '', psql: '', golang: '', jdbc: '', dotnet: '', nodejs: '', php: '', python: '' } + const poolerTld = isSuccessPoolingInfo ? getPoolerTld(poolingInfo.connectionString) : 'com' + const poolerConnStringSyntax = isSuccessPoolingInfo + ? constructConnStringSyntax(poolingInfo.connectionString, { + selectedTab, + usePoolerConnection, + ref: projectRef as string, + cloudProvider: isProjectLoading ? '' : project?.cloud_provider || '', + region: isProjectLoading ? '' : project?.region || '', + tld: usePoolerConnection ? poolerTld : connectionTld, + portNumber: usePoolerConnection + ? poolingInfo.db_port.toString() + : connectionInfo.db_port.toString(), + }) + : [] useEffect(() => { if ( @@ -99,40 +137,103 @@ export const DatabaseConnectionString = () => { return (
-
-
-
- Connection string -
- {readReplicasEnabled && } -
- - {CONNECTION_TYPES.map((type) => ( - - ))} - - -
- -
- {isLoading && } - {isError && } - {isSuccess && ( - handleCopy(selectedTab)} - /> - )} -
+ +
+
+ Connection string +
+
+ {readReplicasEnabled && } + +
+
+ + {CONNECTION_TYPES.map((type) => ( + + ))} + + +
+ } + > + + {isLoading && } + {isError && } + {isSuccess && ( +
+ + {!usePoolerConnection && } + handleCopy(selectedTab)} + /> + {poolerConnStringSyntax.length > 0 && poolingInfo?.supavisor_enabled && ( +
+

+ You can use the following URI format to switch to a different database or user + when using connection pooling. +

+

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

+ {x.tooltip} +
+ + + + + ) + } else { + return ( + + {x.value} + + ) + } + })} +

+
+ )} +
+ )} +
+ ) } diff --git a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.tsx b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.tsx index 0f37ea9d5b..fffecfaa92 100644 --- a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.tsx +++ b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.tsx @@ -1,5 +1,4 @@ import { useParams, useTelemetryProps } from 'common' -import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useRef, useState } from 'react' @@ -9,9 +8,7 @@ import Panel from 'components/ui/Panel' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { useProjectSettingsQuery } from 'data/config/project-settings-query' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' -import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' -import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query' -import { useFlag, useSelectedOrganization, useSelectedProject } from 'hooks' +import { useFlag, useSelectedProject } from 'hooks' import { pluckObjectFields } from 'lib/helpers' import Telemetry from 'lib/telemetry' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' @@ -19,16 +16,14 @@ import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, - Badge, - Button, - IconAlertTriangle, - IconExternalLink, + IconAlertCircle, Input, - Separator, } from 'ui' -import ConfirmDisableReadOnlyModeModal from './ConfirmDisableReadOnlyModal' +import { IPv4DeprecationNotice } from '../IPv4DeprecationNotice' +import { UsePoolerCheckbox } from '../UsePoolerCheckbox' import ResetDbPassword from './ResetDbPassword' -import { DatabaseConnectionString } from './DatabaseConnectionString' +import { usePoolingConfigurationQuery } from 'data/database/pooling-configuration-query' +import { getHostFromConnectionString } from './DatabaseSettings.utils' const DatabaseSettings = () => { const router = useRouter() @@ -36,17 +31,21 @@ const DatabaseSettings = () => { const telemetryProps = useTelemetryProps() const state = useDatabaseSelectorStateSnapshot() const selectedProject = useSelectedProject() - const organization = useSelectedOrganization() const readReplicasEnabled = useFlag('readReplicas') const showReadReplicasUI = readReplicasEnabled && selectedProject?.is_read_replicas_enabled const connectionStringsRef = useRef(null) - const [showConfirmationModal, setShowConfirmationModal] = useState(false) + const [usePoolerConnection, setUsePoolerConnection] = useState(true) - // [Joshen] TODO this needs to be obtained from BE as 26th Jan is when we'll start - projects will be affected at different rates - const resolvesToIpV6 = false // Number(new Date()) > Number(dayjs.utc('01-26-2024', 'MM-DD-YYYY').toDate()) - - const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) + const { + data: poolingInfo, + error: poolingInfoError, + isLoading: isLoadingPoolingInfo, + isError: isErrorPoolingInfo, + isSuccess: isSuccessPoolingInfo, + } = usePoolingConfigurationQuery({ + projectRef, + }) const { data, error: projectSettingsError, @@ -54,7 +53,6 @@ const DatabaseSettings = () => { isError: isErrorProjectSettings, isSuccess: isSuccessProjectSettings, } = useProjectSettingsQuery({ projectRef }) - const { data: resourceWarnings } = useResourceWarningsQuery() const { data: databases, error: readReplicasError, @@ -62,26 +60,40 @@ const DatabaseSettings = () => { isError: isErrorReadReplicas, isSuccess: isSuccessReadReplicas, } = useReadReplicasQuery({ projectRef }) - const error = showReadReplicasUI ? readReplicasError : projectSettingsError - const isLoading = showReadReplicasUI ? isLoadingReadReplicas : isLoadingProjectSettings - const isError = showReadReplicasUI ? isErrorReadReplicas : isErrorProjectSettings - const isSuccess = showReadReplicasUI ? isSuccessReadReplicas : isSuccessProjectSettings + const error = showReadReplicasUI ? readReplicasError : projectSettingsError || poolingInfoError + const isLoading = showReadReplicasUI + ? isLoadingReadReplicas + : isLoadingProjectSettings || isLoadingPoolingInfo + const isError = showReadReplicasUI + ? isErrorReadReplicas + : isErrorProjectSettings || isErrorPoolingInfo + const isSuccess = showReadReplicasUI + ? isSuccessReadReplicas + : isSuccessProjectSettings || isSuccessPoolingInfo const selectedDatabase = (databases ?? []).find( (db) => db.identifier === state.selectedDatabaseId ) - - const isReadOnlyMode = - (resourceWarnings ?? [])?.find((warning) => warning.project === projectRef) - ?.is_readonly_mode_enabled ?? false + const isMd5 = poolingInfo?.connectionString.includes('?options=reference') const { project } = data ?? {} - const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at'] + const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user'] const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' } - const connectionInfo = showReadReplicasUI + const dbConnectionInfo = showReadReplicasUI ? pluckObjectFields(selectedDatabase || emptyState, DB_FIELDS) : pluckObjectFields(project || emptyState, DB_FIELDS) + const connectionInfo = usePoolerConnection + ? { + db_host: isSuccessPoolingInfo + ? getHostFromConnectionString(poolingInfo?.connectionString) + : '', + db_name: poolingInfo?.db_name, + db_port: poolingInfo?.db_port, + db_user: `postgres.${projectRef}`, + } + : dbConnectionInfo + const handleCopy = (labelValue?: string) => Telemetry.sendEvent( { @@ -103,68 +115,16 @@ const DatabaseSettings = () => { return ( <>
- {isReadOnlyMode && ( - - - - Project is in read-only mode and database is no longer accepting write requests - - - You have reached 95% of your project's disk space, and read-only mode has been enabled - to preserve your database's stability and prevent your project from exceeding its - current billing plan. To resolve this, you may: -
    -
  • - Temporarily disable read-only mode to free up space and reduce your database size -
  • - {subscription?.plan.id === 'free' ? ( -
  • - - Upgrade to the Pro plan - {' '} - to increase your database size limit to 8GB. -
  • - ) : subscription?.plan.id === 'pro' && subscription?.usage_billing_enabled ? ( -
  • - - Disable your Spend Cap - {' '} - to allow your project to auto-scale and expand beyond the 8GB database size - limit -
  • - ) : null} -
-
-
- - -
-
- )} -
-
Connect to your database directly
- - {resolvesToIpV6 ? 'Resolves to IPv6' : 'Resolves to IPv4'} - +
Connection parameters
{showReadReplicasUI && } } - className="!m-0" > {isLoading && @@ -180,51 +140,27 @@ const DatabaseSettings = () => { {isError && } {isSuccess && ( <> - - - - Direct database access via IPv4 and pgBouncer will be removed from January 26th - 2024 - - -

- We strongly recommend using{' '} - { - const connectionPooler = document.getElementById('connection-pooler') - connectionPooler?.scrollIntoView({ block: 'center', behavior: 'smooth' }) - }} - > - connection pooling - {' '} - to connect to your database. You'll only need to change the connection string - that you're using in your application to the pooler's connection string which - can be found in the{' '} - { - const connectionPooler = document.getElementById('connection-pooler') - connectionPooler?.scrollIntoView({ block: 'center', behavior: 'smooth' }) - }} - > - connection pooling settings - - . -

- -
-
+
+ + {!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. + + + )} +
{ handleCopy('Host') }} /> - { value={connectionInfo.db_name} label="Database name" /> - - + {isMd5 && ( + + )} { value={connectionInfo.db_user} label="User" /> - { )}
- -
- - setShowConfirmationModal(false)} - /> ) } diff --git a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.utils.ts b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.utils.ts index 50223293c6..96ce0f50dc 100644 --- a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.utils.ts +++ b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.utils.ts @@ -1,32 +1,50 @@ -export const getConnectionStrings = (connectionInfo: { - db_user: string - db_port: number - db_host: string - db_name: string -}) => { - const uriConnString = - `postgresql://${connectionInfo.db_user}:[YOUR-PASSWORD]@` + - `${connectionInfo.db_host}:${connectionInfo.db_port.toString()}` + - `/${connectionInfo.db_name}` +import { PoolingConfiguration } from 'data/database/pooling-configuration-query' + +export const getHostFromConnectionString = (str: string) => { + const segment = str.split('[YOUR-PASSWORD]@') + const [output] = segment[1].split(':') + return output +} + +export const getConnectionStrings = ( + connectionInfo: { + db_user: string + db_port: number + db_host: string + db_name: string + }, + poolingInfo: PoolingConfiguration, + metadata: { + usePoolerConnection: boolean + projectRef?: string + pgVersion?: string + } +) => { + const { usePoolerConnection, projectRef } = metadata + + // Pooler: user, host port + const user = usePoolerConnection ? `postgres.${projectRef}` : connectionInfo.db_user + const port = usePoolerConnection ? poolingInfo?.db_port : connectionInfo.db_port + // [Joshen] Temp FE: extract host from pooler connection string + const host = usePoolerConnection + ? getHostFromConnectionString(poolingInfo.connectionString) + : connectionInfo.db_host + const name = usePoolerConnection ? poolingInfo?.db_name : connectionInfo.db_name + + const uriConnString = usePoolerConnection + ? poolingInfo?.connectionString + : `postgresql://${user}:[YOUR-PASSWORD]@` + `${host}:${port}` + `/${name}` const golangConnString = - `user=${connectionInfo.db_user} password=[YOUR-PASSWORD] ` + - `host=${connectionInfo.db_host} port=${connectionInfo.db_port.toString()}` + - ` dbname=${connectionInfo.db_name}` - const psqlConnString = - `psql -h ${connectionInfo.db_host} -p ` + - `${connectionInfo.db_port.toString()} -d ${connectionInfo.db_name} ` + - `-U ${connectionInfo.db_user}` + `user=${user} password=[YOUR-PASSWORD] ` + `host=${host} port=${port}` + ` dbname=${name}` + const psqlConnString = `psql -h ${host} -p ` + `${port} -d ${name} ` + `-U ${user}` const jdbcConnString = - `jdbc:postgresql://${connectionInfo.db_host}:${connectionInfo.db_port.toString()}` + - `/${connectionInfo.db_name}?user=${connectionInfo.db_user}&password=[YOUR-PASSWORD]` + `jdbc:postgresql://${host}:${port}` + `/${name}?user=${user}&password=[YOUR-PASSWORD]` const dotNetConnString = - `User Id=${connectionInfo.db_user};Password=[YOUR-PASSWORD];` + - `Server=${connectionInfo.db_host};Port=${connectionInfo.db_port.toString()};` + - `Database=${connectionInfo.db_name}` + `User Id=${user};Password=[YOUR-PASSWORD];` + + `Server=${host};Port=${port};` + + `Database=${name}` const pythonConnString = - `user=${connectionInfo.db_user} password=[YOUR-PASSWORD]` + - ` host=${connectionInfo.db_host} port=${connectionInfo.db_port.toString()}` + - ` database=${connectionInfo.db_name}` + `user=${user} password=[YOUR-PASSWORD]` + ` host=${host} port=${port}` + ` database=${name}` return { psql: psqlConnString, @@ -39,3 +57,302 @@ export const getConnectionStrings = (connectionInfo: { python: pythonConnString, } } + +// [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's reference ID" }, + { value: `.supabase.${tld}`, tooltip: undefined }, + ] + + if (selectedTab === 'uri' || selectedTab === 'nodejs') { + if (isMd5) { + return [ + { value: 'postgres://', tooltip: undefined }, + { value: '[user]', tooltip: 'Database user (e.g postgres)' }, + { value: ':', tooltip: undefined }, + { value: '[password]', tooltip: 'Database password' }, + { value: '@', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: ':', tooltip: undefined }, + { value: portNumber, tooltip: 'Port number (Use 5432 if using prepared statements)' }, + { value: '/', tooltip: undefined }, + { value: '[db-name]', tooltip: 'Database name (e.g postgres)' }, + ...(usePoolerConnection + ? [ + { value: `?options=reference%3D`, tooltip: undefined }, + { value: ref, tooltip: "Project's reference ID" }, + ] + : []), + ] + } else { + return [ + { value: 'postgres://', tooltip: undefined }, + { value: '[user]', tooltip: 'Database user (e.g postgres)' }, + ...(usePoolerConnection + ? [ + { value: '.', tooltip: undefined }, + { value: ref, tooltip: "Project's reference ID" }, + ] + : []), + { value: ':', tooltip: undefined }, + { value: '[password]', tooltip: 'Database password' }, + { value: '@', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: ':', tooltip: undefined }, + { value: portNumber, tooltip: 'Port number (Use 5432 if using prepared statements)' }, + { value: '/', tooltip: undefined }, + { value: '[db-name]', tooltip: 'Database name (e.g postgres)' }, + ] + } + } + + if (selectedTab === 'psql') { + if (isMd5) { + return [ + { value: 'psql "postgresql://', tooltip: undefined }, + { value: '[user]', tooltip: 'Database user (e.g postgres)' }, + { value: ':', tooltip: undefined }, + { value: '[password]', tooltip: 'Database password' }, + { value: '@', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: ':', tooltip: undefined }, + { value: portNumber, tooltip: 'Port number (Use 5432 if using prepared statements)' }, + { value: '/', tooltip: undefined }, + { value: '[db-name]', tooltip: 'Database name (e.g postgres)' }, + ...(usePoolerConnection + ? [ + { value: '?options=reference%3D', tooltip: undefined }, + { value: ref, tooltip: "Project's reference ID" }, + ] + : []), + ] + } else { + return [ + { value: 'psql -h ', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: ' -p ', tooltip: undefined }, + { value: portNumber, tooltip: 'Port number (Use 5432 if using prepared statements)' }, + { value: ' -d ', tooltip: undefined }, + { value: '[db-name]', tooltip: 'Database name (e.g postgres)' }, + { value: ' -U ', tooltip: undefined }, + { value: '[user]', tooltip: 'Database user (e.g postgres)' }, + ...(usePoolerConnection + ? [ + { value: '.', tooltip: undefined }, + { value: ref, tooltip: "Project's reference ID" }, + ] + : []), + ] + } + } + + if (selectedTab === 'golang' || selectedTab === 'php') { + if (isMd5) { + return [ + { value: 'user=', tooltip: undefined }, + { value: '[user]', tooltip: 'Database user (e.g postgres)' }, + { value: ' password=', tooltip: undefined }, + { value: '[password]', tooltip: 'Database password' }, + { value: ' host=', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: ' port=', tooltip: undefined }, + { value: portNumber, tooltip: 'Port number (Use 5432 if using prepared statements)' }, + { value: ' dbname=', tooltip: undefined }, + { value: '[db-name]', tooltip: 'Database name (e.g postgres)' }, + ...(usePoolerConnection + ? [ + { value: ' options=reference=', tooltip: undefined }, + { value: ref, tooltip: "Project's reference ID" }, + ] + : []), + ] + } else { + return [ + { value: 'user=', tooltip: undefined }, + { value: '[user]', tooltip: 'Database user (e.g postgres)' }, + ...(usePoolerConnection + ? [ + { value: '.', tooltip: undefined }, + { value: ref, tooltip: "Project's reference ID" }, + ] + : []), + { value: ' password=', tooltip: undefined }, + { value: '[password]', tooltip: 'Database password' }, + { value: ' host=', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: ' port=', tooltip: undefined }, + { value: portNumber, tooltip: 'Port number (Use 5432 if using prepared statements)' }, + { value: ' dbname=', tooltip: undefined }, + { value: '[db-name]', tooltip: 'Database name (e.g postgres)' }, + ] + } + } + + if (selectedTab === 'jdbc') { + if (isMd5) { + return [ + { value: 'jdbc:postgresql://', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: ':', tooltip: undefined }, + { value: portNumber, tooltip: 'Port number (Use 5432 if using prepared statements)' }, + { value: '/', tooltip: undefined }, + { value: '[db-name]', tooltip: 'Database name (e.g postgres)' }, + { value: '?user=', tooltip: undefined }, + { value: '[user]', tooltip: 'Database user (e.g postgres)' }, + { value: '&password=', tooltip: undefined }, + { value: '[password]', tooltip: 'Database password' }, + ...(usePoolerConnection + ? [ + { value: '&options=reference%3D', tooltip: undefined }, + { value: ref, tooltip: "Project's reference ID" }, + ] + : []), + ] + } else { + return [ + { value: 'jdbc:postgresql://', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: `:`, tooltip: undefined }, + { value: portNumber, tooltip: 'Port number (Use 5432 if using prepared statements)' }, + { value: '/', tooltip: undefined }, + { value: '[db-name]', tooltip: 'Database name (e.g postgres)' }, + { value: '?user=', tooltip: undefined }, + { value: '[user]', tooltip: 'Database user (e.g postgres)' }, + ...(usePoolerConnection + ? [ + { value: '.', tooltip: undefined }, + { value: ref, tooltip: "Project's reference ID" }, + ] + : []), + { value: '&password=', tooltip: undefined }, + { value: '[password]', tooltip: 'Database password' }, + ] + } + } + + if (selectedTab === 'dotnet') { + if (isMd5) { + return [ + { value: 'User Id=', tooltip: undefined }, + { value: '[user]', tooltip: 'Database user (e.g postgres)' }, + { value: ';Password=', tooltip: undefined }, + { value: '[password]', tooltip: 'Database password' }, + { value: ';Server=', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: ';Port=', tooltip: undefined }, + { value: portNumber, tooltip: 'Port number (Use 5432 if using prepared statements)' }, + { value: ';Database=', tooltip: undefined }, + { value: '[db-name]', tooltip: 'Database name (e.g postgres)' }, + ...(usePoolerConnection + ? [ + { value: ";Options='reference=", tooltip: undefined }, + { value: ref, tooltip: "Project's reference ID" }, + { value: "'", tooltip: undefined }, + ] + : []), + ] + } else { + return [ + { value: 'User Id=', tooltip: undefined }, + { value: '[user]', tooltip: 'Database user (e.g postgres)' }, + ...(usePoolerConnection + ? [ + { value: '.', tooltip: undefined }, + { value: ref, tooltip: "Project's reference ID" }, + ] + : []), + { value: ';Password=', tooltip: undefined }, + { value: '[password]', tooltip: 'Database password' }, + { value: ';Server=', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: ';Port=', tooltip: undefined }, + { value: portNumber, tooltip: 'Port number (Use 5432 if using prepared statements)' }, + { value: ';Database=', tooltip: undefined }, + { value: '[db-name]', tooltip: 'Database name (e.g postgres)' }, + ] + } + } + + if ('python') { + if (isMd5) { + return [ + { value: 'user=', tooltip: undefined }, + { value: '[user]', tooltip: 'Database user (e.g postgres)' }, + { value: ' password=', tooltip: undefined }, + { value: '[password]', tooltip: 'Database password' }, + { value: ' host=', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: ' port=', tooltip: undefined }, + { value: portNumber, tooltip: 'Port number (Use 5432 if using prepared statements)' }, + { value: ' database=', tooltip: undefined }, + { value: '[db-name]', tooltip: 'Database name (e.g postgres)' }, + ...(usePoolerConnection + ? [ + { value: ' options=reference=', tooltip: undefined }, + { value: ref, tooltip: "Project's reference ID" }, + ] + : []), + ] + } else { + return [ + { value: 'user=', tooltip: undefined }, + { value: '[user]', tooltip: 'Database user (e.g postgres)' }, + ...(usePoolerConnection + ? [ + { value: '.', tooltip: undefined }, + { value: ref, tooltip: "Project's reference ID" }, + ] + : []), + { value: ' password=', tooltip: undefined }, + { value: '[password]', tooltip: 'Database password' }, + { value: ' host=', tooltip: undefined }, + ...(usePoolerConnection ? poolerHostDetails : dbHostDetails), + { value: ' port=', tooltip: undefined }, + { value: portNumber, tooltip: 'Port number (Use 5432 if using prepared statements)' }, + { value: ' database=', tooltip: undefined }, + { value: '[db-name]', tooltip: 'Database name (e.g postgres)' }, + ] + } + } + + 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/Settings/Database/IPv4DeprecationNotice.tsx b/apps/studio/components/interfaces/Settings/Database/IPv4DeprecationNotice.tsx new file mode 100644 index 0000000000..d0eb82e14b --- /dev/null +++ b/apps/studio/components/interfaces/Settings/Database/IPv4DeprecationNotice.tsx @@ -0,0 +1,35 @@ +import { + Alert_Shadcn_, + IconAlertTriangle, + AlertTitle_Shadcn_, + AlertDescription_Shadcn_, + Button, + IconExternalLink, +} from 'ui' + +export const IPv4DeprecationNotice = () => { + return ( + + + + Direct database access via IPv4 and pgBouncer will be removed from January 26th 2024 + + +

+ We strongly recommend using connection pooling to + connect to your database. You'll only need to change the connection string that you're + using in your application to the pooler's connection string. +

+ +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Settings/Database/UsePoolerCheckbox.tsx b/apps/studio/components/interfaces/Settings/Database/UsePoolerCheckbox.tsx new file mode 100644 index 0000000000..466b3c7189 --- /dev/null +++ b/apps/studio/components/interfaces/Settings/Database/UsePoolerCheckbox.tsx @@ -0,0 +1,53 @@ +import { useParams } from 'common' +import { Badge, Checkbox_Shadcn_ } from 'ui' + +import { Markdown } from 'components/interfaces/Markdown' +import { usePoolingConfigurationQuery } from 'data/database/pooling-configuration-query' + +interface UsePoolerCheckboxInterface { + id: string + checked: boolean + onCheckedChange: (value: boolean) => void +} + +export const UsePoolerCheckbox = ({ id, checked, onCheckedChange }: UsePoolerCheckboxInterface) => { + const { ref: projectRef } = useParams() + const { data, isSuccess } = usePoolingConfigurationQuery({ projectRef }) + + // [Joshen] TODO this needs to be obtained from BE as 26th Jan is when we'll start - projects will be affected at different rates + const resolvesToIpV6 = !data?.supavisor_enabled && false // Number(new Date()) > Number(dayjs.utc('01-26-2024', 'MM-DD-YYYY').toDate()) + + return ( +
+ onCheckedChange(!checked)} + /> +
+ + +
+
+ ) +} diff --git a/apps/studio/data/database/pooling-configuration-query.ts b/apps/studio/data/database/pooling-configuration-query.ts index 4959c1ecd2..1c2ffa8191 100644 --- a/apps/studio/data/database/pooling-configuration-query.ts +++ b/apps/studio/data/database/pooling-configuration-query.ts @@ -2,11 +2,14 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query' import { get } from 'data/fetchers' import { ResponseError } from 'types' import { databaseKeys } from './keys' +import { components } from 'data/api' export type PoolingConfigurationVariables = { projectRef?: string } +export type PoolingConfiguration = components['schemas']['PgbouncerConfigResponse'] + export async function getPoolingConfiguration( { projectRef }: PoolingConfigurationVariables, signal?: AbortSignal diff --git a/apps/studio/pages/project/[ref]/settings/database.tsx b/apps/studio/pages/project/[ref]/settings/database.tsx index 3401c8184b..4e7dbfc4e3 100644 --- a/apps/studio/pages/project/[ref]/settings/database.tsx +++ b/apps/studio/pages/project/[ref]/settings/database.tsx @@ -1,29 +1,36 @@ -import { observer } from 'mobx-react-lite' -import { NextPageWithLayout } from 'types' -import { SettingsLayout } from 'components/layouts' import { ConnectionPooling, DatabaseSettings, NetworkRestrictions, } from 'components/interfaces/Settings/Database' +import { SettingsLayout } from 'components/layouts' +import { observer } from 'mobx-react-lite' +import { NextPageWithLayout } from 'types' -import SSLConfiguration from 'components/interfaces/Settings/Database/SSLConfiguration' -import DiskSizeConfiguration from 'components/interfaces/Settings/Database/DiskSizeConfiguration' import BannedIPs from 'components/interfaces/Settings/Database/BannedIPs' +import { DatabaseConnectionString } from 'components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString' +import DiskSizeConfiguration from 'components/interfaces/Settings/Database/DiskSizeConfiguration' +import SSLConfiguration from 'components/interfaces/Settings/Database/SSLConfiguration' +import { DatabaseReadOnlyAlert } from 'components/interfaces/Settings/Database/DatabaseReadOnlyAlert' const ProjectSettings: NextPageWithLayout = () => { return (
-
+

Database Settings

-
- - +
+
+ + + + +
+ + + + +
- - - -
)