useConnectState.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. import { useParams } from 'common'
  2. import { useCallback, useMemo, useState } from 'react'
  3. import { FEATURE_GROUPS_PLATFORM, MCP_CLIENTS } from 'ui-patterns/McpUrlBuilder'
  4. import {
  5. connectionStringMethodOptions,
  6. DATABASE_CONNECTION_TYPES,
  7. FRAMEWORKS,
  8. MOBILES,
  9. ORMS,
  10. } from './Connect.constants'
  11. import {
  12. getActiveFields,
  13. getDefaultState,
  14. resetDependentFields,
  15. resolveSteps,
  16. } from './connect.resolver'
  17. import { connectSchema } from './connect.schema'
  18. import type {
  19. ConnectMode,
  20. ConnectSchema,
  21. ConnectState,
  22. FieldOption,
  23. ResolvedField,
  24. ResolvedStep,
  25. } from './Connect.types'
  26. import { resolveFrameworkLibraryKey } from './Connect.utils'
  27. import { Database, useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
  28. import { formatDatabaseID, formatDatabaseRegion } from '@/data/read-replicas/replicas.utils'
  29. import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
  30. import { useIsHighAvailability } from '@/hooks/misc/useSelectedProject'
  31. // ============================================================================
  32. // Data Source Helpers
  33. // ============================================================================
  34. /**
  35. * Get field options from a data source reference.
  36. * This maps source names to actual data.
  37. */
  38. function getFieldOptionsFromSource({
  39. source,
  40. state,
  41. databases,
  42. }: {
  43. source: string
  44. state: ConnectState
  45. databases: Database[]
  46. }): FieldOption[] {
  47. switch (source) {
  48. case 'frameworks':
  49. return [...FRAMEWORKS, ...MOBILES].map((f) => ({
  50. value: f.key,
  51. label: f.label,
  52. icon: f.icon,
  53. }))
  54. case 'frameworkVariants': {
  55. // Get variants for the selected framework
  56. const allFrameworks = [...FRAMEWORKS, ...MOBILES]
  57. const selected = allFrameworks.find((f) => f.key === state.framework)
  58. if (!selected?.children?.length) return []
  59. // Only return if there are multiple children (variants)
  60. if (selected.children.length <= 1) return []
  61. return selected.children.map((c) => ({
  62. value: c.key,
  63. label: c.label,
  64. icon: c.icon,
  65. }))
  66. }
  67. case 'libraries': {
  68. // Get libraries for the selected framework and variant
  69. const allFrameworks = [...FRAMEWORKS, ...MOBILES]
  70. const selectedFramework = allFrameworks.find((f) => f.key === state.framework)
  71. if (!selectedFramework) return []
  72. // If framework has variants, look in the variant
  73. if (selectedFramework.children?.length > 1 && state.frameworkVariant) {
  74. const variant = selectedFramework.children.find((c) => c.key === state.frameworkVariant)
  75. if (variant?.children?.length) {
  76. return variant.children.map((c) => ({
  77. value: c.key,
  78. label: c.label,
  79. icon: c.icon,
  80. }))
  81. }
  82. }
  83. // Otherwise look directly in framework children
  84. if (selectedFramework.children?.length === 1) {
  85. const child = selectedFramework.children[0]
  86. if (child.children?.length) {
  87. return child.children.map((c) => ({
  88. value: c.key,
  89. label: c.label,
  90. icon: c.icon,
  91. }))
  92. }
  93. // The child itself is the library
  94. return [{ value: child.key, label: child.label, icon: child.icon }]
  95. }
  96. return []
  97. }
  98. case 'connectionMethods':
  99. return Object.values(connectionStringMethodOptions).map((m) => ({
  100. value: m.value,
  101. label: m.label,
  102. description: m.description,
  103. }))
  104. case 'connectionSources':
  105. return databases.map((db) => {
  106. const region = formatDatabaseRegion(db?.region ?? '')
  107. const id = formatDatabaseID(db.identifier ?? '')
  108. const label = db.identifier.includes('-rr-')
  109. ? `Read Replica (${region} - ${id}}`
  110. : 'Primary Database'
  111. return { value: db.identifier, label }
  112. })
  113. case 'connectionTypes':
  114. return DATABASE_CONNECTION_TYPES.map((t) => ({
  115. value: t.id,
  116. label: t.label,
  117. }))
  118. case 'orms':
  119. return ORMS.map((o) => ({
  120. value: o.key,
  121. label: o.label,
  122. icon: o.icon,
  123. }))
  124. case 'mcpClients':
  125. return MCP_CLIENTS.map((c) => ({
  126. value: c.key,
  127. label: c.label,
  128. icon: c.icon,
  129. }))
  130. case 'mcpFeatures':
  131. return FEATURE_GROUPS_PLATFORM.map((f) => ({
  132. value: f.id,
  133. label: f.name,
  134. description: f.description,
  135. }))
  136. default:
  137. return []
  138. }
  139. }
  140. /**
  141. * Resolve field options, handling both static options and data source references.
  142. */
  143. function resolveFieldOptionsWithSource({
  144. field,
  145. state,
  146. databases,
  147. }: {
  148. field: ResolvedField
  149. state: ConnectState
  150. databases: Database[]
  151. }): FieldOption[] {
  152. // If already resolved (from conditional resolution)
  153. if (field.resolvedOptions.length > 0) {
  154. return field.resolvedOptions
  155. }
  156. // Check if it's a source reference
  157. const options = connectSchema.fields[field.id]?.options
  158. if (options && typeof options === 'object' && 'source' in options) {
  159. return getFieldOptionsFromSource({ source: options.source as string, state, databases })
  160. }
  161. return []
  162. }
  163. // ============================================================================
  164. // Hook
  165. // ============================================================================
  166. export interface UseConnectStateReturn {
  167. state: ConnectState
  168. updateField: (fieldId: string, value: string | boolean | string[]) => void
  169. setMode: (mode: ConnectMode) => void
  170. activeFields: ResolvedField[]
  171. resolvedSteps: ResolvedStep[]
  172. getFieldOptions: (fieldId: string) => FieldOption[]
  173. schema: ConnectSchema
  174. }
  175. export function useConnectState(initialState?: Partial<ConnectState>): UseConnectStateReturn {
  176. const { ref: projectRef } = useParams()
  177. const { data: databases = [] } = useReadReplicasQuery({ projectRef })
  178. const { hasAccess: hasDedicatedPooler } = useCheckEntitlements('dedicated_pooler')
  179. const isHighAvailability = useIsHighAvailability()
  180. const [state, setState] = useState<ConnectState>(() => {
  181. const defaults = getDefaultState({ schema: connectSchema })
  182. // Set initial framework if mode is framework
  183. if (defaults.mode === 'framework' && !defaults.framework) {
  184. const firstFramework = FRAMEWORKS[0]
  185. defaults.framework = firstFramework?.key ?? ''
  186. // Set initial variant if framework has variants
  187. if (firstFramework?.children?.length > 1) {
  188. defaults.frameworkVariant = firstFramework.children[0]?.key ?? ''
  189. }
  190. // Set initial library
  191. const libraryKey = resolveFrameworkLibraryKey({
  192. framework: defaults.framework,
  193. frameworkVariant: defaults.frameworkVariant,
  194. library: defaults.library,
  195. })
  196. if (libraryKey) defaults.library = libraryKey
  197. }
  198. // Set initial ORM if mode is orm
  199. if (defaults.mode === 'orm' && !defaults.orm) {
  200. defaults.orm = ORMS[0]?.key ?? ''
  201. }
  202. // Set initial MCP client if mode is mcp
  203. if (defaults.mode === 'mcp' && !defaults.mcpClient) {
  204. defaults.mcpClient = MCP_CLIENTS[0]?.key ?? ''
  205. }
  206. return { ...defaults, ...initialState } as ConnectState
  207. })
  208. const updateField = useCallback((fieldId: string, value: string | boolean | string[]) => {
  209. setState((prev) => {
  210. const next = { ...prev, [fieldId]: value }
  211. // Handle cascading updates for framework selection
  212. if (fieldId === 'framework') {
  213. const allFrameworks = [...FRAMEWORKS, ...MOBILES]
  214. const selected = allFrameworks.find((f) => f.key === value)
  215. // Reset variant if framework changed
  216. if (selected?.children && selected.children.length > 1) {
  217. next.frameworkVariant = selected.children[0]?.key ?? ''
  218. } else {
  219. delete next.frameworkVariant
  220. }
  221. // Reset library
  222. const libraryKey = resolveFrameworkLibraryKey({
  223. framework: next.framework,
  224. frameworkVariant: next.frameworkVariant,
  225. })
  226. if (libraryKey) {
  227. next.library = libraryKey
  228. } else {
  229. delete next.library
  230. }
  231. }
  232. // Handle cascading updates for variant selection
  233. if (fieldId === 'frameworkVariant') {
  234. const libraryKey = resolveFrameworkLibraryKey({
  235. framework: prev.framework,
  236. frameworkVariant: String(value),
  237. })
  238. if (libraryKey) next.library = libraryKey
  239. }
  240. // Reset useSharedPooler when connectionMethod changes to 'direct'
  241. if (fieldId === 'connectionMethod' && value === 'direct') {
  242. next.useSharedPooler = false
  243. }
  244. return resetDependentFields(next, fieldId, connectSchema)
  245. })
  246. }, [])
  247. const setMode = useCallback(
  248. (mode: ConnectMode) => {
  249. setState((prev) => {
  250. const next: ConnectState = { ...prev, mode }
  251. // Initialize mode-specific defaults
  252. if (mode === 'framework' && !next.framework) {
  253. const firstFramework = FRAMEWORKS[0]
  254. next.framework = firstFramework?.key ?? ''
  255. if (firstFramework?.children?.length > 1) {
  256. next.frameworkVariant = firstFramework.children[0]?.key ?? ''
  257. }
  258. const libraryKey = resolveFrameworkLibraryKey({
  259. framework: next.framework,
  260. frameworkVariant: next.frameworkVariant,
  261. })
  262. if (libraryKey) next.library = libraryKey
  263. }
  264. if (mode === 'direct') {
  265. next.connectionMethod = next.connectionMethod ?? 'direct'
  266. next.connectionType = next.connectionType ?? 'uri'
  267. next.connectionSource = projectRef ?? '_'
  268. }
  269. if (mode === 'orm' && !next.orm) {
  270. next.orm = ORMS[0]?.key ?? ''
  271. }
  272. if (mode === 'mcp' && !next.mcpClient) {
  273. next.mcpClient = MCP_CLIENTS[0]?.key ?? ''
  274. }
  275. return next
  276. })
  277. },
  278. [projectRef]
  279. )
  280. const activeFields = useMemo(() => {
  281. let fields = getActiveFields(connectSchema, state)
  282. if (!hasDedicatedPooler) {
  283. fields = fields.filter((f) => f.id !== 'useSharedPooler')
  284. }
  285. if (isHighAvailability) {
  286. fields = fields
  287. .filter((f) => f.id !== 'connectionMethod' && f.id !== 'useSharedPooler')
  288. .map((f) => (f.id === 'connectionType' ? { ...f, label: 'Connection Type' } : f))
  289. }
  290. return fields
  291. }, [state, hasDedicatedPooler, isHighAvailability])
  292. const resolvedSteps = useMemo(() => resolveSteps(connectSchema, state), [state])
  293. const getFieldOptions = useCallback(
  294. (fieldId: string): FieldOption[] => {
  295. const field = activeFields.find((f) => f.id === fieldId)
  296. if (!field) return []
  297. return resolveFieldOptionsWithSource({ field, state, databases })
  298. },
  299. [activeFields, state, databases]
  300. )
  301. return {
  302. state,
  303. updateField,
  304. setMode,
  305. activeFields,
  306. resolvedSteps,
  307. getFieldOptions,
  308. schema: connectSchema,
  309. }
  310. }