ConnectionPanel.tsx293 lines · main
1import { useParams } from 'common'
2import { ChevronRight, FileCode, X } from 'lucide-react'
3import Link from 'next/link'
4import { PropsWithChildren, ReactNode } from 'react'
5import {
6 Badge,
7 Button,
8 cn,
9 Collapsible,
10 CollapsibleContent,
11 CollapsibleTrigger,
12 WarningIcon,
13} from 'ui'
14import { Admonition } from 'ui-patterns/admonition'
15import { CodeBlock, type CodeBlockLang } from 'ui-patterns/CodeBlock'
16
17import { ConnectionParameters } from './ConnectionParameters'
18import { useSupavisorConfigurationQuery } from '@/data/database/supavisor-configuration-query'
19import { IS_PLATFORM } from '@/lib/constants'
20import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
21
22interface 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
47const 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
92export 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
103export 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}