Files
supabase/apps/studio/components/interfaces/Database/RestoreToNewProject/RestoreToNewProject.tsx
T
Danny White 4c148ea060 chore(studio): move short Admonitions to descriptions (#46049)
## What kind of change does this PR introduce?

Chore. Follow-up to DEPR-551, #45302, #45535, and #45618.

## What is the current behaviour?

Some short Studio Admonitions still put their entire message in `title`
or legacy `label`, so body-copy callouts render as headings.

## What is the new behaviour?

Moves selected single-message Studio Admonitions to `description`,
keeping the follow-up deliberately limited to Studio callsites.

This PR does not touch Docs content, shared Alert styling, ui-patterns,
design-system registry/docs, or Tailwind config.

| Before | After |
| --- | --- |
| <img width="1818" height="388" alt="Image"
src="https://github.com/user-attachments/assets/283a1853-348a-4d74-a408-013957350e5e"
/> | <img width="1380" height="462" alt="Image"
src="https://github.com/user-attachments/assets/e5761e8e-3697-423b-805b-45110205099a"
/> |
| <img width="1640" height="716" alt="CleanShot 2026-04-28 at 15 17
25@2x"
src="https://github.com/user-attachments/assets/a5be4d5f-2bf7-4dc2-b396-56129fe64ec9"
/> | <img width="1630" height="716" alt="CleanShot 2026-04-28 at 15 16
00@2x"
src="https://github.com/user-attachments/assets/0d589252-aaf8-4efc-9d81-15ec4f99ec61"
/> |


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Style**
* Refined message displays and admonition styling across settings,
database, dashboard, and admin interfaces for improved visual
consistency and clarity.

* **UI Updates**
* Updated search input layouts and form element styling in publications
tables and other admin pages.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46049?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-19 10:20:54 +10:00

315 lines
11 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Loader2 } from 'lucide-react'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { Alert, AlertDescription, AlertTitle, Button } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { PreviousRestoreItem } from './PreviousRestoreItem'
import { PITRForm } from '@/components/interfaces/Database/Backups/PITR/PITRForm'
import { BackupsList } from '@/components/interfaces/Database/Backups/RestoreToNewProject/BackupsList'
import { ConfirmRestoreDialog } from '@/components/interfaces/Database/Backups/RestoreToNewProject/ConfirmRestoreDialog'
import { CreateNewProjectDialog } from '@/components/interfaces/Database/Backups/RestoreToNewProject/CreateNewProjectDialog'
import { projectSpecToMonthlyPrice } from '@/components/interfaces/Database/Backups/RestoreToNewProject/RestoreToNewProject.utils'
import { DiskType } from '@/components/interfaces/DiskManagement/ui/DiskManagement.constants'
import { Markdown } from '@/components/interfaces/Markdown'
import AlertError from '@/components/ui/AlertError'
import { InlineLink } from '@/components/ui/InlineLink'
import NoPermission from '@/components/ui/NoPermission'
import Panel from '@/components/ui/Panel'
import { UpgradeToPro } from '@/components/ui/UpgradeToPro'
import { useDiskAttributesQuery } from '@/data/config/disk-attributes-query'
import { useCloneBackupsQuery } from '@/data/projects/clone-query'
import { useCloneStatusQuery } from '@/data/projects/clone-status-query'
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import {
useIsAwsK8sCloudProvider,
useIsOrioleDb,
useSelectedProjectQuery,
} from '@/hooks/misc/useSelectedProject'
import { DOCS_URL, PROJECT_STATUS } from '@/lib/constants'
import { getDatabaseMajorVersion } from '@/lib/helpers'
export const RestoreToNewProject = () => {
const { data: project } = useSelectedProjectQuery()
const { data: organization } = useSelectedOrganizationQuery()
const { hasAccess: hasAccessToRestoreToNewProject, isLoading: isLoadingEntitlement } =
useCheckEntitlements('backup.restore_to_new_project')
const isOrioleDb = useIsOrioleDb()
const isAwsK8s = useIsAwsK8sCloudProvider()
const [refetchInterval, setRefetchInterval] = useState<number | false>(false)
const [selectedBackupId, setSelectedBackupId] = useState<number | null>(null)
const [showConfirmationDialog, setShowConfirmationDialog] = useState(false)
const [showNewProjectDialog, setShowNewProjectDialog] = useState(false)
const [recoveryTimeTarget, setRecoveryTimeTarget] = useState<number | null>(null)
const {
data: cloneBackups,
error,
isPending: cloneBackupsLoading,
isError,
} = useCloneBackupsQuery(
{ projectRef: project?.ref },
{ enabled: hasAccessToRestoreToNewProject }
)
const isActiveHealthy = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY
const { can: canReadPhysicalBackups, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
PermissionAction.READ,
'physical_backups'
)
const { can: canTriggerPhysicalBackups } = useAsyncCheckPermissions(
PermissionAction.INFRA_EXECUTE,
'queue_job.restore.prepare'
)
const PITR_ENABLED = cloneBackups?.pitr_enabled
const PHYSICAL_BACKUPS_ENABLED = project?.is_physical_backups_enabled
const dbVersion = getDatabaseMajorVersion(project?.dbVersion ?? '')
const IS_PG15_OR_ABOVE = dbVersion >= 15
const targetVolumeSizeGb = cloneBackups?.target_volume_size_gb
const targetComputeSize = cloneBackups?.target_compute_size
const planId = organization?.plan?.id ?? 'free'
const { data } = useDiskAttributesQuery({ projectRef: project?.ref })
const storageType = data?.attributes?.type ?? 'gp3'
const {
data: cloneStatus,
refetch: refetchCloneStatus,
isPending: cloneStatusLoading,
isSuccess: isCloneStatusSuccess,
} = useCloneStatusQuery(
{
projectRef: project?.ref,
},
{
refetchInterval,
refetchOnWindowFocus: false,
enabled: PHYSICAL_BACKUPS_ENABLED || PITR_ENABLED,
}
)
const isLoading = !isPermissionsLoaded || cloneBackupsLoading || cloneStatusLoading
useEffect(() => {
if (!isCloneStatusSuccess) return
const hasTransientState = cloneStatus.clones.some((c) => c.status === 'IN_PROGRESS')
if (!hasTransientState) {
setRefetchInterval(false)
}
}, [cloneStatus?.clones, isCloneStatusSuccess])
const previousClones = cloneStatus?.clones
const isRestoring = previousClones?.some((c) => c.status === 'IN_PROGRESS')
const restoringClone = previousClones?.find((c) => c.status === 'IN_PROGRESS')
if (isLoadingEntitlement) {
return <GenericSkeletonLoader />
}
if (!hasAccessToRestoreToNewProject) {
return (
<UpgradeToPro
buttonText="Upgrade"
source="backupsRestoreToNewProject"
featureProposition="enable restoring to new project"
primaryText="Restore to a new project requires Pro Plan and above"
secondaryText="To restore to a new project, you need to upgrade to a Pro Plan and have physical backups enabled."
/>
)
}
if (isOrioleDb) {
return (
<Admonition
type="default"
title="Restoring to new projects are not available for OrioleDB"
description="OrioleDB is currently in public alpha and projects created are strictly ephemeral with no database backups"
/>
)
}
if (isAwsK8s) {
return (
<Admonition
type="default"
description="Restoring to new projects is temporarily not available for AWS (Revamped) projects."
/>
)
}
if (!canReadPhysicalBackups) {
return <NoPermission resourceText="view backups" />
}
if (!canTriggerPhysicalBackups) {
return <NoPermission resourceText="restore backups" />
}
if (!IS_PG15_OR_ABOVE) {
return (
<Admonition
type="default"
title="Restore to new project is not available for this database version"
>
<Markdown
className="max-w-full"
content={`Restore to new project is only available for Postgres 15 and above.
Go to [infrastructure settings](/project/${project?.ref}/settings/infrastructure)
to upgrade your database version.
`}
/>
</Admonition>
)
}
if (!PHYSICAL_BACKUPS_ENABLED) {
return (
<Admonition
type="default"
title="Physical backups are required"
description={
<>
Physical backups must be enabled to restore your database to a new project.{' '}
<InlineLink href={`${DOCS_URL}/guides/platform/backups`}>Learn more</InlineLink>
</>
}
/>
)
}
if (isLoading) {
return <GenericSkeletonLoader />
}
if (isError) {
return <AlertError error={error} subject="Failed to retrieve backups" />
}
if (!isActiveHealthy) {
return (
<Admonition
type="default"
title="Restore to new project is not available while project is offline"
description="Your project needs to be online to restore your database to a new project"
/>
)
}
if (
!isLoading &&
PITR_ENABLED &&
!cloneBackups?.physicalBackupData.earliestPhysicalBackupDateUnix
) {
return (
<Admonition
type="default"
title="No backups found"
description="PITR is enabled, but no backups were found. Check again in a few minutes."
/>
)
}
if (!isLoading && !PITR_ENABLED && cloneBackups?.backups.length === 0) {
return (
<>
<Admonition
type="default"
title="No backups found"
description="Backups are enabled, but no backups were found. Check again tomorrow."
/>
</>
)
}
const additionalMonthlySpend = projectSpecToMonthlyPrice({
targetVolumeSizeGb: targetVolumeSizeGb ?? 0,
targetComputeSize: targetComputeSize ?? 'nano',
planId: planId ?? 'free',
storageType: storageType as DiskType,
})
return (
<div className="flex flex-col gap-4">
<ConfirmRestoreDialog
open={showConfirmationDialog}
onOpenChange={setShowConfirmationDialog}
onSelectContinue={() => {
setShowConfirmationDialog(false)
setShowNewProjectDialog(true)
}}
additionalMonthlySpend={additionalMonthlySpend}
/>
<CreateNewProjectDialog
open={showNewProjectDialog}
selectedBackupId={selectedBackupId}
recoveryTimeTarget={recoveryTimeTarget}
additionalMonthlySpend={additionalMonthlySpend}
hasAccess={hasAccessToRestoreToNewProject}
onOpenChange={setShowNewProjectDialog}
onCloneSuccess={() => {
refetchCloneStatus()
setRefetchInterval(5000)
setShowNewProjectDialog(false)
}}
/>
{isRestoring ? (
<Alert className="[&>svg]:bg-none! [&>svg]:text-foreground-light mb-6">
<Loader2 className="animate-spin" />
<AlertTitle>Restoration in progress</AlertTitle>
<AlertDescription>
<p>
The new project {(restoringClone?.target_project as any)?.name || ''} is currently
being created. You'll be able to restore again once the project is ready.
</p>
<Button asChild type="default" className="mt-2">
<Link href={`/project/${restoringClone?.target_project?.ref ?? '_'}`}>
Go to new project
</Link>
</Button>
</AlertDescription>
</Alert>
) : null}
{previousClones?.length ? (
<div className="flex flex-col gap-2">
<h3 className="text-sm font-medium">Previous restorations</h3>
<Panel className="flex flex-col divide-y divide-border">
{previousClones?.map((c) => (
<PreviousRestoreItem key={c.inserted_at} clone={c} />
))}
</Panel>
</div>
) : null}
{PITR_ENABLED ? (
<>
<PITRForm
disabled={isRestoring}
onSubmit={(v) => {
setShowConfirmationDialog(true)
setRecoveryTimeTarget(v.recoveryTimeTargetUnix)
}}
earliestAvailableBackupUnix={
cloneBackups?.physicalBackupData.earliestPhysicalBackupDateUnix || 0
}
latestAvailableBackupUnix={
cloneBackups?.physicalBackupData.latestPhysicalBackupDateUnix || 0
}
/>
</>
) : (
<BackupsList
disabled={isRestoring}
hasAccess={hasAccessToRestoreToNewProject}
onSelectRestore={(id) => {
setSelectedBackupId(id)
setShowConfirmationDialog(true)
}}
/>
)}
</div>
)
}