mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
feat: paused project restore window copy and backup downloads (#48279)
Shows the restore deadline as a date on the paused-project screens, and offers backup downloads while a paused project is still restorable (previously only after the restore window ended). Depends on a backend change — keep as draft until that is live. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Extended the paused-project restore window from 90 days to up to 1 year. * Added clearer, downloadable options for database backups and storage objects while a project is paused. * Paused-project screens now show an “available until” date when applicable (and updated resume guidance). * **Documentation** * Updated platform and troubleshooting guides to reflect the new 1-year restore window and post-window recovery limitations. * **Bug Fixes** * Standardized restore-window wording across the pause confirmation and paused-state UI. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
34e2585756
commit
8c092185ae
10 files changed
+244
-228
No files matched your search
@@ -28,7 +28,7 @@ After receiving an initial email warning, the pause can be prevented by taking o
|
||||
|
||||
## Restoring a paused project
|
||||
|
||||
You can restore a paused project for up to 90 days after it was paused:
|
||||
You can restore a paused project for up to 1 year after it was paused:
|
||||
|
||||
1. Open the [Supabase Dashboard](/dashboard/organizations)
|
||||
2. Select the organization, followed by the paused project
|
||||
@@ -36,9 +36,9 @@ You can restore a paused project for up to 90 days after it was paused:
|
||||
|
||||
The project will return to its previous state, including data and configurations.
|
||||
|
||||
### 90-day window to restore
|
||||
### Restore window [#90-day-window-to-restore]
|
||||
|
||||
Once the project is paused, there is a 90-day window to restore the project on the platform from within Supabase Studio. The 90-day window allows Supabase to introduce platform changes that may not be backward compatible with older backups. Unlike active projects, static backups can't be updated to accommodate such changes.
|
||||
Once the project is paused, there is a 1-year window to restore the project on the platform from within Supabase Studio. The time limit exists because backups are only retained for a limited period, and platform changes may not be backward compatible with older backups. Unlike active projects, static backups can't be updated to accommodate such changes.
|
||||
|
||||
## Preventing automatic project pausing
|
||||
|
||||
|
||||
@@ -94,11 +94,11 @@ When upgrading, the Supabase platform will "right-size" your disk based on the c
|
||||
|
||||
### Time limits
|
||||
|
||||
Starting from 2024-06-24, when a project is paused, users then have a 90-day window to restore the project on the platform from within Supabase Studio.
|
||||
When a project is paused, users have a 1-year window to restore the project on the platform from within Supabase Studio.
|
||||
|
||||
The 90-day window allows Supabase to introduce platform changes that may not be backwards compatible with older backups. Unlike active projects, static backups can't be updated to accommodate such changes.
|
||||
The restore window exists because backups are only retained for a limited period, and platform changes may not be backwards compatible with older backups. Unlike active projects, static backups can't be updated to accommodate such changes.
|
||||
|
||||
During the 90-day restore window a paused project can be restored to the platform with a single button click from [Studio's dashboard page](/dashboard/projects).
|
||||
During the restore window a paused project can be restored to the platform with a single button click from [Studio's dashboard page](/dashboard/projects).
|
||||
|
||||
<Image
|
||||
alt="Project Paused: 90 Days Remaining"
|
||||
@@ -107,7 +107,7 @@ During the 90-day restore window a paused project can be restored to the platfor
|
||||
src="/docs/img/guides/platform/paused-90-day.png"
|
||||
/>
|
||||
|
||||
After the 90-day restore window, you can download your project's backup file, and Storage objects from the project dashboard. You can restore the data in the following ways:
|
||||
After the restore window, you can download your project's backup file, and Storage objects from the project dashboard. You can restore the data in the following ways:
|
||||
|
||||
- [Restore a backup to a new Supabase project](/docs/guides/platform/migrating-within-supabase/dashboard-restore)
|
||||
- [Restore a backup locally](/docs/guides/local-development/restoring-downloaded-backup)
|
||||
@@ -119,7 +119,7 @@ After the 90-day restore window, you can download your project's backup file, an
|
||||
height={306}
|
||||
/>
|
||||
|
||||
If you upgrade to a paid plan while your project is paused within the 90-day restore window, any expired one-click restore options are reenabled. Since the backup was taken outside the backwards compatibility window, it may fail to restore. If you have a problem restoring your backup after upgrading, contact [Support](/support).
|
||||
If you upgrade to a paid plan while your project is paused, any expired one-click restore options are reenabled. Since the backup was taken outside the backwards compatibility window, it may fail to restore. If you have a problem restoring your backup after upgrading, contact [Support](/support).
|
||||
|
||||
<Image
|
||||
alt="Project Paused: Paid Tier Restore"
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
---
|
||||
title = "How To Restore Project After 90-Day Pause"
|
||||
title = "How To Restore a Project Paused for More Than 1 Year"
|
||||
topics = [ "platform" ]
|
||||
keywords = [ "pause", "restore", "90 days", "project", "backup", "migration", "storage" ]
|
||||
keywords = [ "pause", "restore", "1 year", "90 days", "project", "backup", "migration", "storage" ]
|
||||
---
|
||||
|
||||
Projects paused for more than 90 days can no longer be restored through Supabase Studio. You can still recover your data by downloading the available backups and migrating them to a new project.
|
||||
Projects paused for more than 1 year can no longer be restored through Supabase Studio. You can still recover your data by downloading the available backups and migrating them to a new project.
|
||||
|
||||
Both the database backup and Storage objects can be downloaded from the [Project Overview](/dashboard/project/_) section in Supabase Studio before the project is deleted.
|
||||
|
||||
<Image
|
||||
alt="Restore project after 90-day pause"
|
||||
alt="Restore project after pause"
|
||||
src={{
|
||||
dark: '/docs/img/restore-after-90-day-dark.png',
|
||||
light: '/docs/img/restore-after-90-day-light.png',
|
||||
|
||||
+1
-1
@@ -110,7 +110,7 @@ export const PauseProjectButton = () => {
|
||||
<AlertDialogTitle>Pause {entityLabel}?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This {entityLabel} will be unavailable while paused. Paused {entityLabel} can be
|
||||
resumed for 90 days. After that, backups remain available to download.
|
||||
resumed for up to 1 year. After that, backups remain available to download.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { useParams } from 'common'
|
||||
import { Database, Storage } from 'icons'
|
||||
import { ChevronDown, Download } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from 'ui'
|
||||
|
||||
import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip'
|
||||
import { useBackupDownloadMutation } from '@/data/database/backup-download-mutation'
|
||||
import { useProjectPauseStatusQuery } from '@/data/projects/project-pause-status-query'
|
||||
import { useStorageArchiveCreateMutation } from '@/data/storage/storage-archive-create-mutation'
|
||||
import { useStorageArchiveQuery } from '@/data/storage/storage-archive-query'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { PROJECT_STATUS } from '@/lib/constants'
|
||||
|
||||
export const DownloadBackupsSection = () => {
|
||||
const { ref } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const [toastId, setToastId] = useState<string | number>()
|
||||
const [refetchInterval, setRefetchInterval] = useState<number | false>(false)
|
||||
|
||||
const dbVersion = project?.dbVersion?.replace('supabase-postgres-', '')
|
||||
|
||||
const { data: pauseStatus } = useProjectPauseStatusQuery(
|
||||
{ ref },
|
||||
{ enabled: project?.status === PROJECT_STATUS.INACTIVE }
|
||||
)
|
||||
const latestBackup = pauseStatus?.latest_downloadable_backup_id
|
||||
|
||||
const { data: storageArchive, isSuccess: isStorageArchiveSuccess } = useStorageArchiveQuery(
|
||||
{ projectRef: ref },
|
||||
{
|
||||
refetchInterval,
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isStorageArchiveSuccess) return
|
||||
if (storageArchive.fileUrl && refetchInterval !== false) {
|
||||
toast.success('Downloading storage objects', { id: toastId })
|
||||
setToastId(undefined)
|
||||
setRefetchInterval(false)
|
||||
downloadStorageArchive(storageArchive.fileUrl)
|
||||
}
|
||||
}, [isStorageArchiveSuccess, storageArchive, refetchInterval])
|
||||
|
||||
const storageArchiveUrl = storageArchive?.fileUrl
|
||||
|
||||
const { mutate: downloadBackup } = useBackupDownloadMutation({
|
||||
onSuccess: (res) => {
|
||||
const { fileUrl } = res
|
||||
|
||||
// Trigger browser download by create,trigger and remove tempLink
|
||||
const tempLink = document.createElement('a')
|
||||
tempLink.href = fileUrl
|
||||
document.body.appendChild(tempLink)
|
||||
tempLink.click()
|
||||
document.body.removeChild(tempLink)
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate: createStorageArchive } = useStorageArchiveCreateMutation({
|
||||
onSuccess: () => {
|
||||
const toastId = toast.loading(
|
||||
'Retrieving storage archive. This may take a few minutes depending on the size of your storage objects.'
|
||||
)
|
||||
setToastId(toastId)
|
||||
setRefetchInterval(5000)
|
||||
},
|
||||
})
|
||||
|
||||
const onSelectDownloadBackup = () => {
|
||||
if (ref === undefined) return console.error('Project ref is required')
|
||||
if (!latestBackup) return toast.error('No backups available for download')
|
||||
|
||||
const toastId = toast.loading('Fetching database backup')
|
||||
|
||||
downloadBackup(
|
||||
{
|
||||
ref,
|
||||
backup: {
|
||||
id: latestBackup,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success('Downloading database backup', { id: toastId })
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const downloadStorageArchive = (url: string) => {
|
||||
const tempLink = document.createElement('a')
|
||||
tempLink.href = url
|
||||
document.body.appendChild(tempLink)
|
||||
tempLink.click()
|
||||
document.body.removeChild(tempLink)
|
||||
}
|
||||
|
||||
const onSelectDownloadStorageArchive = () => {
|
||||
if (!storageArchiveUrl) {
|
||||
createStorageArchive({ projectRef: ref })
|
||||
} else {
|
||||
toast.success('Downloading storage objects')
|
||||
downloadStorageArchive(storageArchiveUrl)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-t flex flex-col gap-3 sm:flex-row sm:justify-between sm:items-center px-6 py-4 bg-alternative">
|
||||
<div>
|
||||
<p className="text-sm">Export your data</p>
|
||||
<p className="text-sm text-foreground-lighter">
|
||||
Download backups for your database and storage objects
|
||||
</p>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="default" icon={<Download />} iconRight={<ChevronDown />}>
|
||||
Download backups
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-60" align="end">
|
||||
<DropdownMenuItemTooltip
|
||||
className="gap-x-2"
|
||||
disabled={!latestBackup}
|
||||
onClick={() => onSelectDownloadBackup()}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'right',
|
||||
text: 'No backups available, please reach out via support for assistance',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Database size={16} />
|
||||
Database backup (PG: {dbVersion})
|
||||
</DropdownMenuItemTooltip>
|
||||
<DropdownMenuItem className="gap-x-2" onClick={() => onSelectDownloadStorageArchive()}>
|
||||
<Storage size={16} />
|
||||
Storage objects
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,121 +1,23 @@
|
||||
import { useParams } from 'common'
|
||||
import { Database, Storage } from 'icons'
|
||||
import { ChevronDown, Download, ExternalLink } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from 'ui'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
import { TimestampInfo } from 'ui-patterns/TimestampInfo'
|
||||
|
||||
import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip'
|
||||
import { DownloadBackupsSection } from './DownloadBackupsSection'
|
||||
import { InlineLink } from '@/components/ui/InlineLink'
|
||||
import { useBackupDownloadMutation } from '@/data/database/backup-download-mutation'
|
||||
import { useProjectPauseStatusQuery } from '@/data/projects/project-pause-status-query'
|
||||
import { useStorageArchiveCreateMutation } from '@/data/storage/storage-archive-create-mutation'
|
||||
import { useStorageArchiveQuery } from '@/data/storage/storage-archive-query'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { DOCS_URL, PROJECT_STATUS } from '@/lib/constants'
|
||||
import { formatRestoreWindow } from '@/lib/helpers'
|
||||
|
||||
export const PauseDisabledState = () => {
|
||||
const { ref } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const [toastId, setToastId] = useState<string | number>()
|
||||
const [refetchInterval, setRefetchInterval] = useState<number | false>(false)
|
||||
|
||||
const dbVersion = project?.dbVersion?.replace('supabase-postgres-', '')
|
||||
|
||||
const { data: pauseStatus } = useProjectPauseStatusQuery(
|
||||
{ ref },
|
||||
{ enabled: project?.status === PROJECT_STATUS.INACTIVE }
|
||||
)
|
||||
const latestBackup = pauseStatus?.latest_downloadable_backup_id
|
||||
|
||||
const { data: storageArchive, isSuccess: isStorageArchiveSuccess } = useStorageArchiveQuery(
|
||||
{ projectRef: ref },
|
||||
{
|
||||
refetchInterval,
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isStorageArchiveSuccess) return
|
||||
if (storageArchive.fileUrl && refetchInterval !== false) {
|
||||
toast.success('Downloading storage objects', { id: toastId })
|
||||
setToastId(undefined)
|
||||
setRefetchInterval(false)
|
||||
downloadStorageArchive(storageArchive.fileUrl)
|
||||
}
|
||||
}, [isStorageArchiveSuccess, storageArchive, refetchInterval])
|
||||
|
||||
const storageArchiveUrl = storageArchive?.fileUrl
|
||||
|
||||
const { mutate: downloadBackup } = useBackupDownloadMutation({
|
||||
onSuccess: (res) => {
|
||||
const { fileUrl } = res
|
||||
|
||||
// Trigger browser download by create,trigger and remove tempLink
|
||||
const tempLink = document.createElement('a')
|
||||
tempLink.href = fileUrl
|
||||
document.body.appendChild(tempLink)
|
||||
tempLink.click()
|
||||
document.body.removeChild(tempLink)
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate: createStorageArchive } = useStorageArchiveCreateMutation({
|
||||
onSuccess: () => {
|
||||
const toastId = toast.loading(
|
||||
'Retrieving storage archive. This may take a few minutes depending on the size of your storage objects.'
|
||||
)
|
||||
setToastId(toastId)
|
||||
setRefetchInterval(5000)
|
||||
},
|
||||
})
|
||||
|
||||
const onSelectDownloadBackup = () => {
|
||||
if (ref === undefined) return console.error('Project ref is required')
|
||||
if (!latestBackup) return toast.error('No backups available for download')
|
||||
|
||||
const toastId = toast.loading('Fetching database backup')
|
||||
|
||||
downloadBackup(
|
||||
{
|
||||
ref,
|
||||
backup: {
|
||||
id: latestBackup,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success('Downloading database backup', { id: toastId })
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const downloadStorageArchive = (url: string) => {
|
||||
const tempLink = document.createElement('a')
|
||||
tempLink.href = url
|
||||
document.body.appendChild(tempLink)
|
||||
tempLink.click()
|
||||
document.body.removeChild(tempLink)
|
||||
}
|
||||
|
||||
const onSelectDownloadStorageArchive = () => {
|
||||
if (!storageArchiveUrl) {
|
||||
createStorageArchive({ projectRef: ref })
|
||||
} else {
|
||||
toast.success('Downloading storage objects')
|
||||
downloadStorageArchive(storageArchiveUrl)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -128,7 +30,7 @@ export const PauseDisabledState = () => {
|
||||
<p className="leading-normal!">
|
||||
This project has been paused for over{' '}
|
||||
<span className="text-foreground">
|
||||
{pauseStatus?.max_days_till_restore_disabled ?? 90} days
|
||||
{formatRestoreWindow(pauseStatus?.max_days_till_restore_disabled ?? 365)}
|
||||
</span>{' '}
|
||||
and cannot be restored through the dashboard. However, your data remains intact and can be
|
||||
downloaded as a backup.
|
||||
@@ -165,46 +67,7 @@ export const PauseDisabledState = () => {
|
||||
</ul>
|
||||
</div>
|
||||
</Admonition>
|
||||
<div className="border-t flex flex-col gap-3 sm:flex-row sm:justify-between sm:items-center px-6 py-4 bg-alternative">
|
||||
<div>
|
||||
<p className="text-sm">Export your data</p>
|
||||
<p className="text-sm text-foreground-lighter">
|
||||
Download backups for your database and storage objects
|
||||
</p>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="default" icon={<Download />} iconRight={<ChevronDown />}>
|
||||
Download backups
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-60" align="end">
|
||||
<DropdownMenuItemTooltip
|
||||
className="gap-x-2"
|
||||
disabled={!latestBackup}
|
||||
onClick={() => onSelectDownloadBackup()}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'right',
|
||||
text: 'No backups available, please reach out via support for assistance',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Database size={16} />
|
||||
Database backup (PG: {dbVersion})
|
||||
</DropdownMenuItemTooltip>
|
||||
<DropdownMenuItem className="gap-x-2" onClick={() => onSelectDownloadStorageArchive()}>
|
||||
<Storage size={16} />
|
||||
Storage objects
|
||||
</DropdownMenuItem>
|
||||
{/* [Joshen] Once storage object download is supported, can just use the below component */}
|
||||
{/* <DropdownMenuItem className="gap-x-2" onClick={() => onSelectDownloadStorageArchive()}>
|
||||
<Storage size={16} />
|
||||
Download storage objects
|
||||
</DropdownMenuItem> */}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<DownloadBackupsSection />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -2,23 +2,14 @@ import { useParams } from 'common'
|
||||
import dayjs from 'dayjs'
|
||||
import { PauseCircle } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
cn,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from 'ui'
|
||||
import { Button, Card, CardContent, CardFooter } from 'ui'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
import { TimestampInfo } from 'ui-patterns/TimestampInfo'
|
||||
|
||||
import { DownloadBackupsSection } from './DownloadBackupsSection'
|
||||
import { PauseDisabledState } from './PauseDisabledState'
|
||||
import { ResumeProjectButton } from '@/components/interfaces/Project/ResumeProjectButton'
|
||||
import { AlertError } from '@/components/ui/AlertError'
|
||||
import { InlineLinkClassName } from '@/components/ui/InlineLink'
|
||||
import { UpgradePlanButton } from '@/components/ui/UpgradePlanButton'
|
||||
import { useProjectPauseStatusQuery } from '@/data/projects/project-pause-status-query'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
@@ -45,10 +36,11 @@ export const ProjectPausedState = ({ product }: ProjectPausedStateProps) => {
|
||||
isPending: isLoading,
|
||||
} = useProjectPauseStatusQuery({ ref }, { enabled: project?.status === PROJECT_STATUS.INACTIVE })
|
||||
|
||||
const finalDaysRemainingBeforeRestoreDisabled =
|
||||
pauseStatus?.remaining_days_till_restore_disabled ??
|
||||
pauseStatus?.max_days_till_restore_disabled ??
|
||||
0
|
||||
// null when there is no expiry (e.g. paid plans can always restore)
|
||||
const restoreExpiresAt =
|
||||
pauseStatus?.remaining_days_till_restore_disabled != null
|
||||
? dayjs().utc().add(pauseStatus.remaining_days_till_restore_disabled, 'day').toISOString()
|
||||
: null
|
||||
|
||||
const isFreePlan = selectedOrganization?.plan?.id === 'free'
|
||||
const isRestoreDisabled = isPauseStatusSuccess && !pauseStatus.can_restore
|
||||
@@ -67,36 +59,26 @@ export const ProjectPausedState = ({ product }: ProjectPausedStateProps) => {
|
||||
isFreePlan ? (
|
||||
<ul className="text-sm list-disc pl-4 space-y-2">
|
||||
<li>All data, including backups and storage objects, remains safe.</li>
|
||||
<li>
|
||||
You can resume this project from the dashboard within{' '}
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<span className={cn(InlineLinkClassName, 'text-foreground')}>
|
||||
{finalDaysRemainingBeforeRestoreDisabled} day
|
||||
{finalDaysRemainingBeforeRestoreDisabled > 1 ? 's' : ''}
|
||||
</span>{' '}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="w-80 text-center">
|
||||
Free projects cannot be restored through the dashboard if they are paused
|
||||
for more than {pauseStatus.max_days_till_restore_disabled} days
|
||||
</TooltipContent>
|
||||
</Tooltip>{' '}
|
||||
(until{' '}
|
||||
<TimestampInfo
|
||||
displayAs="local"
|
||||
utcTimestamp={dayjs()
|
||||
.utc()
|
||||
.add(pauseStatus.remaining_days_till_restore_disabled ?? 0, 'day')
|
||||
.toISOString()}
|
||||
className="text-sm text-foreground"
|
||||
labelFormat="DD MMM YYYY"
|
||||
/>
|
||||
).
|
||||
</li>
|
||||
<li>
|
||||
After that, this project will not be resumable, but data will still be
|
||||
available for download.
|
||||
</li>
|
||||
{restoreExpiresAt ? (
|
||||
<>
|
||||
<li>
|
||||
You can resume this project from the dashboard until{' '}
|
||||
<TimestampInfo
|
||||
displayAs="local"
|
||||
utcTimestamp={restoreExpiresAt}
|
||||
className="text-sm text-foreground"
|
||||
labelFormat="DD MMM YYYY"
|
||||
/>
|
||||
.
|
||||
</li>
|
||||
<li>
|
||||
After that, this project will not be resumable, but data will still be
|
||||
available for download.
|
||||
</li>
|
||||
</>
|
||||
) : (
|
||||
<li>You can resume this project from the dashboard.</li>
|
||||
)}
|
||||
<li>
|
||||
{enableProBenefitWording === 'variant-a'
|
||||
? 'Upgrade to Pro to prevent pauses and unlock features like branching, compute upgrades, and daily backups.'
|
||||
@@ -106,22 +88,20 @@ export const ProjectPausedState = ({ product }: ProjectPausedStateProps) => {
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
Your project data is safe and available for{' '}
|
||||
<span className="text-foreground">
|
||||
{finalDaysRemainingBeforeRestoreDisabled} day
|
||||
{finalDaysRemainingBeforeRestoreDisabled > 1 ? 's' : ''}
|
||||
</span>{' '}
|
||||
(until{' '}
|
||||
<TimestampInfo
|
||||
displayAs="local"
|
||||
utcTimestamp={dayjs()
|
||||
.utc()
|
||||
.add(pauseStatus.remaining_days_till_restore_disabled ?? 0, 'day')
|
||||
.toISOString()}
|
||||
className="text-sm text-foreground"
|
||||
labelFormat="DD MMM YYYY"
|
||||
/>
|
||||
), but inaccessible while paused.
|
||||
Your project data is safe
|
||||
{restoreExpiresAt ? (
|
||||
<>
|
||||
{' '}
|
||||
and available until{' '}
|
||||
<TimestampInfo
|
||||
displayAs="local"
|
||||
utcTimestamp={restoreExpiresAt}
|
||||
className="text-sm text-foreground"
|
||||
labelFormat="DD MMM YYYY"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
, but inaccessible while paused.
|
||||
</p>
|
||||
<p className="text-sm mt-2">
|
||||
Once resumed, usage will be billed by compute size and hours active.
|
||||
@@ -156,17 +136,20 @@ export const ProjectPausedState = ({ product }: ProjectPausedStateProps) => {
|
||||
)}
|
||||
|
||||
{isPauseStatusSuccess && !isRestoreDisabled && (
|
||||
<CardFooter className="flex flex-wrap justify-end items-center gap-2">
|
||||
<ResumeProjectButton size="tiny" variant="default" />
|
||||
<>
|
||||
<CardFooter className="flex flex-wrap justify-end items-center gap-2">
|
||||
<ResumeProjectButton size="tiny" variant="default" />
|
||||
|
||||
{isFreePlan ? (
|
||||
<UpgradePlanButton source="projectPausedStateRestore" />
|
||||
) : (
|
||||
<Button asChild variant="default">
|
||||
<Link href={`/project/${ref}/settings/general`}>View project settings</Link>
|
||||
</Button>
|
||||
)}
|
||||
</CardFooter>
|
||||
{isFreePlan ? (
|
||||
<UpgradePlanButton source="projectPausedStateRestore" />
|
||||
) : (
|
||||
<Button asChild variant="default">
|
||||
<Link href={`/project/${ref}/settings/general`}>View project settings</Link>
|
||||
</Button>
|
||||
)}
|
||||
</CardFooter>
|
||||
<DownloadBackupsSection />
|
||||
</>
|
||||
)}
|
||||
|
||||
{isPauseStatusSuccess && isRestoreDisabled && <PauseDisabledState />}
|
||||
|
||||
@@ -52,7 +52,7 @@ export function buildPauseStatus(override: PauseStateOverride): PauseStatusRespo
|
||||
can_restore: isRestorable,
|
||||
last_paused_on: null,
|
||||
latest_downloadable_backup_id: null,
|
||||
max_days_till_restore_disabled: 90,
|
||||
remaining_days_till_restore_disabled: isRestorable ? 78 : 0,
|
||||
max_days_till_restore_disabled: 400,
|
||||
remaining_days_till_restore_disabled: isRestorable ? 350 : 0,
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
extractUrls,
|
||||
formatBytes,
|
||||
formatCurrency,
|
||||
formatRestoreWindow,
|
||||
getDatabaseMajorVersion,
|
||||
getDistanceLatLonKM,
|
||||
getSemanticVersion,
|
||||
@@ -702,3 +703,15 @@ describe('tablesToSQL', () => {
|
||||
expect(result).not.toContain('CREATE TABLE')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatRestoreWindow', () => {
|
||||
it('renders windows under a year in days', () => {
|
||||
expect(formatRestoreWindow(90)).toBe('90 days')
|
||||
expect(formatRestoreWindow(364)).toBe('364 days')
|
||||
})
|
||||
|
||||
it('renders windows of a year or more as 1 year', () => {
|
||||
expect(formatRestoreWindow(365)).toBe('1 year')
|
||||
expect(formatRestoreWindow(400)).toBe('1 year')
|
||||
})
|
||||
})
|
||||
@@ -351,6 +351,9 @@ const formatSemver = (version: string) => {
|
||||
return formattedSemver
|
||||
}
|
||||
|
||||
// Windows of a year or more read better as years than day counts
|
||||
export const formatRestoreWindow = (days: number) => (days >= 365 ? '1 year' : `${days} days`)
|
||||
|
||||
export const getSemanticVersion = (version: string) => {
|
||||
if (!version) return 0
|
||||
|
||||
|
||||
Reference in new issue
Block a user