Addons.tsx393 lines · main
1import { SupportCategories } from '@supabase/shared-types/out/constants'
2import { useFlag, useParams } from 'common'
3import { Lock } from 'lucide-react'
4import { useTheme } from 'next-themes'
5import Image from 'next/image'
6import {
7 Alert,
8 AlertDescription,
9 AlertTitle,
10 Badge,
11 Button,
12 Tooltip,
13 TooltipContent,
14 TooltipTrigger,
15} from 'ui'
16import { Admonition } from 'ui-patterns'
17import { PageContainer } from 'ui-patterns/PageContainer'
18import { PageSection } from 'ui-patterns/PageSection'
19
20import {
21 getCustomDomainDisabledReason,
22 getIPv4DisabledReason,
23 getPitrAlertState,
24 getPitrDisabledReason,
25} from './Addons.utils'
26import CustomDomainSidePanel from './CustomDomainSidePanel'
27import IPv4SidePanel from './IPv4SidePanel'
28import PITRSidePanel from './PITRSidePanel'
29import {
30 getAddons,
31 subscriptionHasHipaaAddon,
32} from '@/components/interfaces/Billing/Subscription/Subscription.utils'
33import { ProjectUpdateDisabledTooltip } from '@/components/interfaces/Organization/BillingSettings/ProjectUpdateDisabledTooltip'
34import { SupportLink } from '@/components/interfaces/Support/SupportLink'
35import AlertError from '@/components/ui/AlertError'
36import { InlineLink } from '@/components/ui/InlineLink'
37import { ResourceItem } from '@/components/ui/Resource/ResourceItem'
38import { ResourceList } from '@/components/ui/Resource/ResourceList'
39import { HorizontalShimmerWithIcon } from '@/components/ui/Shimmers'
40import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
41import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
42import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
43import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
44import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
45import {
46 useIsAwsCloudProvider,
47 useIsOrioleDbInAws,
48 useIsProjectActive,
49 useSelectedProjectQuery,
50} from '@/hooks/misc/useSelectedProject'
51import { BASE_PATH, DOCS_URL } from '@/lib/constants'
52import { getDatabaseMajorVersion, getSemanticVersion } from '@/lib/helpers'
53import { useAddonsPagePanel } from '@/state/addons-page'
54
55export const Addons = () => {
56 const { resolvedTheme } = useTheme()
57 const { ref: projectRef } = useParams()
58 const { setPanel } = useAddonsPagePanel()
59 const isAws = useIsAwsCloudProvider()
60 const isProjectActive = useIsProjectActive()
61 const isOrioleDbInAws = useIsOrioleDbInAws() === true
62
63 const { projectSettingsCustomDomains, projectAddonsDedicatedIpv4Address } = useIsFeatureEnabled([
64 'project_settings:custom_domains',
65 'project_addons:dedicated_ipv4_address',
66 ])
67
68 const { data: selectedOrg } = useSelectedOrganizationQuery()
69 const { data: selectedProject } = useSelectedProjectQuery()
70 const isBranch = selectedProject?.parent_project_ref
71
72 const { data: settings } = useProjectSettingsV2Query({ projectRef })
73 const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: selectedOrg?.slug })
74
75 const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
76 const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) && settings?.is_sensitive === true
77
78 // Only projects of version greater than briven-postgrest-14.1.0.44 can use PITR
79 const sufficientPgVersion =
80 // introduced as generatedSemantic version could be < 141044 even if actual version is indeed past it
81 // eg. 15.1.1.2 leads to 15112
82 getDatabaseMajorVersion(selectedProject?.dbVersion ?? '') > 14 ||
83 getSemanticVersion(selectedProject?.dbVersion ?? '') >= 141044
84
85 const {
86 data: addons,
87 error,
88 isPending: isLoading,
89 isError,
90 isSuccess,
91 } = useProjectAddonsQuery({ projectRef })
92
93 const selectedAddons = addons?.selected_addons ?? []
94 const { pitr, customDomain, ipv4 } = getAddons(selectedAddons)
95
96 const canUpdateIPv4 = settings?.db_ip_addr_config === 'ipv6'
97
98 const ipv4Enabled = ipv4 !== undefined
99 const pitrEnabled = pitr !== undefined
100 const customDomainEnabled = customDomain !== undefined
101
102 const canOpenIPv4 =
103 isAws && isProjectActive && !projectUpdateDisabled && (canUpdateIPv4 || ipv4Enabled)
104 const canOpenPITR =
105 isProjectActive &&
106 !projectUpdateDisabled &&
107 sufficientPgVersion &&
108 !hasHipaaAddon &&
109 !isOrioleDbInAws
110 const canOpenCustomDomain = isProjectActive && !projectUpdateDisabled
111
112 const ipv4DisabledReason = getIPv4DisabledReason({
113 isAws,
114 isProjectActive,
115 projectUpdateDisabled,
116 canUpdateIPv4,
117 ipv4Enabled,
118 })
119
120 const pitrDisabledReason = getPitrDisabledReason({
121 isProjectActive,
122 projectUpdateDisabled,
123 hasHipaaAddon,
124 sufficientPgVersion,
125 isOrioleDbInAws,
126 })
127
128 const customDomainDisabledReason = getCustomDomainDisabledReason({
129 isProjectActive,
130 projectUpdateDisabled,
131 })
132 const pitrAlertState = getPitrAlertState({
133 hasHipaaAddon,
134 sufficientPgVersion,
135 isOrioleDbInAws,
136 })
137
138 const listTopSpacing = isBranch ? 'mt-6' : undefined
139 const resourceItemClassName =
140 'min-h-[128px] border-b! last:border-b-0! [&>div:first-child]:hidden @lg:[&>div:first-child]:flex'
141
142 let pitrAlert = null
143
144 if (pitrAlertState === 'hipaa') {
145 pitrAlert = (
146 <Alert className="rounded-none border-0 border-b px-6">
147 <AlertTitle>PITR cannot be changed with HIPAA</AlertTitle>
148 <AlertDescription>
149 All projects should have PITR enabled by default and cannot be changed with HIPAA enabled.
150 Contact support for further assistance.
151 </AlertDescription>
152 <div className="mt-4">
153 <Button type="default" asChild>
154 <SupportLink>Contact support</SupportLink>
155 </Button>
156 </div>
157 </Alert>
158 )
159 } else if (pitrAlertState === 'legacy-project') {
160 pitrAlert = (
161 <Alert className="rounded-none border-0 border-b px-6">
162 <AlertTitle>Your project is too old to enable PITR</AlertTitle>
163 <AlertDescription>
164 <p className="text-sm leading-normal mb-2">
165 Reach out to us via support if you're interested
166 </p>
167 <Button asChild type="default">
168 <SupportLink
169 queryParams={{
170 projectRef,
171 category: SupportCategories.SALES_ENQUIRY,
172 subject: 'Project too old old for PITR',
173 }}
174 >
175 Contact support
176 </SupportLink>
177 </Button>
178 </AlertDescription>
179 </Alert>
180 )
181 } else if (pitrAlertState === 'orioledb') {
182 pitrAlert = (
183 <Alert className="rounded-none border-0 border-b px-6">
184 <AlertTitle>PITR not supported</AlertTitle>
185 <AlertDescription>Point in time recovery is not supported with OrioleDB</AlertDescription>
186 </Alert>
187 )
188 }
189
190 return (
191 <PageContainer size="default">
192 <PageSection className="last:pb-0 gap-0">
193 {isBranch && (
194 <Admonition
195 type="default"
196 className="mb-4"
197 title="You are currently on a preview branch of your project"
198 >
199 Updating add-ons here will only apply to this preview branch. To manage add-ons for your
200 main branch, please visit the{' '}
201 <InlineLink href={`/project/${selectedProject.parent_project_ref}/settings/addons`}>
202 main branch
203 </InlineLink>
204 .
205 </Admonition>
206 )}
207
208 {isLoading && (
209 <ResourceList className={listTopSpacing}>
210 {Array.from({ length: 3 }).map((_, index) => (
211 <div
212 key={index}
213 className="flex min-h-[128px] items-center gap-4 border-b px-6 py-4 last:border-b-none"
214 >
215 <div className="hidden @lg:flex h-24 w-40 shrink-0 items-center justify-center rounded-lg border">
216 <div className="shimmering-loader h-full w-full rounded-lg" />
217 </div>
218 <div className="flex-1">
219 <HorizontalShimmerWithIcon />
220 </div>
221 </div>
222 ))}
223 </ResourceList>
224 )}
225
226 {isError && <AlertError error={error} subject="Failed to retrieve project add-ons" />}
227
228 {isSuccess && (
229 <ResourceList className={listTopSpacing}>
230 {projectAddonsDedicatedIpv4Address && (
231 <ResourceItem
232 className={resourceItemClassName}
233 onClick={canOpenIPv4 ? () => setPanel('ipv4') : undefined}
234 media={
235 <Image
236 className="bg rounded-lg border"
237 alt="IPv4"
238 width={160}
239 height={96}
240 src={
241 ipv4Enabled
242 ? `${BASE_PATH}/img/ipv4-on${resolvedTheme?.includes('dark') ? '' : '--light'}.svg?v=2`
243 : `${BASE_PATH}/img/ipv4-off${resolvedTheme?.includes('dark') ? '' : '--light'}.svg?v=2`
244 }
245 />
246 }
247 meta={
248 <div className="flex items-center gap-4">
249 <ProjectUpdateDisabledTooltip
250 projectUpdateDisabled={projectUpdateDisabled}
251 projectNotActive={!isProjectActive}
252 tooltip={ipv4DisabledReason}
253 >
254 {ipv4Enabled ? (
255 <Badge variant="success">Enabled</Badge>
256 ) : (
257 <Badge variant="default">Disabled</Badge>
258 )}
259 </ProjectUpdateDisabledTooltip>
260 </div>
261 }
262 >
263 <div className="space-y-1">
264 <div>Dedicated IPv4 address</div>
265 <p className="m-0 text-foreground-light text-sm">
266 Reserve a dedicated IPv4 address for your project.
267 </p>
268 <InlineLink
269 className="text-foreground-light"
270 href={`${DOCS_URL}/guides/platform/ipv4-address`}
271 onClick={(e) => e.stopPropagation()}
272 >
273 About IPv4 deprecation
274 </InlineLink>
275 </div>
276 </ResourceItem>
277 )}
278
279 <ResourceItem
280 className={resourceItemClassName}
281 onClick={canOpenPITR ? () => setPanel('pitr') : undefined}
282 media={
283 <Image
284 className="bg rounded-lg border"
285 alt="PITR"
286 width={160}
287 height={96}
288 src={
289 pitrEnabled
290 ? `${BASE_PATH}/img/pitr-on${resolvedTheme?.includes('dark') ? '' : '--light'}.svg`
291 : `${BASE_PATH}/img/pitr-off${resolvedTheme?.includes('dark') ? '' : '--light'}.svg`
292 }
293 />
294 }
295 meta={
296 <div className="flex items-center gap-4">
297 {pitrEnabled ? (
298 <Badge variant="success">Enabled</Badge>
299 ) : (
300 <Badge variant="default">Disabled</Badge>
301 )}
302 {!canOpenPITR && pitrDisabledReason && (
303 <Tooltip>
304 <TooltipTrigger>
305 <Lock strokeWidth={1.5} className="text-foreground-light" size={16} />
306 </TooltipTrigger>
307 <TooltipContent>{pitrDisabledReason}</TooltipContent>
308 </Tooltip>
309 )}
310 </div>
311 }
312 >
313 <div className="space-y-1">
314 <div>Point in time recovery</div>
315 <p className="m-0 text-foreground-light text-sm">
316 Restore your database to a specific moment in the past.
317 </p>
318 <InlineLink
319 href={`${DOCS_URL}/guides/platform/backups#point-in-time-recovery`}
320 className="text-foreground-light"
321 onClick={(e) => e.stopPropagation()}
322 >
323 About PITR backups
324 </InlineLink>
325 </div>
326 </ResourceItem>
327
328 {pitrAlert}
329
330 {projectSettingsCustomDomains && (
331 <ResourceItem
332 className={resourceItemClassName}
333 onClick={canOpenCustomDomain ? () => setPanel('customDomain') : undefined}
334 media={
335 <Image
336 className="bg rounded-lg border"
337 alt="Custom Domain"
338 width={160}
339 height={96}
340 src={
341 customDomainEnabled
342 ? `${BASE_PATH}/img/custom-domain-on${
343 resolvedTheme?.includes('dark') ? '' : '--light'
344 }.svg`
345 : `${BASE_PATH}/img/custom-domain-off${
346 resolvedTheme?.includes('dark') ? '' : '--light'
347 }.svg`
348 }
349 />
350 }
351 meta={
352 <div className="flex items-center gap-4">
353 {customDomainEnabled ? (
354 <Badge variant="success">Enabled</Badge>
355 ) : (
356 <Badge variant="default">Disabled</Badge>
357 )}
358 {!canOpenCustomDomain && customDomainDisabledReason && (
359 <Tooltip>
360 <TooltipTrigger>
361 <Lock strokeWidth={1.5} className="text-foreground-light" size={16} />
362 </TooltipTrigger>
363 <TooltipContent>{customDomainDisabledReason}</TooltipContent>
364 </Tooltip>
365 )}
366 </div>
367 }
368 >
369 <div className="space-y-1">
370 <div>Custom domain</div>
371 <p className="m-0 text-foreground-light text-sm">
372 Serve your project on your own domain name.
373 </p>
374 <InlineLink
375 href={`${DOCS_URL}/guides/platform/custom-domains`}
376 className="text-foreground-light"
377 onClick={(e) => e.stopPropagation()}
378 >
379 About custom domains
380 </InlineLink>
381 </div>
382 </ResourceItem>
383 )}
384 </ResourceList>
385 )}
386
387 <PITRSidePanel />
388 <CustomDomainSidePanel />
389 <IPv4SidePanel />
390 </PageSection>
391 </PageContainer>
392 )
393}