diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx b/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx
index 45b75de7498..66040bf3b64 100644
--- a/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx
+++ b/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx
@@ -44,8 +44,6 @@ export function ConnectConfigSection({
}: ConnectConfigSectionProps) {
if (activeFields.length === 0) return null
- const formLayoutClassName = 'md:[&>div:first-child]:!w-1/3 xl:[&>div:first-child]:!w-2/5'
-
return (
{activeFields.map((field) => {
diff --git a/apps/studio/components/interfaces/ConnectSheet/connect.resolver.test.ts b/apps/studio/components/interfaces/ConnectSheet/connect.resolver.test.ts
index 57d7fdb0672..34f3ea26daa 100644
--- a/apps/studio/components/interfaces/ConnectSheet/connect.resolver.test.ts
+++ b/apps/studio/components/interfaces/ConnectSheet/connect.resolver.test.ts
@@ -471,7 +471,7 @@ describe('connect.resolver:getDefaultState', () => {
steps: [],
}
- const state = getDefaultState(schema)
+ const state = getDefaultState({ schema })
expect(state.mode).toBe('framework')
})
@@ -501,7 +501,7 @@ describe('connect.resolver:getDefaultState', () => {
steps: [],
}
- const state = getDefaultState(schema)
+ const state = getDefaultState({ schema })
expect(state.framework).toBe('nextjs')
expect(state.library).toBe('supabasejs')
expect(state.mcpReadonly).toBe(false)
@@ -514,7 +514,7 @@ describe('connect.resolver:getDefaultState', () => {
steps: [],
}
- const state = getDefaultState(schema)
+ const state = getDefaultState({ schema })
expect(state.mode).toBe('direct')
})
})
diff --git a/apps/studio/components/interfaces/ConnectSheet/connect.resolver.ts b/apps/studio/components/interfaces/ConnectSheet/connect.resolver.ts
index 2baeb7940c1..7874640a1f2 100644
--- a/apps/studio/components/interfaces/ConnectSheet/connect.resolver.ts
+++ b/apps/studio/components/interfaces/ConnectSheet/connect.resolver.ts
@@ -196,7 +196,7 @@ function resolveFieldOptions(field: { options?: unknown }, state: ConnectState):
/**
* Gets default state for the schema, using first mode and default field values.
*/
-export function getDefaultState(schema: ConnectSchema): ConnectState {
+export function getDefaultState({ schema }: { schema: ConnectSchema }): ConnectState {
const defaultMode = schema.modes[0]?.id ?? 'direct'
const state: ConnectState = { mode: defaultMode }
diff --git a/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts b/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts
index 32f0f6f2460..90d16a9e1ce 100644
--- a/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts
+++ b/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts
@@ -180,7 +180,7 @@ export const connectSchema: ConnectSchema = {
id: 'direct',
label: 'Direct',
description: 'Connection string',
- fields: ['connectionMethod', 'useSharedPooler', 'connectionType'],
+ fields: ['connectionSource', 'connectionMethod', 'useSharedPooler', 'connectionType'],
},
{
id: 'orm',
@@ -233,6 +233,13 @@ export const connectSchema: ConnectSchema = {
},
// Direct connection fields
+ connectionSource: {
+ id: 'connectionSource',
+ type: 'select',
+ label: 'Source',
+ options: { source: 'connectionSources' },
+ defaultValue: undefined,
+ },
connectionMethod: {
id: 'connectionMethod',
type: 'radio-list',
diff --git a/apps/studio/components/interfaces/ConnectSheet/content/steps/direct-connection/content.tsx b/apps/studio/components/interfaces/ConnectSheet/content/steps/direct-connection/content.tsx
index 261fdb58997..698bcf37e94 100644
--- a/apps/studio/components/interfaces/ConnectSheet/content/steps/direct-connection/content.tsx
+++ b/apps/studio/components/interfaces/ConnectSheet/content/steps/direct-connection/content.tsx
@@ -1,7 +1,10 @@
+import { useParams } from 'common'
import { useMemo } from 'react'
import { CodeBlock } from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
+import { getConnectionStrings } from '../../../DatabaseSettings.utils'
+import { getAddons } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
import {
type ConnectionStringMethod,
type DatabaseConnectionType,
@@ -15,6 +18,12 @@ import {
PASSWORD_PLACEHOLDER,
resolveConnectionString,
} from '@/components/interfaces/ConnectSheet/ConnectionString.utils'
+import { usePgbouncerConfigQuery } from '@/data/database/pgbouncer-config-query'
+import { useSupavisorConfigurationQuery } from '@/data/database/supavisor-configuration-query'
+import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
+import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
+import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
+import { pluckObjectFields } from '@/lib/helpers'
const buildPsqlCommand = (params: { host: string; port: string; database: string; user: string }) =>
`psql -h ${params.host} -p ${params.port} -d ${params.database} -U ${params.user}`
@@ -22,15 +31,93 @@ const buildPsqlCommand = (params: { host: string; port: string; database: string
const buildJdbcString = (params: { host: string; port: string; database: string; user: string }) =>
`jdbc:postgresql://${params.host}:${params.port}/${params.database}?user=${params.user}&password=${PASSWORD_PLACEHOLDER}`
+/**
+ * [Joshen] ConnectStepsSection does something similar but since only this page needs to consider connection strings
+ * from all databases (including read replicas), am opting to separate the logic for retrieving connection strings here
+ *
+ * We can however, consider to shift this logic into ConnectStepsSection, such that we can consider read replicas for
+ * the other tabs like "Framework" and "ORM" too. However, leaving them out for now and only updating "Direct"
+ */
+const useConnectionStringDatabases = () => {
+ const { ref: projectRef } = useParams()
+ const { hasAccess: allowPgBouncerSelection } = useCheckEntitlements('dedicated_pooler')
+
+ const { data: databases = [] } = useReadReplicasQuery({ projectRef })
+ const { data: pgbouncerConfig } = usePgbouncerConfigQuery({ projectRef })
+ const { data: supavisorConfig } = useSupavisorConfigurationQuery({ projectRef })
+ const { data: addons } = useProjectAddonsQuery({ projectRef })
+ const { ipv4: ipv4Addon } = getAddons(addons?.selected_addons ?? [])
+
+ const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at']
+ const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' }
+
+ return Object.fromEntries(
+ databases.map((db) => {
+ const connectionInfo = pluckObjectFields(db || emptyState, DB_FIELDS)
+ const poolingConfigurationShared = supavisorConfig?.find(
+ (x) => x.identifier === db.identifier
+ )
+ const poolingConfigurationDedicated = allowPgBouncerSelection ? pgbouncerConfig : undefined
+
+ const connectionStringsShared = getConnectionStrings({
+ connectionInfo,
+ poolingInfo: {
+ connectionString: poolingConfigurationShared?.connection_string ?? '',
+ db_host: poolingConfigurationShared?.db_host ?? '',
+ db_name: poolingConfigurationShared?.db_name ?? '',
+ db_port: poolingConfigurationShared?.db_port ?? 0,
+ db_user: poolingConfigurationShared?.db_user ?? '',
+ },
+ metadata: { projectRef: db.identifier },
+ })
+
+ const connectionStringsDedicated =
+ poolingConfigurationDedicated !== undefined
+ ? getConnectionStrings({
+ connectionInfo,
+ poolingInfo: {
+ connectionString: poolingConfigurationDedicated.connection_string.replace(
+ projectRef ?? '_',
+ db.identifier
+ ),
+ db_host: poolingConfigurationDedicated.db_host,
+ db_name: poolingConfigurationDedicated.db_name,
+ db_port: poolingConfigurationDedicated.db_port,
+ db_user: poolingConfigurationDedicated.db_user,
+ },
+ metadata: { projectRef: db.identifier },
+ })
+ : undefined
+
+ return [
+ db.identifier,
+ {
+ transactionShared: connectionStringsShared.pooler.uri,
+ sessionShared: connectionStringsShared.pooler.uri.replace('6543', '5432'),
+ transactionDedicated: connectionStringsDedicated?.pooler.uri,
+ sessionDedicated: connectionStringsDedicated?.pooler.uri.replace('6543', '5432'),
+ ipv4SupportedForDedicatedPooler: !!ipv4Addon,
+ direct: connectionStringsShared.direct.uri,
+ },
+ ]
+ })
+ )
+}
+
/**
* Step component for direct database connections.
* Uses state to determine which connection string to show.
*/
-function DirectConnectionContent({ state, connectionStringPooler }: StepContentProps) {
+function DirectConnectionContent({ state }: StepContentProps) {
+ const connectionSource = state.connectionSource
const connectionType = (state.connectionType as DatabaseConnectionType) ?? 'uri'
const connectionMethod = (state.connectionMethod as ConnectionStringMethod) ?? 'direct'
const useSharedPooler = Boolean(state.useSharedPooler)
+ const connectionStrings = useConnectionStringDatabases()
+ const connectionStringPooler =
+ connectionStrings[connectionSource as keyof typeof connectionStrings]
+
// Determine which connection string to use
const resolvedConnectionString = useMemo(
() =>
diff --git a/apps/studio/components/interfaces/ConnectSheet/useConnectState.test.ts b/apps/studio/components/interfaces/ConnectSheet/useConnectState.test.ts
index a882d8c77de..79de26f040d 100644
--- a/apps/studio/components/interfaces/ConnectSheet/useConnectState.test.ts
+++ b/apps/studio/components/interfaces/ConnectSheet/useConnectState.test.ts
@@ -1,8 +1,16 @@
import { act, renderHook } from '@testing-library/react'
-import { describe, expect, test } from 'vitest'
+import { describe, expect, test, vi } from 'vitest'
import { useConnectState } from './useConnectState'
+vi.mock('common', () => ({
+ useParams: () => ({ ref: 'test-ref' }),
+}))
+
+vi.mock('@/data/read-replicas/replicas-query', () => ({
+ useReadReplicasQuery: () => ({ data: [] }),
+}))
+
describe('useConnectState', () => {
// ============================================================================
// Initial State Tests
diff --git a/apps/studio/components/interfaces/ConnectSheet/useConnectState.ts b/apps/studio/components/interfaces/ConnectSheet/useConnectState.ts
index b330928fede..5e55f1e1ad7 100644
--- a/apps/studio/components/interfaces/ConnectSheet/useConnectState.ts
+++ b/apps/studio/components/interfaces/ConnectSheet/useConnectState.ts
@@ -1,3 +1,4 @@
+import { useParams } from 'common'
import { useCallback, useMemo, useState } from 'react'
import { FEATURE_GROUPS_PLATFORM, MCP_CLIENTS } from 'ui-patterns/McpUrlBuilder'
@@ -24,6 +25,8 @@ import type {
ResolvedStep,
} from './Connect.types'
import { resolveFrameworkLibraryKey } from './Connect.utils'
+import { Database, useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
+import { formatDatabaseID, formatDatabaseRegion } from '@/data/read-replicas/replicas.utils'
// ============================================================================
// Data Source Helpers
@@ -33,7 +36,15 @@ import { resolveFrameworkLibraryKey } from './Connect.utils'
* Get field options from a data source reference.
* This maps source names to actual data.
*/
-function getFieldOptionsFromSource(source: string, state: ConnectState): FieldOption[] {
+function getFieldOptionsFromSource({
+ source,
+ state,
+ databases,
+}: {
+ source: string
+ state: ConnectState
+ databases: Database[]
+}): FieldOption[] {
switch (source) {
case 'frameworks':
return [...FRAMEWORKS, ...MOBILES].map((f) => ({
@@ -98,6 +109,16 @@ function getFieldOptionsFromSource(source: string, state: ConnectState): FieldOp
description: m.description,
}))
+ case 'connectionSources':
+ return databases.map((db) => {
+ const region = formatDatabaseRegion(db?.region ?? '')
+ const id = formatDatabaseID(db.identifier ?? '')
+ const label = db.identifier.includes('-rr-')
+ ? `Read Replica (${region} - ${id}}`
+ : 'Primary Database'
+ return { value: db.identifier, label }
+ })
+
case 'connectionTypes':
return DATABASE_CONNECTION_TYPES.map((t) => ({
value: t.id,
@@ -133,7 +154,15 @@ function getFieldOptionsFromSource(source: string, state: ConnectState): FieldOp
/**
* Resolve field options, handling both static options and data source references.
*/
-function resolveFieldOptionsWithSource(field: ResolvedField, state: ConnectState): FieldOption[] {
+function resolveFieldOptionsWithSource({
+ field,
+ state,
+ databases,
+}: {
+ field: ResolvedField
+ state: ConnectState
+ databases: Database[]
+}): FieldOption[] {
// If already resolved (from conditional resolution)
if (field.resolvedOptions.length > 0) {
return field.resolvedOptions
@@ -142,7 +171,7 @@ function resolveFieldOptionsWithSource(field: ResolvedField, state: ConnectState
// Check if it's a source reference
const options = connectSchema.fields[field.id]?.options
if (options && typeof options === 'object' && 'source' in options) {
- return getFieldOptionsFromSource(options.source as string, state)
+ return getFieldOptionsFromSource({ source: options.source as string, state, databases })
}
return []
@@ -163,8 +192,11 @@ export interface UseConnectStateReturn {
}
export function useConnectState(initialState?: Partial): UseConnectStateReturn {
+ const { ref: projectRef } = useParams()
+ const { data: databases = [] } = useReadReplicasQuery({ projectRef })
+
const [state, setState] = useState(() => {
- const defaults = getDefaultState(connectSchema)
+ const defaults = getDefaultState({ schema: connectSchema })
// Set initial framework if mode is framework
if (defaults.mode === 'framework' && !defaults.framework) {
@@ -246,41 +278,45 @@ export function useConnectState(initialState?: Partial): UseConnec
})
}, [])
- const setMode = useCallback((mode: ConnectMode) => {
- setState((prev) => {
- const next: ConnectState = { ...prev, mode }
+ const setMode = useCallback(
+ (mode: ConnectMode) => {
+ setState((prev) => {
+ const next: ConnectState = { ...prev, mode }
- // Initialize mode-specific defaults
- if (mode === 'framework' && !next.framework) {
- const firstFramework = FRAMEWORKS[0]
- next.framework = firstFramework?.key ?? ''
- if (firstFramework?.children?.length > 1) {
- next.frameworkVariant = firstFramework.children[0]?.key ?? ''
+ // Initialize mode-specific defaults
+ if (mode === 'framework' && !next.framework) {
+ const firstFramework = FRAMEWORKS[0]
+ next.framework = firstFramework?.key ?? ''
+ if (firstFramework?.children?.length > 1) {
+ next.frameworkVariant = firstFramework.children[0]?.key ?? ''
+ }
+ const libraryKey = resolveFrameworkLibraryKey({
+ framework: next.framework,
+ frameworkVariant: next.frameworkVariant,
+ library: next.library,
+ })
+ if (libraryKey) next.library = libraryKey
}
- const libraryKey = resolveFrameworkLibraryKey({
- framework: next.framework,
- frameworkVariant: next.frameworkVariant,
- library: next.library,
- })
- if (libraryKey) next.library = libraryKey
- }
- if (mode === 'direct') {
- next.connectionMethod = next.connectionMethod ?? 'direct'
- next.connectionType = next.connectionType ?? 'uri'
- }
+ if (mode === 'direct') {
+ next.connectionMethod = next.connectionMethod ?? 'direct'
+ next.connectionType = next.connectionType ?? 'uri'
+ next.connectionSource = projectRef ?? '_'
+ }
- if (mode === 'orm' && !next.orm) {
- next.orm = ORMS[0]?.key ?? ''
- }
+ if (mode === 'orm' && !next.orm) {
+ next.orm = ORMS[0]?.key ?? ''
+ }
- if (mode === 'mcp' && !next.mcpClient) {
- next.mcpClient = MCP_CLIENTS[0]?.key ?? ''
- }
+ if (mode === 'mcp' && !next.mcpClient) {
+ next.mcpClient = MCP_CLIENTS[0]?.key ?? ''
+ }
- return next
- })
- }, [])
+ return next
+ })
+ },
+ [projectRef]
+ )
const activeFields = useMemo(() => getActiveFields(connectSchema, state), [state])
@@ -290,9 +326,9 @@ export function useConnectState(initialState?: Partial): UseConnec
(fieldId: string): FieldOption[] => {
const field = activeFields.find((f) => f.id === fieldId)
if (!field) return []
- return resolveFieldOptionsWithSource(field, state)
+ return resolveFieldOptionsWithSource({ field, state, databases })
},
- [activeFields, state]
+ [activeFields, state, databases]
)
return {