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