ProjectPausedState.tsx153 lines · main
| 1 | import { useParams } from 'common' |
| 2 | import dayjs from 'dayjs' |
| 3 | import { PauseCircle } from 'lucide-react' |
| 4 | import Link from 'next/link' |
| 5 | import { |
| 6 | Button, |
| 7 | Card, |
| 8 | CardContent, |
| 9 | CardFooter, |
| 10 | cn, |
| 11 | Tooltip, |
| 12 | TooltipContent, |
| 13 | TooltipTrigger, |
| 14 | } from 'ui' |
| 15 | import { TimestampInfo } from 'ui-patterns' |
| 16 | import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' |
| 17 | |
| 18 | import { PauseDisabledState } from './PauseDisabledState' |
| 19 | import { ResumeProjectButton } from '@/components/interfaces/Project/ResumeProjectButton' |
| 20 | import { AlertError } from '@/components/ui/AlertError' |
| 21 | import { InlineLinkClassName } from '@/components/ui/InlineLink' |
| 22 | import { UpgradePlanButton } from '@/components/ui/UpgradePlanButton' |
| 23 | import { useProjectPauseStatusQuery } from '@/data/projects/project-pause-status-query' |
| 24 | import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' |
| 25 | import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' |
| 26 | import { usePHFlag } from '@/hooks/ui/useFlag' |
| 27 | import { PROJECT_STATUS } from '@/lib/constants' |
| 28 | |
| 29 | export interface ProjectPausedStateProps { |
| 30 | product?: string |
| 31 | } |
| 32 | |
| 33 | export const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { |
| 34 | const { ref } = useParams() |
| 35 | const { data: project } = useSelectedProjectQuery() |
| 36 | const { data: selectedOrganization } = useSelectedOrganizationQuery() |
| 37 | |
| 38 | const enableProBenefitWording = usePHFlag('proBenefitWording') |
| 39 | |
| 40 | const { |
| 41 | data: pauseStatus, |
| 42 | error: pauseStatusError, |
| 43 | isError, |
| 44 | isSuccess: isPauseStatusSuccess, |
| 45 | isPending: isLoading, |
| 46 | } = useProjectPauseStatusQuery({ ref }, { enabled: project?.status === PROJECT_STATUS.INACTIVE }) |
| 47 | |
| 48 | const finalDaysRemainingBeforeRestoreDisabled = |
| 49 | pauseStatus?.remaining_days_till_restore_disabled ?? |
| 50 | pauseStatus?.max_days_till_restore_disabled ?? |
| 51 | 0 |
| 52 | |
| 53 | const isFreePlan = selectedOrganization?.plan?.id === 'free' |
| 54 | const isRestoreDisabled = isPauseStatusSuccess && !pauseStatus.can_restore |
| 55 | |
| 56 | return ( |
| 57 | <Card className="w-full max-w-160 mx-auto"> |
| 58 | <CardContent> |
| 59 | <PauseCircle size={48} strokeWidth={1} className="text-foreground-lighter shrink-0 mb-4" /> |
| 60 | <div className="flex-1"> |
| 61 | <div> |
| 62 | <h2 className="mb-4">The project "{project?.name}" is currently paused</h2> |
| 63 | <div className="text-foreground-light max-w-4xl"> |
| 64 | {isLoading && <GenericSkeletonLoader className="mt-3" />} |
| 65 | |
| 66 | {isPauseStatusSuccess && !isRestoreDisabled ? ( |
| 67 | isFreePlan ? ( |
| 68 | <> |
| 69 | <p className="text-sm"> |
| 70 | All data, including backups and storage objects, remains safe. You can resume |
| 71 | this project from the dashboard within{' '} |
| 72 | <Tooltip> |
| 73 | <TooltipTrigger> |
| 74 | <span className={cn(InlineLinkClassName, 'text-foreground')}> |
| 75 | {finalDaysRemainingBeforeRestoreDisabled} day |
| 76 | {finalDaysRemainingBeforeRestoreDisabled > 1 ? 's' : ''} |
| 77 | </span>{' '} |
| 78 | </TooltipTrigger> |
| 79 | <TooltipContent side="bottom" className="w-80 text-center"> |
| 80 | Free projects cannot be restored through the dashboard if they are paused |
| 81 | for more than {pauseStatus.max_days_till_restore_disabled} days |
| 82 | </TooltipContent> |
| 83 | </Tooltip>{' '} |
| 84 | (until{' '} |
| 85 | <TimestampInfo |
| 86 | displayAs="local" |
| 87 | utcTimestamp={dayjs() |
| 88 | .utc() |
| 89 | .add(pauseStatus.remaining_days_till_restore_disabled ?? 0, 'day') |
| 90 | .toISOString()} |
| 91 | className="text-sm text-foreground" |
| 92 | labelFormat="DD MMM YYYY" |
| 93 | /> |
| 94 | ). After that, this project will not be resumable, but data will still be |
| 95 | available for download. |
| 96 | </p> |
| 97 | <p className="text-sm mt-4"> |
| 98 | {enableProBenefitWording === 'variant-a' |
| 99 | ? 'Upgrade to Pro to prevent pauses and unlock features like branching, compute upgrades, and daily backups.' |
| 100 | : 'To prevent future pauses, consider upgrading to Pro.'} |
| 101 | </p> |
| 102 | </> |
| 103 | ) : ( |
| 104 | <p className="text-sm"> |
| 105 | Your project data is safe but inaccessible while paused. Once resumed, usage |
| 106 | will be billed by compute size and hours active. |
| 107 | </p> |
| 108 | ) |
| 109 | ) : !isLoading ? ( |
| 110 | <p className="text-sm"> |
| 111 | All of your project's data is still intact, but your project is inaccessible while |
| 112 | paused.{' '} |
| 113 | {product !== undefined ? ( |
| 114 | <> |
| 115 | Resume this project to access the{' '} |
| 116 | <span className="text-brand">{product}</span> page. |
| 117 | </> |
| 118 | ) : !isRestoreDisabled ? ( |
| 119 | 'Resume this project and get back to building!' |
| 120 | ) : null} |
| 121 | </p> |
| 122 | ) : null} |
| 123 | </div> |
| 124 | </div> |
| 125 | </div> |
| 126 | </CardContent> |
| 127 | |
| 128 | {isError && ( |
| 129 | <AlertError |
| 130 | className="rounded-none border-0" |
| 131 | error={pauseStatusError} |
| 132 | subject="Failed to retrieve pause status" |
| 133 | /> |
| 134 | )} |
| 135 | |
| 136 | {isPauseStatusSuccess && !isRestoreDisabled && ( |
| 137 | <CardFooter className="flex flex-wrap justify-end items-center gap-2"> |
| 138 | <ResumeProjectButton size="tiny" type="default" /> |
| 139 | |
| 140 | {isFreePlan ? ( |
| 141 | <UpgradePlanButton source="projectPausedStateRestore" /> |
| 142 | ) : ( |
| 143 | <Button asChild type="default"> |
| 144 | <Link href={`/project/${ref}/settings/general`}>View project settings</Link> |
| 145 | </Button> |
| 146 | )} |
| 147 | </CardFooter> |
| 148 | )} |
| 149 | |
| 150 | {isPauseStatusSuccess && isRestoreDisabled && <PauseDisabledState />} |
| 151 | </Card> |
| 152 | ) |
| 153 | } |