Addons.tsx 14 KB

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