import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' import dayjs from 'dayjs' import { ExternalLink, PauseCircle } from 'lucide-react' import Link from 'next/link' import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { z } from 'zod' import { useParams } from 'common' import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { PostgresEngine, ProjectUnpausePostgresVersion, ReleaseChannel, useProjectUnpausePostgresVersionsQuery, } from 'data/config/project-unpause-postgres-versions-query' import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query' import { useProjectPauseStatusQuery } from 'data/projects/project-pause-status-query' import { useProjectRestoreMutation } from 'data/projects/project-restore-mutation' import { setProjectStatus } from 'data/projects/projects-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { useFlag } from 'hooks/ui/useFlag' import { PROJECT_STATUS } from 'lib/constants' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Badge, Button, FormControl_Shadcn_, FormField_Shadcn_, Form_Shadcn_, Modal, SelectContent_Shadcn_, SelectGroup_Shadcn_, SelectItem_Shadcn_, SelectTrigger_Shadcn_, SelectValue_Shadcn_, Select_Shadcn_, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { PauseDisabledState } from './PauseDisabledState' import { RestorePaidPlanProjectNotice } from '../RestorePaidPlanProjectNotice' import { useProjectContext } from '../ProjectContext' export interface ProjectPausedStateProps { product?: string } interface PostgresVersionDetails { postgresEngine: PostgresEngine releaseChannel: ReleaseChannel } const formatValue = ({ postgres_engine, release_channel }: ProjectUnpausePostgresVersion) => { return `${postgres_engine}|${release_channel}` } export const extractPostgresVersionDetails = (value: string): PostgresVersionDetails => { const [postgresEngine, releaseChannel] = value.split('|') return { postgresEngine, releaseChannel } as PostgresVersionDetails } export const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { const { ref } = useParams() const queryClient = useQueryClient() const { project } = useProjectContext() const selectedOrganization = useSelectedOrganization() const enforceNinetyDayUnpauseExpiry = useFlag('enforceNinetyDayUnpauseExpiry') const projectVersionSelectionDisabled = useFlag('disableProjectVersionSelection') const orgSlug = selectedOrganization?.slug const { data: subscription } = useOrgSubscriptionQuery({ orgSlug }) const { data: pauseStatus, error: pauseStatusError, isError, isSuccess, isLoading, } = useProjectPauseStatusQuery( { ref }, { enabled: project?.status === PROJECT_STATUS.INACTIVE && enforceNinetyDayUnpauseExpiry, } ) const finalDaysRemainingBeforeRestoreDisabled = pauseStatus?.remaining_days_till_restore_disabled ?? pauseStatus?.max_days_till_restore_disabled ?? 0 const isFreePlan = subscription?.plan?.id === 'free' const isRestoreDisabled = enforceNinetyDayUnpauseExpiry && isSuccess && !pauseStatus.can_restore const { data: membersExceededLimit } = useFreeProjectLimitCheckQuery( { slug: orgSlug }, { enabled: isFreePlan } ) const { data: availablePostgresVersions } = useProjectUnpausePostgresVersionsQuery({ projectRef: project?.ref, }) const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0 const [showConfirmRestore, setShowConfirmRestore] = useState(false) const [showFreeProjectLimitWarning, setShowFreeProjectLimitWarning] = useState(false) const { mutate: restoreProject, isLoading: isRestoring } = useProjectRestoreMutation({ onSuccess: (_, variables) => { setProjectStatus(queryClient, variables.ref, PROJECT_STATUS.RESTORING) toast.success('Restoring project') }, }) const canResumeProject = useCheckPermissions( PermissionAction.INFRA_EXECUTE, 'queue_jobs.projects.initialize_or_resume' ) const onSelectRestore = () => { if (!canResumeProject) { toast.error('You do not have the required permissions to restore this project') } else if (hasMembersExceedingFreeTierLimit) setShowFreeProjectLimitWarning(true) else setShowConfirmRestore(true) } const onConfirmRestore = async (values: z.infer) => { if (!project) { return toast.error('Unable to restore: project is required') } if (projectVersionSelectionDisabled) { restoreProject({ ref: project.ref }) } else { const { postgresVersionSelection } = values const postgresVersionDetails = extractPostgresVersionDetails(postgresVersionSelection) restoreProject({ ref: project.ref, releaseChannel: postgresVersionDetails.releaseChannel, postgresEngine: postgresVersionDetails.postgresEngine, }) } } const FormSchema = z.object({ postgresVersionSelection: z.string(), }) const form = useForm>({ resolver: zodResolver(FormSchema), mode: 'onChange', defaultValues: { postgresVersionSelection: '', }, }) useEffect(() => { const defaultValue = availablePostgresVersions?.available_versions[0] ? formatValue(availablePostgresVersions?.available_versions[0]) : '' form.setValue('postgresVersionSelection', defaultValue) }, [availablePostgresVersions, form]) return ( <>

The project "{project?.name ?? ''}" is currently paused.

All of your project's data is still intact, but your project is inaccessible while paused.{' '} {product !== undefined ? ( <> Restore this project to access the{' '} {product} page ) : ( 'Restore this project and get back to building!' )}

{enforceNinetyDayUnpauseExpiry && ( <> {isLoading && } {isError && ( )} {isSuccess && ( <> {isRestoreDisabled ? ( ) : isFreePlan ? ( <>

To prevent future pauses, consider upgrading to Pro.

Project can be restored through the dashboard within the next{' '} {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 . The latest that your project can be restored is by{' '} {dayjs() .utc() .add( pauseStatus.remaining_days_till_restore_disabled ?? 0, 'day' ) .format('DD MMM YYYY')} . However, your database backup and Storage objects will still be available for download thereafter. ) : ( )} )} )} {!enforceNinetyDayUnpauseExpiry && !isFreePlan && }
{(!enforceNinetyDayUnpauseExpiry || (isSuccess && !isRestoreDisabled)) && (
Restore project {isFreePlan ? ( ) : ( )}
)}
setShowConfirmRestore(false)} header={'Restore this project'} >
{!projectVersionSelectionDisabled && (
( {(availablePostgresVersions?.available_versions || [])?.map( (value) => { const postgresVersion = value.version.split('supabase-postgres-')[1] return (
{postgresVersion} {value.release_channel !== 'ga' && ( {value.release_channel} )}
) } )}
)} />
)}
setShowFreeProjectLimitWarning(false)} >

The following members have reached their maximum limits for the number of active free plan projects within organizations where they are an administrator or owner:

    {(membersExceededLimit || []).map((member, idx: number) => (
  • {member.username || member.primary_email} (Limit: {member.free_project_limit} free projects)
  • ))}

These members will need to either delete, pause, or upgrade one or more of these projects before you're able to unpause this project.

) }