mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 10:55:06 +03:00
Studio-side guard for Multigres (`high_availability`) projects, mirroring the platform API guard from supabase/platform#37527. Pause, restore/PITR, and add-on affordances now show a clear "unavailable on High Availability projects" state instead of failing with a 400 after the click. <img width="1195" height="632" alt="Screenshot 2026-09-04 at 2 03 11 PM" src="https://github.com/user-attachments/assets/718c09f2-d92b-49dc-90ed-5d9ff810b03d" /> **Added:** - Pause project button is disabled on HA projects with a tooltip - Scheduled backups tab short-circuits to an HA empty state (matches the existing PITR tab). Per-row Restore buttons are also disabled with a tooltip as defense in depth, since BackupItem is reusable - Restore to new project shows an HA admonition ahead of the permission / PG15 / physical-backup checks - Add-ons page shows a page-level HA notice, all three rows are locked with a tooltip, and the side panels are not mounted on HA so `?panel=pitr|ipv4|customDomain` deep links are inert - Component tests for `PauseProjectButton` and `BackupItem`, plus unit tests for the new `isHighAvailability` branch in `Addons.utils.ts` **Changed:** - Add-ons rows are now consistent: the IPv4 row uses the same padlock tooltip as PITR and custom domain instead of a tooltip on the badge. Same disabled-reason strings as before, just surfaced via the padlock on non-HA projects too - `BackupItem` tooltip text extracted into a `getTooltipText()` function (mirrors `PauseProjectButton`) - `HighAvailabilityDisabledSectionNotice` accepts a `className` Detection reuses the existing `useIsHighAvailability()` hook, which the rest of Studio already treats as the Multigres signal. ## To test Use an HA project (`project.high_availability === true`) and a normal project. HA project: - Settings > General: "Pause project" is disabled, tooltip reads "Pausing is unavailable on High Availability projects" - Database > Backups > Scheduled backups: HA empty state, no "No backups yet" / daily backup copy - Database > Backups > Restore to new project: HA admonition, no restore controls - Settings > Add-ons: notice at the top, padlock on all three rows with per-row tooltip, clicking rows does nothing, and `?panel=pitr` / `?panel=ipv4` / `?panel=customDomain` open nothing Normal project (regression): - No "High Availability" strings on any of the above pages - Pause button enabled (or disabled only for its usual reasons, e.g. paid plan) - Add-on rows open their side panels on click and via `?panel=pitr` - Scheduled backups tab shows its normal list / empty state <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Features** - High Availability projects now clearly indicate when scheduled backups, backup restoration, project pausing, IPv4, PITR, and custom domains are unavailable. - Added explanatory notices, disabled controls, and tooltips throughout affected settings and backup screens. - Restore-to-new-project workflows now provide guidance to contact support when unavailable. - **Bug Fixes** - Improved consistency of availability messaging across High Availability project settings and database backup actions. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
415 lines
15 KiB
TypeScript
415 lines
15 KiB
TypeScript
import { SupportCategories } from '@supabase/shared-types/out/constants'
|
|
import { useFlag, useParams } from 'common'
|
|
import { Lock } from 'lucide-react'
|
|
import { useTheme } from 'next-themes'
|
|
import Image from 'next/image'
|
|
import {
|
|
Alert,
|
|
AlertDescription,
|
|
AlertTitle,
|
|
Badge,
|
|
Button,
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from 'ui'
|
|
import { Admonition } from 'ui-patterns/Admonition'
|
|
import { PageContainer } from 'ui-patterns/PageContainer'
|
|
import { PageSection } from 'ui-patterns/PageSection'
|
|
|
|
import {
|
|
getCustomDomainDisabledReason,
|
|
getIPv4DisabledReason,
|
|
getPitrAlertState,
|
|
getPitrDisabledReason,
|
|
} from './Addons.utils'
|
|
import CustomDomainSidePanel from './CustomDomainSidePanel'
|
|
import IPv4SidePanel from './IPv4SidePanel'
|
|
import PITRSidePanel from './PITRSidePanel'
|
|
import {
|
|
getAddons,
|
|
subscriptionHasHipaaAddon,
|
|
} from '@/components/interfaces/Billing/Subscription/Subscription.utils'
|
|
import { SupportLink } from '@/components/interfaces/Support/SupportLink'
|
|
import { AlertError } from '@/components/ui/AlertError'
|
|
import { HighAvailabilityDisabledSectionNotice } from '@/components/ui/HighAvailability/HighAvailabilityDisabledSectionNotice'
|
|
import { InlineLink } from '@/components/ui/InlineLink'
|
|
import { ResourceItem } from '@/components/ui/Resource/ResourceItem'
|
|
import { ResourceList } from '@/components/ui/Resource/ResourceList'
|
|
import { HorizontalShimmerWithIcon } from '@/components/ui/Shimmers'
|
|
import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
|
|
import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
|
|
import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
|
|
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
|
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
|
import {
|
|
useIsAwsCloudProvider,
|
|
useIsHighAvailability,
|
|
useIsOrioleDbInAws,
|
|
useIsProjectActive,
|
|
useSelectedProjectQuery,
|
|
} from '@/hooks/misc/useSelectedProject'
|
|
import { BASE_PATH, DOCS_URL } from '@/lib/constants'
|
|
import { getDatabaseMajorVersion, getSemanticVersion } from '@/lib/helpers'
|
|
import { useAddonsPagePanel } from '@/state/addons-page'
|
|
|
|
export const Addons = () => {
|
|
const { resolvedTheme } = useTheme()
|
|
const { ref: projectRef } = useParams()
|
|
const { setPanel } = useAddonsPagePanel()
|
|
const isAws = useIsAwsCloudProvider()
|
|
const isProjectActive = useIsProjectActive()
|
|
const isOrioleDbInAws = useIsOrioleDbInAws() === true
|
|
const isHighAvailability = useIsHighAvailability()
|
|
|
|
const { projectSettingsCustomDomains, projectAddonsDedicatedIpv4Address } = useIsFeatureEnabled([
|
|
'project_settings:custom_domains',
|
|
'project_addons:dedicated_ipv4_address',
|
|
])
|
|
|
|
const { data: selectedOrg } = useSelectedOrganizationQuery()
|
|
const { data: selectedProject } = useSelectedProjectQuery()
|
|
const isBranch = selectedProject?.parent_project_ref
|
|
|
|
const { data: settings } = useProjectSettingsV2Query({ projectRef })
|
|
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: selectedOrg?.slug })
|
|
|
|
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
|
|
const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) && settings?.is_sensitive === true
|
|
|
|
// Only projects of version greater than supabase-postgrest-14.1.0.44 can use PITR
|
|
const sufficientPgVersion =
|
|
// introduced as generatedSemantic version could be < 141044 even if actual version is indeed past it
|
|
// eg. 15.1.1.2 leads to 15112
|
|
getDatabaseMajorVersion(selectedProject?.dbVersion ?? '') > 14 ||
|
|
getSemanticVersion(selectedProject?.dbVersion ?? '') >= 141044
|
|
|
|
const {
|
|
data: addons,
|
|
error,
|
|
isPending: isLoading,
|
|
isError,
|
|
isSuccess,
|
|
} = useProjectAddonsQuery({ projectRef })
|
|
|
|
const selectedAddons = addons?.selected_addons ?? []
|
|
const { pitr, customDomain, ipv4 } = getAddons(selectedAddons)
|
|
|
|
const canUpdateIPv4 = settings?.db_ip_addr_config === 'ipv6'
|
|
|
|
const ipv4Enabled = ipv4 !== undefined
|
|
const pitrEnabled = pitr !== undefined
|
|
const customDomainEnabled = customDomain !== undefined
|
|
|
|
const canOpenIPv4 =
|
|
isAws &&
|
|
isProjectActive &&
|
|
!projectUpdateDisabled &&
|
|
(canUpdateIPv4 || ipv4Enabled) &&
|
|
!isHighAvailability
|
|
const canOpenPITR =
|
|
isProjectActive &&
|
|
!projectUpdateDisabled &&
|
|
sufficientPgVersion &&
|
|
!hasHipaaAddon &&
|
|
!isOrioleDbInAws &&
|
|
!isHighAvailability
|
|
const canOpenCustomDomain = isProjectActive && !projectUpdateDisabled && !isHighAvailability
|
|
|
|
const ipv4DisabledReason = getIPv4DisabledReason({
|
|
isAws,
|
|
isProjectActive,
|
|
projectUpdateDisabled,
|
|
canUpdateIPv4,
|
|
ipv4Enabled,
|
|
isHighAvailability,
|
|
})
|
|
|
|
const pitrDisabledReason = getPitrDisabledReason({
|
|
isProjectActive,
|
|
projectUpdateDisabled,
|
|
hasHipaaAddon,
|
|
sufficientPgVersion,
|
|
isOrioleDbInAws,
|
|
isHighAvailability,
|
|
})
|
|
|
|
const customDomainDisabledReason = getCustomDomainDisabledReason({
|
|
isProjectActive,
|
|
projectUpdateDisabled,
|
|
isHighAvailability,
|
|
})
|
|
const pitrAlertState = getPitrAlertState({
|
|
hasHipaaAddon,
|
|
sufficientPgVersion,
|
|
isOrioleDbInAws,
|
|
})
|
|
|
|
const listTopSpacing = isBranch ? 'mt-6' : undefined
|
|
const resourceItemClassName =
|
|
'min-h-[128px] border-b! last:border-b-0! [&>div:first-child]:hidden @lg:[&>div:first-child]:flex'
|
|
|
|
let pitrAlert = null
|
|
|
|
if (pitrAlertState === 'hipaa') {
|
|
pitrAlert = (
|
|
<Alert className="rounded-none border-0 border-b px-6">
|
|
<AlertTitle>PITR cannot be changed with HIPAA</AlertTitle>
|
|
<AlertDescription>
|
|
All projects should have PITR enabled by default and cannot be changed with HIPAA enabled.
|
|
Contact support for further assistance.
|
|
</AlertDescription>
|
|
<div className="mt-4">
|
|
<Button variant="default" asChild>
|
|
<SupportLink>Contact support</SupportLink>
|
|
</Button>
|
|
</div>
|
|
</Alert>
|
|
)
|
|
} else if (pitrAlertState === 'legacy-project') {
|
|
pitrAlert = (
|
|
<Alert className="rounded-none border-0 border-b px-6">
|
|
<AlertTitle>Your project is too old to enable PITR</AlertTitle>
|
|
<AlertDescription>
|
|
<p className="text-sm leading-normal mb-2">
|
|
Reach out to us via support if you're interested
|
|
</p>
|
|
<Button asChild variant="default">
|
|
<SupportLink
|
|
queryParams={{
|
|
projectRef,
|
|
category: SupportCategories.SALES_ENQUIRY,
|
|
subject: 'Project too old old for PITR',
|
|
}}
|
|
>
|
|
Contact support
|
|
</SupportLink>
|
|
</Button>
|
|
</AlertDescription>
|
|
</Alert>
|
|
)
|
|
} else if (pitrAlertState === 'orioledb') {
|
|
pitrAlert = (
|
|
<Alert className="rounded-none border-0 border-b px-6">
|
|
<AlertTitle>PITR not supported</AlertTitle>
|
|
<AlertDescription>Point in time recovery is not supported with OrioleDB</AlertDescription>
|
|
</Alert>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<PageContainer size="default">
|
|
<PageSection className="last:pb-0 gap-0">
|
|
<HighAvailabilityDisabledSectionNotice
|
|
className="mb-4"
|
|
title="Add-ons unavailable on High Availability projects"
|
|
description="We're working to bring add-ons to High Availability projects. Contact support if this is blocking your work."
|
|
/>
|
|
{isBranch && (
|
|
<Admonition
|
|
type="default"
|
|
className="mb-4"
|
|
title="You are currently on a preview branch of your project"
|
|
>
|
|
Updating add-ons here will only apply to this preview branch. To manage add-ons for your
|
|
main branch, please visit the{' '}
|
|
<InlineLink href={`/project/${selectedProject.parent_project_ref}/settings/addons`}>
|
|
main branch
|
|
</InlineLink>
|
|
.
|
|
</Admonition>
|
|
)}
|
|
|
|
{isLoading && (
|
|
<ResourceList className={listTopSpacing}>
|
|
{Array.from({ length: 3 }).map((_, index) => (
|
|
<div
|
|
key={index}
|
|
className="flex min-h-[128px] items-center gap-4 border-b px-6 py-4 last:border-b-none"
|
|
>
|
|
<div className="hidden @lg:flex h-24 w-40 shrink-0 items-center justify-center rounded-lg border">
|
|
<div className="shimmering-loader h-full w-full rounded-lg" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<HorizontalShimmerWithIcon />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</ResourceList>
|
|
)}
|
|
|
|
{isError && <AlertError error={error} subject="Failed to retrieve project add-ons" />}
|
|
|
|
{isSuccess && (
|
|
<ResourceList className={listTopSpacing}>
|
|
{projectAddonsDedicatedIpv4Address && (
|
|
<ResourceItem
|
|
className={resourceItemClassName}
|
|
onClick={canOpenIPv4 ? () => setPanel('ipv4') : undefined}
|
|
media={
|
|
<Image
|
|
className="bg rounded-lg border"
|
|
alt="IPv4"
|
|
width={160}
|
|
height={96}
|
|
src={
|
|
ipv4Enabled
|
|
? `${BASE_PATH}/img/ipv4-on${resolvedTheme?.includes('dark') ? '' : '--light'}.svg?v=2`
|
|
: `${BASE_PATH}/img/ipv4-off${resolvedTheme?.includes('dark') ? '' : '--light'}.svg?v=2`
|
|
}
|
|
/>
|
|
}
|
|
meta={
|
|
<div className="flex items-center gap-4">
|
|
{ipv4Enabled ? (
|
|
<Badge variant="success">Enabled</Badge>
|
|
) : (
|
|
<Badge variant="default">Disabled</Badge>
|
|
)}
|
|
{!canOpenIPv4 && ipv4DisabledReason && (
|
|
<Tooltip>
|
|
<TooltipTrigger>
|
|
<Lock strokeWidth={1.5} className="text-foreground-light" size={16} />
|
|
</TooltipTrigger>
|
|
<TooltipContent>{ipv4DisabledReason}</TooltipContent>
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
}
|
|
>
|
|
<div className="space-y-1">
|
|
<div>Dedicated IPv4 address</div>
|
|
<p className="m-0 text-foreground-light text-sm">
|
|
Reserve a dedicated IPv4 address for your project.
|
|
</p>
|
|
<InlineLink
|
|
className="text-foreground-light"
|
|
href={`${DOCS_URL}/guides/platform/ipv4-address`}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
About IPv4 deprecation
|
|
</InlineLink>
|
|
</div>
|
|
</ResourceItem>
|
|
)}
|
|
|
|
<ResourceItem
|
|
className={resourceItemClassName}
|
|
onClick={canOpenPITR ? () => setPanel('pitr') : undefined}
|
|
media={
|
|
<Image
|
|
className="bg rounded-lg border"
|
|
alt="PITR"
|
|
width={160}
|
|
height={96}
|
|
src={
|
|
pitrEnabled
|
|
? `${BASE_PATH}/img/pitr-on${resolvedTheme?.includes('dark') ? '' : '--light'}.svg`
|
|
: `${BASE_PATH}/img/pitr-off${resolvedTheme?.includes('dark') ? '' : '--light'}.svg`
|
|
}
|
|
/>
|
|
}
|
|
meta={
|
|
<div className="flex items-center gap-4">
|
|
{pitrEnabled ? (
|
|
<Badge variant="success">Enabled</Badge>
|
|
) : (
|
|
<Badge variant="default">Disabled</Badge>
|
|
)}
|
|
{!canOpenPITR && pitrDisabledReason && (
|
|
<Tooltip>
|
|
<TooltipTrigger>
|
|
<Lock strokeWidth={1.5} className="text-foreground-light" size={16} />
|
|
</TooltipTrigger>
|
|
<TooltipContent>{pitrDisabledReason}</TooltipContent>
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
}
|
|
>
|
|
<div className="space-y-1">
|
|
<div>Point in time recovery</div>
|
|
<p className="m-0 text-foreground-light text-sm">
|
|
Restore your database to a specific moment in the past.
|
|
</p>
|
|
<InlineLink
|
|
href={`${DOCS_URL}/guides/platform/backups#point-in-time-recovery`}
|
|
className="text-foreground-light"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
About PITR backups
|
|
</InlineLink>
|
|
</div>
|
|
</ResourceItem>
|
|
|
|
{pitrAlert}
|
|
|
|
{projectSettingsCustomDomains && (
|
|
<ResourceItem
|
|
className={resourceItemClassName}
|
|
onClick={canOpenCustomDomain ? () => setPanel('customDomain') : undefined}
|
|
media={
|
|
<Image
|
|
className="bg rounded-lg border"
|
|
alt="Custom Domain"
|
|
width={160}
|
|
height={96}
|
|
src={
|
|
customDomainEnabled
|
|
? `${BASE_PATH}/img/custom-domain-on${
|
|
resolvedTheme?.includes('dark') ? '' : '--light'
|
|
}.svg`
|
|
: `${BASE_PATH}/img/custom-domain-off${
|
|
resolvedTheme?.includes('dark') ? '' : '--light'
|
|
}.svg`
|
|
}
|
|
/>
|
|
}
|
|
meta={
|
|
<div className="flex items-center gap-4">
|
|
{customDomainEnabled ? (
|
|
<Badge variant="success">Enabled</Badge>
|
|
) : (
|
|
<Badge variant="default">Disabled</Badge>
|
|
)}
|
|
{!canOpenCustomDomain && customDomainDisabledReason && (
|
|
<Tooltip>
|
|
<TooltipTrigger>
|
|
<Lock strokeWidth={1.5} className="text-foreground-light" size={16} />
|
|
</TooltipTrigger>
|
|
<TooltipContent>{customDomainDisabledReason}</TooltipContent>
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
}
|
|
>
|
|
<div className="space-y-1">
|
|
<div>Custom domain</div>
|
|
<p className="m-0 text-foreground-light text-sm">
|
|
Serve your project on your own domain name.
|
|
</p>
|
|
<InlineLink
|
|
href={`${DOCS_URL}/guides/platform/custom-domains`}
|
|
className="text-foreground-light"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
About custom domains
|
|
</InlineLink>
|
|
</div>
|
|
</ResourceItem>
|
|
)}
|
|
</ResourceList>
|
|
)}
|
|
|
|
{!isHighAvailability && (
|
|
<>
|
|
<PITRSidePanel />
|
|
<CustomDomainSidePanel />
|
|
<IPv4SidePanel />
|
|
</>
|
|
)}
|
|
</PageSection>
|
|
</PageContainer>
|
|
)
|
|
}
|