Addons.tsx393 lines · main
| 1 | import { SupportCategories } from '@supabase/shared-types/out/constants' |
| 2 | import { useFlag, useParams } from 'common' |
| 3 | import { Lock } from 'lucide-react' |
| 4 | import { useTheme } from 'next-themes' |
| 5 | import Image from 'next/image' |
| 6 | import { |
| 7 | Alert, |
| 8 | AlertDescription, |
| 9 | AlertTitle, |
| 10 | Badge, |
| 11 | Button, |
| 12 | Tooltip, |
| 13 | TooltipContent, |
| 14 | TooltipTrigger, |
| 15 | } from 'ui' |
| 16 | import { Admonition } from 'ui-patterns' |
| 17 | import { PageContainer } from 'ui-patterns/PageContainer' |
| 18 | import { PageSection } from 'ui-patterns/PageSection' |
| 19 | |
| 20 | import { |
| 21 | getCustomDomainDisabledReason, |
| 22 | getIPv4DisabledReason, |
| 23 | getPitrAlertState, |
| 24 | getPitrDisabledReason, |
| 25 | } from './Addons.utils' |
| 26 | import CustomDomainSidePanel from './CustomDomainSidePanel' |
| 27 | import IPv4SidePanel from './IPv4SidePanel' |
| 28 | import PITRSidePanel from './PITRSidePanel' |
| 29 | import { |
| 30 | getAddons, |
| 31 | subscriptionHasHipaaAddon, |
| 32 | } from '@/components/interfaces/Billing/Subscription/Subscription.utils' |
| 33 | import { ProjectUpdateDisabledTooltip } from '@/components/interfaces/Organization/BillingSettings/ProjectUpdateDisabledTooltip' |
| 34 | import { SupportLink } from '@/components/interfaces/Support/SupportLink' |
| 35 | import AlertError from '@/components/ui/AlertError' |
| 36 | import { InlineLink } from '@/components/ui/InlineLink' |
| 37 | import { ResourceItem } from '@/components/ui/Resource/ResourceItem' |
| 38 | import { ResourceList } from '@/components/ui/Resource/ResourceList' |
| 39 | import { HorizontalShimmerWithIcon } from '@/components/ui/Shimmers' |
| 40 | import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query' |
| 41 | import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query' |
| 42 | import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query' |
| 43 | import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' |
| 44 | import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' |
| 45 | import { |
| 46 | useIsAwsCloudProvider, |
| 47 | useIsOrioleDbInAws, |
| 48 | useIsProjectActive, |
| 49 | useSelectedProjectQuery, |
| 50 | } from '@/hooks/misc/useSelectedProject' |
| 51 | import { BASE_PATH, DOCS_URL } from '@/lib/constants' |
| 52 | import { getDatabaseMajorVersion, getSemanticVersion } from '@/lib/helpers' |
| 53 | import { useAddonsPagePanel } from '@/state/addons-page' |
| 54 | |
| 55 | export 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 | } |