From 2d92563b577da4c19517641afc944326e47f6bab Mon Sep 17 00:00:00 2001
From: Danny White <3104761+dnywh@users.noreply.github.com>
Date: Tue, 28 Apr 2026 19:30:54 +1000
Subject: [PATCH] fix(studio): add resume project flow to project settings
(#45078)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.
YES
## What kind of change does this PR introduce?
Bug fix. Resolves DEPR-511.
## What is the current behavior?
Paused projects in `Project Settings > General > Project availability`
still present restart/pause maintenance controls, but no resume
affordance. That makes the resume path hard to discover from Settings
and pushes users back to the project dashboard to find the correct
action.
The paused state also keeps showing a redundant disabled `Pause project`
row, and the pause confirmation uses a more flexible modal than this
flow needs.
DEPR-519 already covered the unhealthy-project restart guard, but not
this paused-project discoverability path.
## What is the new behavior?
Project Settings is now paused-project aware. It shows a shared `Resume
project` action when the project can still be restored, falls back to
the project dashboard when the restore window has expired or pause
status cannot be confirmed, and reuses the same resume flow, permission
checks, and free-tier guardrails as the paused dashboard.
While a project is already paused, the redundant `Pause project` row is
hidden so the section stays focused on the real next action. For active
projects, the pause row remains in place, including the useful disabled
tooltip states for plans that cannot pause.
The pause confirmation now uses `AlertDialog` with shorter, more
accurate copy about the restore window, and the restart controls now
behave more consistently on smaller breakpoints. The Project Settings
command-menu entry is also searchable via `resume project`.
| Before | After |
| --- | --- |
| | |
| | |
| | |
## Summary by CodeRabbit
* **New Features**
* Added a tooltip-enabled "Resume project" button that handles
permissions, free-plan member gating, optional Postgres version
selection, and navigates to the project after restore.
* **UX**
* Pause confirmation migrated to an alert-style dialog with updated copy
and disabled controls during pausing.
* Restart controls updated for improved responsive layout and refreshed
button visuals.
* Project settings now show appropriate resume/dashboard actions based
on pause/restore eligibility.
* **Tests**
* Added tests for active, resumable-paused, and non-resumable-paused
states.
---------
Co-authored-by: Joshen Lim
---
.../Project/ResumeProjectButton.tsx | 263 ++++++++++++
.../Infrastructure/PauseProjectButton.tsx | 43 +-
.../Infrastructure/RestartServerButton.tsx | 22 +-
.../Settings/General/Project.test.tsx | 172 ++++++++
.../interfaces/Settings/General/Project.tsx | 86 ++--
.../layouts/AuthLayout/Auth.Commands.tsx | 2 +-
.../PausedState/ProjectPausedState.tsx | 395 +++++-------------
.../ProjectSettings.Commands.tsx | 3 +-
8 files changed, 639 insertions(+), 347 deletions(-)
create mode 100644 apps/studio/components/interfaces/Project/ResumeProjectButton.tsx
create mode 100644 apps/studio/components/interfaces/Settings/General/Project.test.tsx
diff --git a/apps/studio/components/interfaces/Project/ResumeProjectButton.tsx b/apps/studio/components/interfaces/Project/ResumeProjectButton.tsx
new file mode 100644
index 00000000000..5f316278dd6
--- /dev/null
+++ b/apps/studio/components/interfaces/Project/ResumeProjectButton.tsx
@@ -0,0 +1,263 @@
+import { zodResolver } from '@hookform/resolvers/zod'
+import { PermissionAction } from '@supabase/shared-types/out/constants'
+import { useFlag, useParams } from 'common'
+import { useRouter } from 'next/router'
+import { useMemo, useState, type ComponentPropsWithoutRef } from 'react'
+import { useForm } from 'react-hook-form'
+import { AWS_REGIONS, CloudProvider } from 'shared-data'
+import { toast } from 'sonner'
+import {
+ Button,
+ cn,
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogSection,
+ DialogTitle,
+ Form,
+ FormField,
+} from 'ui'
+import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
+import { z } from 'zod'
+
+import {
+ extractPostgresVersionDetails,
+ PostgresVersionSelector,
+} from '@/components/interfaces/ProjectCreation/PostgresVersionSelector'
+import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
+import { useFreeProjectLimitCheckQuery } from '@/data/organizations/free-project-limit-check-query'
+import { useSetProjectStatus } from '@/data/projects/project-detail-query'
+import { useProjectPauseStatusQuery } from '@/data/projects/project-pause-status-query'
+import { useProjectRestoreMutation } from '@/data/projects/project-restore-mutation'
+import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
+import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
+import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
+import { PROJECT_STATUS } from '@/lib/constants'
+
+const FormSchema = z.object({
+ postgresVersionSelection: z.string(),
+})
+
+type ResumeProjectButtonProps = Pick<
+ ComponentPropsWithoutRef,
+ 'className' | 'size' | 'type'
+> & {
+ label?: string
+}
+
+export const ResumeProjectButton = ({
+ className,
+ label = 'Resume project',
+ size,
+ type = 'default',
+}: ResumeProjectButtonProps) => {
+ const router = useRouter()
+ const { ref } = useParams()
+ const { data: project } = useSelectedProjectQuery()
+ const { data: selectedOrganization } = useSelectedOrganizationQuery()
+ const { setProjectStatus } = useSetProjectStatus()
+
+ const showPostgresVersionSelector = useFlag('showPostgresVersionSelector')
+ const region = Object.values(AWS_REGIONS).find((x) => x.code === project?.region)
+ const orgSlug = selectedOrganization?.slug
+ const isFreePlan = selectedOrganization?.plan?.id === 'free'
+
+ const {
+ data: pauseStatus,
+ isPending: isPauseStatusPending,
+ isSuccess: isPauseStatusSuccess,
+ } = useProjectPauseStatusQuery({ ref }, { enabled: project?.status === PROJECT_STATUS.INACTIVE })
+
+ const isRestoreDisabled = isPauseStatusSuccess && !pauseStatus.can_restore
+
+ const { data: membersExceededLimit } = useFreeProjectLimitCheckQuery(
+ { slug: orgSlug },
+ { enabled: isFreePlan }
+ )
+
+ const hasMembersExceedingFreeTierLimit = (membersExceededLimit ?? []).length > 0
+
+ const [showConfirmRestore, setShowConfirmRestore] = useState(false)
+ const [showFreeProjectLimitWarning, setShowFreeProjectLimitWarning] = useState(false)
+
+ const { can: canResumeProject } = useAsyncCheckPermissions(
+ PermissionAction.INFRA_EXECUTE,
+ 'queue_jobs.projects.initialize_or_resume'
+ )
+
+ const { mutate: restoreProject, isPending: isRestoring } = useProjectRestoreMutation({
+ onSuccess: async (_, variables) => {
+ setProjectStatus({ ref: variables.ref, status: PROJECT_STATUS.RESTORING })
+ toast.success('Restoring project, project will be ready in a few minutes')
+ await router.push(`/project/${variables.ref}`)
+ },
+ })
+
+ const form = useForm>({
+ resolver: zodResolver(FormSchema),
+ mode: 'onChange',
+ defaultValues: { postgresVersionSelection: '' },
+ })
+
+ const onSelectRestore = () => {
+ if (project?.status !== PROJECT_STATUS.INACTIVE) {
+ return toast.error('Unable to resume: project is not paused')
+ }
+
+ if (isRestoreDisabled) {
+ return toast.error('This project can no longer be resumed from the dashboard')
+ }
+
+ if (!canResumeProject) {
+ return toast.error('You do not have the required permissions to restore this project')
+ }
+
+ if (hasMembersExceedingFreeTierLimit) {
+ return setShowFreeProjectLimitWarning(true)
+ }
+
+ setShowConfirmRestore(true)
+ }
+
+ const onConfirmRestore = async (values: z.infer) => {
+ if (!project) {
+ return toast.error('Unable to restore: project is required')
+ }
+
+ if (!showPostgresVersionSelector) {
+ return restoreProject({ ref: project.ref })
+ }
+
+ const postgresVersionDetails = extractPostgresVersionDetails(values.postgresVersionSelection)
+
+ restoreProject({
+ ref: project.ref,
+ releaseChannel: postgresVersionDetails.releaseChannel,
+ postgresEngine: postgresVersionDetails.postgresEngine,
+ })
+ }
+
+ const buttonDisabled =
+ project?.status !== PROJECT_STATUS.INACTIVE ||
+ project === undefined ||
+ isPauseStatusPending ||
+ isRestoring ||
+ isRestoreDisabled ||
+ !canResumeProject
+
+ const tooltipText = useMemo(() => {
+ if (isPauseStatusPending) return 'Checking whether this project can be resumed'
+ if (project?.status !== PROJECT_STATUS.INACTIVE) {
+ return 'Project must be paused before it can be resumed'
+ }
+ if (isRestoreDisabled) return 'This project can no longer be resumed from the dashboard'
+ if (!canResumeProject) return 'You need additional permissions to resume this project'
+ return undefined
+ }, [canResumeProject, isPauseStatusPending, isRestoreDisabled, project?.status])
+
+ return (
+ <>
+
+ {label}
+
+
+ setShowConfirmRestore(false)}
+ onConfirm={() => form.handleSubmit(onConfirmRestore)()}
+ loading={isRestoring}
+ confirmLabel="Resume"
+ confirmLabelLoading="Resuming"
+ cancelLabel="Cancel"
+ >
+
+
+ {isFreePlan
+ ? 'Your project’s data will be restored to when it was initially paused.'
+ : 'Your project’s data will be restored and billing will resume based on compute size and hours active.'}
+
- Restarts only the database - faster but may not be able to recover from all
- failure modes
+ Restarts only the database. Faster, but may not be able to recover from all
+ failure modes.
- All data, including backups and storage objects, remains safe. You can
- resume this project from the dashboard within{' '}
-
-
-
- {finalDaysRemainingBeforeRestoreDisabled} day
- {finalDaysRemainingBeforeRestoreDisabled > 1 ? 's' : ''}
- {' '}
-
-
- Free projects cannot be restored through the dashboard if they are
- paused for more than {pauseStatus.max_days_till_restore_disabled} days
-
- {' '}
- (until{' '}
-
- ). After that, this project will not be resumable, but data will still be
- available for download.
-
-
- {enableProBenefitWording === 'variant-a'
- ? 'Upgrade to Pro to prevent pauses and unlock features like branching, compute upgrades, and daily backups.'
- : 'To prevent future pauses, consider upgrading to Pro.'}
-
- Your project data is safe but inaccessible while paused. Once resumed, usage
- will be billed by compute size and hours active.
+ All data, including backups and storage objects, remains safe. You can resume
+ this project from the dashboard within{' '}
+
+
+
+ {finalDaysRemainingBeforeRestoreDisabled} day
+ {finalDaysRemainingBeforeRestoreDisabled > 1 ? 's' : ''}
+ {' '}
+
+
+ Free projects cannot be restored through the dashboard if they are paused
+ for more than {pauseStatus.max_days_till_restore_disabled} days
+
+ {' '}
+ (until{' '}
+
+ ). After that, this project will not be resumable, but data will still be
+ available for download.
- )
- ) : !isLoading ? (
+
+ {enableProBenefitWording === 'variant-a'
+ ? 'Upgrade to Pro to prevent pauses and unlock features like branching, compute upgrades, and daily backups.'
+ : 'To prevent future pauses, consider upgrading to Pro.'}
+
+ {!!pauseStatus.last_paused_on && (
+
+ Project last paused on{' '}
+
+
+ )}
+ >
+ ) : (
- All of your project's data is still intact, but your project is inaccessible
- while paused.{' '}
- {product !== undefined ? (
- <>
- Resume this project to access the{' '}
- {product} page.
- >
- ) : !isRestoreDisabled ? (
- 'Resume this project and get back to building!'
- ) : null}
+ Your project data is safe but inaccessible while paused. Once resumed, usage
+ will be billed by compute size and hours active.
- ) : null}
-
+ )
+ ) : !isLoading ? (
+
+ All of your project's data is still intact, but your project is inaccessible while
+ paused.{' '}
+ {product !== undefined ? (
+ <>
+ Resume this project to access the{' '}
+ {product} page.
+ >
+ ) : !isRestoreDisabled ? (
+ 'Resume this project and get back to building!'
+ ) : null}
+
- {isFreePlan
- ? 'Your project’s data will be restored to when it was initially paused.'
- : 'Your project’s data will be restored and billing will resume based on compute size and hours active.'}
-