ConnectionPanel.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. import { useParams } from 'common'
  2. import { ChevronRight, FileCode, X } from 'lucide-react'
  3. import Link from 'next/link'
  4. import { PropsWithChildren, ReactNode } from 'react'
  5. import {
  6. Badge,
  7. Button,
  8. cn,
  9. Collapsible,
  10. CollapsibleContent,
  11. CollapsibleTrigger,
  12. WarningIcon,
  13. } from 'ui'
  14. import { Admonition } from 'ui-patterns/admonition'
  15. import { CodeBlock, type CodeBlockLang } from 'ui-patterns/CodeBlock'
  16. import { ConnectionParameters } from './ConnectionParameters'
  17. import { useSupavisorConfigurationQuery } from '@/data/database/supavisor-configuration-query'
  18. import { IS_PLATFORM } from '@/lib/constants'
  19. import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
  20. interface ConnectionPanelProps {
  21. type?: 'direct' | 'transaction' | 'session'
  22. badge?: string
  23. title: string
  24. description: string
  25. contentFooter?: ReactNode
  26. connectionString: string
  27. ipv4Status: {
  28. type: 'error' | 'success'
  29. title: string
  30. description?: string | ReactNode
  31. links?: { text: string; url: string }[]
  32. }
  33. notice?: string[]
  34. parameters?: Array<{
  35. key: string
  36. value: string
  37. description?: string
  38. }>
  39. contentType?: 'input' | 'code'
  40. lang?: CodeBlockLang
  41. fileTitle?: string
  42. onCopyCallback: () => void
  43. }
  44. const IPv4StatusIcon = ({ className, active }: { className?: string; active: boolean }) => {
  45. return (
  46. <div className={cn('relative inline-flex', className)}>
  47. <svg
  48. xmlns="http://www.w3.org/2000/svg"
  49. fill="none"
  50. viewBox="0 0 24 24"
  51. strokeWidth="1"
  52. stroke="currentColor"
  53. className="size-6 stroke-foreground-lighter"
  54. >
  55. <path
  56. strokeLinecap="round"
  57. strokeLinejoin="round"
  58. d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418"
  59. />
  60. </svg>
  61. {!active ? (
  62. <div className="absolute -right-1.5 -top-1.5 bg-destructive rounded-sm w-4 h-4 flex items-center justify-center">
  63. <X size={10} strokeWidth={4} className="text-white rounded-full" />
  64. </div>
  65. ) : (
  66. <div className="absolute -right-1.5 -top-1.5 bg-brand-500 rounded-sm w-4 h-4 flex items-center justify-center">
  67. <svg
  68. width="10"
  69. height="10"
  70. viewBox="0 0 10 10"
  71. fill="none"
  72. xmlns="http://www.w3.org/2000/svg"
  73. >
  74. <path
  75. d="M8.33325 2.5L3.74992 7.08333L1.66659 5"
  76. stroke="white"
  77. strokeWidth="2"
  78. strokeLinecap="round"
  79. strokeLinejoin="round"
  80. />
  81. </svg>
  82. </div>
  83. )}
  84. </div>
  85. )
  86. }
  87. export const CodeBlockFileHeader = ({ title }: { title: string }) => {
  88. return (
  89. <div className="flex items-center justify-between px-4 py-1 bg-surface-100/50 border border-b-0 border-surface rounded-t">
  90. <div className="flex items-center gap-2">
  91. <FileCode size={12} className="text-foreground-muted" strokeWidth={1.5} />
  92. <span className="text-xs text-foreground-light">{title}</span>
  93. </div>
  94. </div>
  95. )
  96. }
  97. export const ConnectionPanel = ({
  98. type = 'direct',
  99. badge,
  100. title,
  101. description,
  102. contentFooter,
  103. connectionString,
  104. ipv4Status,
  105. notice,
  106. parameters = [],
  107. lang = 'bash',
  108. fileTitle,
  109. children,
  110. onCopyCallback,
  111. }: PropsWithChildren<ConnectionPanelProps>) => {
  112. const { ref: projectRef } = useParams()
  113. const state = useDatabaseSelectorStateSnapshot()
  114. const { data: poolingInfo } = useSupavisorConfigurationQuery({ projectRef })
  115. const poolingConfiguration = poolingInfo?.find((x) => x.identifier === state.selectedDatabaseId)
  116. const isSessionMode = poolingConfiguration?.pool_mode === 'session'
  117. const links = ipv4Status.links ?? []
  118. const isTransactionDedicatedPooler = type === 'transaction' && badge === 'Dedicated Pooler'
  119. return (
  120. <div className="relative text-sm flex flex-col gap-5 lg:grid lg:grid-cols-12 w-full">
  121. <div className="col-span-4 flex flex-col">
  122. <div className="flex items-center gap-x-2 mb-2">
  123. <h1 className="text-sm">{title}</h1>
  124. {!!badge && !isTransactionDedicatedPooler && <Badge>{badge}</Badge>}
  125. </div>
  126. <p className="text-sm text-foreground-light mb-4">{description}</p>
  127. {contentFooter}
  128. </div>
  129. <div className="col-span-8 flex flex-col gap-2">
  130. {isTransactionDedicatedPooler && (
  131. <div className="text-xs flex items-center text-foreground-light">
  132. Using the Dedicated Pooler:
  133. </div>
  134. )}
  135. <div className="flex flex-col -space-y-px">
  136. {fileTitle && <CodeBlockFileHeader title={fileTitle} />}
  137. {type === 'transaction' && isSessionMode ? (
  138. <Admonition
  139. showIcon={false}
  140. type="default"
  141. className="[&>h5]:text-xs [&>div]:text-xs"
  142. title="Transaction pooler is unavailable as pool mode is set to Session"
  143. description="If you'd like to use transaction mode, update your pool mode to Transaction for the connection pooler in your project's Database Settings."
  144. >
  145. <Button asChild type="default" className="mt-2">
  146. <Link
  147. href={`/project/${projectRef}/database/settings#connection-pooler`}
  148. className="text-xs text-light hover:text-foreground"
  149. >
  150. Database Settings
  151. </Link>
  152. </Button>
  153. </Admonition>
  154. ) : (
  155. <>
  156. <CodeBlock
  157. wrapperClassName={cn(
  158. '[&_pre]:rounded-b-none [&_pre]:px-4 [&_pre]:py-3',
  159. fileTitle && '[&_pre]:rounded-t-none'
  160. )}
  161. language={lang}
  162. value={connectionString}
  163. className="[&_code]:text-[12px] [&_code]:text-foreground [&_code]:whitespace-normal!"
  164. hideLineNumbers
  165. onCopyCallback={onCopyCallback}
  166. />
  167. {notice && (
  168. <div className="border px-4 py-1 w-full justify-start rounded-t-none !last:rounded-b group-data-open:rounded-b-none">
  169. {notice?.map((text: string) => (
  170. <p key={text} className="text-xs text-foreground-lighter">
  171. {text}
  172. </p>
  173. ))}
  174. </div>
  175. )}
  176. {parameters.length > 0 && <ConnectionParameters parameters={parameters} />}
  177. </>
  178. )}
  179. </div>
  180. <div className="flex flex-col -space-y-px w-full">
  181. {IS_PLATFORM && (
  182. <div className="border border-muted px-5 flex gap-7 items-center py-3 first:rounded-t last:rounded-b">
  183. <div className="flex items-center gap-2">
  184. <IPv4StatusIcon active={ipv4Status.type === 'success'} />
  185. </div>
  186. <div className="flex flex-col">
  187. <span className="text-xs text-foreground">{ipv4Status.title}</span>
  188. {ipv4Status.description &&
  189. (typeof ipv4Status.description === 'string' ? (
  190. <span className="text-xs text-foreground-lighter">
  191. {ipv4Status.description}
  192. </span>
  193. ) : (
  194. ipv4Status.description
  195. ))}
  196. {links.length > 0 && (
  197. <div className="flex items-center gap-x-2 mt-2">
  198. {links.map((link) => (
  199. <Button key={link.text} asChild type="default" size="tiny">
  200. <Link href={link.url} className="text-xs text-light hover:text-foreground">
  201. {link.text}
  202. </Link>
  203. </Button>
  204. ))}
  205. </div>
  206. )}
  207. </div>
  208. </div>
  209. )}
  210. {type === 'session' && (
  211. <div className="border border-muted px-5 flex gap-7 items-center py-3 first:rounded-t last:rounded-b bg-alternative/50">
  212. <div className="flex w-6 h-6 rounded-sm items-center justify-center gap-2 shrink-0 bg-surface-100">
  213. <WarningIcon />
  214. </div>
  215. <div className="flex flex-col">
  216. <span className="text-xs text-foreground">Only use on a IPv4 network</span>
  217. <div className="flex flex-col text-xs text-foreground-lighter">
  218. <p>Session pooler connections are IPv4 proxied for free.</p>
  219. <p>Use Direct Connection if connecting via an IPv6 network.</p>
  220. </div>
  221. </div>
  222. </div>
  223. )}
  224. {IS_PLATFORM && ipv4Status.type === 'error' && (
  225. <Collapsible className="group -space-y-px">
  226. <CollapsibleTrigger
  227. asChild
  228. className="group/collapse w-full justify-start rounded-t-none !last:rounded-b group-data-open:rounded-b-none border-muted"
  229. >
  230. <Button
  231. type="default"
  232. size="tiny"
  233. className="text-foreground-lighter bg-dash-sidebar!"
  234. icon={
  235. <ChevronRight
  236. className={cn(
  237. 'group-data-open/collapse:rotate-90 text-foreground-muted transition-transform'
  238. )}
  239. />
  240. }
  241. >
  242. Some platforms are IPv4-only:
  243. </Button>
  244. </CollapsibleTrigger>
  245. <CollapsibleContent className="bg-dash-sidebar rounded-b border px-3 py-2">
  246. <div className="flex flex-col gap-2">
  247. <p className="text-xs text-foreground-light max-w-xs">
  248. A few major platforms are IPv4-only and may not work with a Direct Connection:
  249. </p>
  250. <div className="flex gap-4">
  251. <div className="text-foreground text-xs">Vercel</div>
  252. <div className="text-foreground text-xs">GitHub Actions</div>
  253. <div className="text-foreground text-xs">Render</div>
  254. <div className="text-foreground text-xs">Retool</div>
  255. </div>
  256. <p className="text-xs text-foreground-lighter max-w-xs">
  257. If you wish to use a Direct Connection with these, please purchase{' '}
  258. <Link
  259. href={`/project/${projectRef}/settings/addons?panel=ipv4`}
  260. className="text-xs text-light hover:text-foreground"
  261. >
  262. IPv4 support
  263. </Link>
  264. .
  265. </p>
  266. <p className="text-xs text-foreground-lighter max-w-xs">
  267. You may also use the{' '}
  268. <span className="text-foreground-light">Session Pooler</span> or{' '}
  269. <span className="text-foreground-light">Transaction Pooler</span> if you are on
  270. a IPv4 network.
  271. </p>
  272. </div>
  273. </CollapsibleContent>
  274. </Collapsible>
  275. )}
  276. </div>
  277. {children}
  278. </div>
  279. </div>
  280. )
  281. }