| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- import { useParams } from 'common'
- import { useCallback, useMemo, useState } from 'react'
- import { FEATURE_GROUPS_PLATFORM, MCP_CLIENTS } from 'ui-patterns/McpUrlBuilder'
- import {
- connectionStringMethodOptions,
- DATABASE_CONNECTION_TYPES,
- FRAMEWORKS,
- MOBILES,
- ORMS,
- } from './Connect.constants'
- import {
- getActiveFields,
- getDefaultState,
- resetDependentFields,
- resolveSteps,
- } from './connect.resolver'
- import { connectSchema } from './connect.schema'
- import type {
- ConnectMode,
- ConnectSchema,
- ConnectState,
- FieldOption,
- ResolvedField,
- 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'
- import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
- import { useIsHighAvailability } from '@/hooks/misc/useSelectedProject'
- // ============================================================================
- // Data Source Helpers
- // ============================================================================
- /**
- * Get field options from a data source reference.
- * This maps source names to actual data.
- */
- function getFieldOptionsFromSource({
- source,
- state,
- databases,
- }: {
- source: string
- state: ConnectState
- databases: Database[]
- }): FieldOption[] {
- switch (source) {
- case 'frameworks':
- return [...FRAMEWORKS, ...MOBILES].map((f) => ({
- value: f.key,
- label: f.label,
- icon: f.icon,
- }))
- case 'frameworkVariants': {
- // Get variants for the selected framework
- const allFrameworks = [...FRAMEWORKS, ...MOBILES]
- const selected = allFrameworks.find((f) => f.key === state.framework)
- if (!selected?.children?.length) return []
- // Only return if there are multiple children (variants)
- if (selected.children.length <= 1) return []
- return selected.children.map((c) => ({
- value: c.key,
- label: c.label,
- icon: c.icon,
- }))
- }
- case 'libraries': {
- // Get libraries for the selected framework and variant
- const allFrameworks = [...FRAMEWORKS, ...MOBILES]
- const selectedFramework = allFrameworks.find((f) => f.key === state.framework)
- if (!selectedFramework) return []
- // If framework has variants, look in the variant
- if (selectedFramework.children?.length > 1 && state.frameworkVariant) {
- const variant = selectedFramework.children.find((c) => c.key === state.frameworkVariant)
- if (variant?.children?.length) {
- return variant.children.map((c) => ({
- value: c.key,
- label: c.label,
- icon: c.icon,
- }))
- }
- }
- // Otherwise look directly in framework children
- if (selectedFramework.children?.length === 1) {
- const child = selectedFramework.children[0]
- if (child.children?.length) {
- return child.children.map((c) => ({
- value: c.key,
- label: c.label,
- icon: c.icon,
- }))
- }
- // The child itself is the library
- return [{ value: child.key, label: child.label, icon: child.icon }]
- }
- return []
- }
- case 'connectionMethods':
- return Object.values(connectionStringMethodOptions).map((m) => ({
- value: m.value,
- label: m.label,
- 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,
- label: t.label,
- }))
- case 'orms':
- return ORMS.map((o) => ({
- value: o.key,
- label: o.label,
- icon: o.icon,
- }))
- case 'mcpClients':
- return MCP_CLIENTS.map((c) => ({
- value: c.key,
- label: c.label,
- icon: c.icon,
- }))
- case 'mcpFeatures':
- return FEATURE_GROUPS_PLATFORM.map((f) => ({
- value: f.id,
- label: f.name,
- description: f.description,
- }))
- default:
- return []
- }
- }
- /**
- * Resolve field options, handling both static options and data source references.
- */
- 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
- }
- // Check if it's a source reference
- const options = connectSchema.fields[field.id]?.options
- if (options && typeof options === 'object' && 'source' in options) {
- return getFieldOptionsFromSource({ source: options.source as string, state, databases })
- }
- return []
- }
- // ============================================================================
- // Hook
- // ============================================================================
- export interface UseConnectStateReturn {
- state: ConnectState
- updateField: (fieldId: string, value: string | boolean | string[]) => void
- setMode: (mode: ConnectMode) => void
- activeFields: ResolvedField[]
- resolvedSteps: ResolvedStep[]
- getFieldOptions: (fieldId: string) => FieldOption[]
- schema: ConnectSchema
- }
- export function useConnectState(initialState?: Partial<ConnectState>): UseConnectStateReturn {
- const { ref: projectRef } = useParams()
- const { data: databases = [] } = useReadReplicasQuery({ projectRef })
- const { hasAccess: hasDedicatedPooler } = useCheckEntitlements('dedicated_pooler')
- const isHighAvailability = useIsHighAvailability()
- const [state, setState] = useState<ConnectState>(() => {
- const defaults = getDefaultState({ schema: connectSchema })
- // Set initial framework if mode is framework
- if (defaults.mode === 'framework' && !defaults.framework) {
- const firstFramework = FRAMEWORKS[0]
- defaults.framework = firstFramework?.key ?? ''
- // Set initial variant if framework has variants
- if (firstFramework?.children?.length > 1) {
- defaults.frameworkVariant = firstFramework.children[0]?.key ?? ''
- }
- // Set initial library
- const libraryKey = resolveFrameworkLibraryKey({
- framework: defaults.framework,
- frameworkVariant: defaults.frameworkVariant,
- library: defaults.library,
- })
- if (libraryKey) defaults.library = libraryKey
- }
- // Set initial ORM if mode is orm
- if (defaults.mode === 'orm' && !defaults.orm) {
- defaults.orm = ORMS[0]?.key ?? ''
- }
- // Set initial MCP client if mode is mcp
- if (defaults.mode === 'mcp' && !defaults.mcpClient) {
- defaults.mcpClient = MCP_CLIENTS[0]?.key ?? ''
- }
- return { ...defaults, ...initialState } as ConnectState
- })
- const updateField = useCallback((fieldId: string, value: string | boolean | string[]) => {
- setState((prev) => {
- const next = { ...prev, [fieldId]: value }
- // Handle cascading updates for framework selection
- if (fieldId === 'framework') {
- const allFrameworks = [...FRAMEWORKS, ...MOBILES]
- const selected = allFrameworks.find((f) => f.key === value)
- // Reset variant if framework changed
- if (selected?.children && selected.children.length > 1) {
- next.frameworkVariant = selected.children[0]?.key ?? ''
- } else {
- delete next.frameworkVariant
- }
- // Reset library
- const libraryKey = resolveFrameworkLibraryKey({
- framework: next.framework,
- frameworkVariant: next.frameworkVariant,
- })
- if (libraryKey) {
- next.library = libraryKey
- } else {
- delete next.library
- }
- }
- // Handle cascading updates for variant selection
- if (fieldId === 'frameworkVariant') {
- const libraryKey = resolveFrameworkLibraryKey({
- framework: prev.framework,
- frameworkVariant: String(value),
- })
- if (libraryKey) next.library = libraryKey
- }
- // Reset useSharedPooler when connectionMethod changes to 'direct'
- if (fieldId === 'connectionMethod' && value === 'direct') {
- next.useSharedPooler = false
- }
- return resetDependentFields(next, fieldId, connectSchema)
- })
- }, [])
- 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 ?? ''
- }
- const libraryKey = resolveFrameworkLibraryKey({
- framework: next.framework,
- frameworkVariant: next.frameworkVariant,
- })
- if (libraryKey) next.library = libraryKey
- }
- 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 === 'mcp' && !next.mcpClient) {
- next.mcpClient = MCP_CLIENTS[0]?.key ?? ''
- }
- return next
- })
- },
- [projectRef]
- )
- const activeFields = useMemo(() => {
- let fields = getActiveFields(connectSchema, state)
- if (!hasDedicatedPooler) {
- fields = fields.filter((f) => f.id !== 'useSharedPooler')
- }
- if (isHighAvailability) {
- fields = fields
- .filter((f) => f.id !== 'connectionMethod' && f.id !== 'useSharedPooler')
- .map((f) => (f.id === 'connectionType' ? { ...f, label: 'Connection Type' } : f))
- }
- return fields
- }, [state, hasDedicatedPooler, isHighAvailability])
- const resolvedSteps = useMemo(() => resolveSteps(connectSchema, state), [state])
- const getFieldOptions = useCallback(
- (fieldId: string): FieldOption[] => {
- const field = activeFields.find((f) => f.id === fieldId)
- if (!field) return []
- return resolveFieldOptionsWithSource({ field, state, databases })
- },
- [activeFields, state, databases]
- )
- return {
- state,
- updateField,
- setMode,
- activeFields,
- resolvedSteps,
- getFieldOptions,
- schema: connectSchema,
- }
- }
|