DiskSizeConfigurationModal.tsx272 lines · main
| 1 | import { zodResolver } from '@hookform/resolvers/zod' |
| 2 | import { SupportCategories } from '@supabase/shared-types/out/constants' |
| 3 | import { useParams } from 'common' |
| 4 | import dayjs from 'dayjs' |
| 5 | import { ExternalLink, Info } from 'lucide-react' |
| 6 | import Link from 'next/link' |
| 7 | import { SetStateAction, useEffect, useMemo } from 'react' |
| 8 | import { SubmitHandler, useForm } from 'react-hook-form' |
| 9 | import { toast } from 'sonner' |
| 10 | import { |
| 11 | Alert, |
| 12 | AlertDescription, |
| 13 | AlertTitle, |
| 14 | Button, |
| 15 | Form, |
| 16 | FormControl, |
| 17 | FormField, |
| 18 | FormInputGroupInput, |
| 19 | InfoIcon, |
| 20 | InputGroup, |
| 21 | InputGroupAddon, |
| 22 | InputGroupText, |
| 23 | Modal, |
| 24 | WarningIcon, |
| 25 | } from 'ui' |
| 26 | import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' |
| 27 | import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' |
| 28 | import * as z from 'zod' |
| 29 | |
| 30 | import { SupportLink } from '@/components/interfaces/Support/SupportLink' |
| 31 | import { useProjectDiskResizeMutation } from '@/data/config/project-disk-resize-mutation' |
| 32 | import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query' |
| 33 | import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' |
| 34 | import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' |
| 35 | import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' |
| 36 | import { DOCS_URL } from '@/lib/constants' |
| 37 | |
| 38 | export interface DiskSizeConfigurationProps { |
| 39 | visible: boolean |
| 40 | hideModal: (value: SetStateAction<boolean>) => void |
| 41 | loading: boolean |
| 42 | } |
| 43 | |
| 44 | const formId = 'disk-size-form' |
| 45 | const maxDiskSize = 200 |
| 46 | |
| 47 | const DiskSizeConfigurationModal = ({ |
| 48 | visible, |
| 49 | loading, |
| 50 | hideModal, |
| 51 | }: DiskSizeConfigurationProps) => { |
| 52 | const { ref: projectRef } = useParams() |
| 53 | const { data: organization } = useSelectedOrganizationQuery() |
| 54 | const { data: project, isPending: isLoadingProject } = useSelectedProjectQuery() |
| 55 | const { lastDatabaseResizeAt } = project ?? {} |
| 56 | |
| 57 | const { data: projectSubscriptionData, isPending: isLoadingSubscription } = |
| 58 | useOrgSubscriptionQuery({ orgSlug: organization?.slug }, { enabled: visible }) |
| 59 | |
| 60 | const { hasAccess: hasAccessToDiskModifications, isLoading: isLoadingDiskEntitlement } = |
| 61 | useCheckEntitlements('instances.disk_modifications') |
| 62 | |
| 63 | const isLoading = isLoadingProject || isLoadingSubscription || isLoadingDiskEntitlement |
| 64 | |
| 65 | const timeTillNextAvailableDatabaseResize = |
| 66 | lastDatabaseResizeAt === null ? 0 : 6 * 60 - dayjs().diff(lastDatabaseResizeAt, 'minutes') |
| 67 | const isAbleToResizeDatabase = timeTillNextAvailableDatabaseResize <= 0 |
| 68 | const formattedTimeTillNextAvailableResize = |
| 69 | timeTillNextAvailableDatabaseResize < 60 |
| 70 | ? `${timeTillNextAvailableDatabaseResize} minute(s)` |
| 71 | : `${Math.floor(timeTillNextAvailableDatabaseResize / 60)} hours and ${ |
| 72 | timeTillNextAvailableDatabaseResize % 60 |
| 73 | } minute(s)` |
| 74 | |
| 75 | const { mutate: updateProjectUsage, isPending: isUpdatingDiskSize } = |
| 76 | useProjectDiskResizeMutation({ |
| 77 | onSuccess: (_res, variables) => { |
| 78 | toast.success(`Successfully updated disk size to ${variables.volumeSize} GB`) |
| 79 | hideModal(false) |
| 80 | }, |
| 81 | }) |
| 82 | |
| 83 | const currentDiskSize = project?.volumeSizeGb ?? 0 |
| 84 | |
| 85 | const INITIAL_VALUES = useMemo( |
| 86 | () => ({ |
| 87 | 'new-disk-size': currentDiskSize, |
| 88 | }), |
| 89 | [currentDiskSize] |
| 90 | ) |
| 91 | |
| 92 | const diskSizeValidationSchema = useMemo( |
| 93 | () => |
| 94 | z.object({ |
| 95 | 'new-disk-size': z.coerce |
| 96 | .number({ required_error: 'Please enter a GB amount you want to resize the disk up to.' }) |
| 97 | .min(Number(currentDiskSize ?? 0), `Must be at least ${currentDiskSize} GB`) |
| 98 | // to do, update with max_disk_volume_size_gb |
| 99 | .max(Number(maxDiskSize), `Must not be more than ${maxDiskSize} GB`), |
| 100 | }), |
| 101 | [currentDiskSize] |
| 102 | ) |
| 103 | |
| 104 | const handleSubmit: SubmitHandler<z.infer<typeof diskSizeValidationSchema>> = async (values) => { |
| 105 | if (!projectRef) return console.error('Project ref is required') |
| 106 | const volumeSize = values['new-disk-size'] |
| 107 | updateProjectUsage({ projectRef, volumeSize }) |
| 108 | } |
| 109 | |
| 110 | const form = useForm<z.infer<typeof diskSizeValidationSchema>>({ |
| 111 | resolver: zodResolver(diskSizeValidationSchema as any), |
| 112 | defaultValues: INITIAL_VALUES, |
| 113 | }) |
| 114 | const { reset, formState } = form |
| 115 | const { isDirty } = formState |
| 116 | |
| 117 | useEffect(() => { |
| 118 | if (isDirty) return |
| 119 | reset(INITIAL_VALUES) |
| 120 | }, [INITIAL_VALUES, isDirty, reset]) |
| 121 | |
| 122 | return ( |
| 123 | <Modal |
| 124 | header="Increase Disk Storage Size" |
| 125 | size="medium" |
| 126 | visible={visible} |
| 127 | loading={loading} |
| 128 | onCancel={() => hideModal(false)} |
| 129 | hideFooter |
| 130 | > |
| 131 | {isLoading ? ( |
| 132 | <div className="flex flex-col gap-4 p-4"> |
| 133 | <ShimmeringLoader /> |
| 134 | <ShimmeringLoader /> |
| 135 | </div> |
| 136 | ) : projectSubscriptionData?.usage_billing_enabled === true && |
| 137 | hasAccessToDiskModifications ? ( |
| 138 | <> |
| 139 | {currentDiskSize >= maxDiskSize ? ( |
| 140 | <Alert variant="warning" className="rounded-t-none border-0"> |
| 141 | <WarningIcon /> |
| 142 | <AlertTitle>Maximum manual disk size increase reached</AlertTitle> |
| 143 | <AlertDescription> |
| 144 | <p> |
| 145 | You cannot manually expand the disk size any more than {maxDiskSize}GB. If you |
| 146 | need more than this, contact us via support for help. |
| 147 | </p> |
| 148 | <Button asChild type="default" className="mt-3"> |
| 149 | <SupportLink |
| 150 | queryParams={{ |
| 151 | projectRef, |
| 152 | category: SupportCategories.PERFORMANCE_ISSUES, |
| 153 | subject: 'Increase disk size beyond 200GB', |
| 154 | }} |
| 155 | > |
| 156 | Contact support |
| 157 | </SupportLink> |
| 158 | </Button> |
| 159 | </AlertDescription> |
| 160 | </Alert> |
| 161 | ) : ( |
| 162 | <> |
| 163 | <Modal.Content className="w-full space-y-4"> |
| 164 | <Alert variant={isAbleToResizeDatabase ? 'default' : 'warning'}> |
| 165 | <Info size={16} /> |
| 166 | <AlertTitle>This operation is only possible every 4 hours</AlertTitle> |
| 167 | <AlertDescription> |
| 168 | <div className="mb-4"> |
| 169 | {isAbleToResizeDatabase |
| 170 | ? `Upon updating your disk size, the next disk size update will only be available from ${dayjs().format( |
| 171 | 'DD MMM YYYY, HH:mm (ZZ)' |
| 172 | )}` |
| 173 | : `Your database was last resized at ${dayjs(lastDatabaseResizeAt).format( |
| 174 | 'DD MMM YYYY, HH:mm (ZZ)' |
| 175 | )}. You can resize your database again in approximately ${formattedTimeTillNextAvailableResize}`} |
| 176 | </div> |
| 177 | <Button asChild type="default" iconRight={<ExternalLink size={14} />}> |
| 178 | <Link href={`${DOCS_URL}/guides/platform/database-size#disk-management`}> |
| 179 | Read more about disk management |
| 180 | </Link> |
| 181 | </Button> |
| 182 | </AlertDescription> |
| 183 | </Alert> |
| 184 | <Form {...form}> |
| 185 | <form id={formId} onSubmit={form.handleSubmit(handleSubmit)} noValidate> |
| 186 | <FormField |
| 187 | control={form.control} |
| 188 | name="new-disk-size" |
| 189 | disabled={!isAbleToResizeDatabase} |
| 190 | render={({ field }) => ( |
| 191 | <FormItemLayout |
| 192 | name="new-disk-size" |
| 193 | layout="vertical" |
| 194 | label="New disk size" |
| 195 | > |
| 196 | <FormControl> |
| 197 | <InputGroup> |
| 198 | <FormInputGroupInput |
| 199 | {...field} |
| 200 | id="new-disk-size" |
| 201 | type="number" |
| 202 | onChange={(e) => field.onChange(Number(e.target.value))} |
| 203 | /> |
| 204 | <InputGroupAddon align="inline-end"> |
| 205 | <InputGroupText>GB</InputGroupText> |
| 206 | </InputGroupAddon> |
| 207 | </InputGroup> |
| 208 | </FormControl> |
| 209 | </FormItemLayout> |
| 210 | )} |
| 211 | /> |
| 212 | </form> |
| 213 | </Form> |
| 214 | </Modal.Content> |
| 215 | <Modal.Separator /> |
| 216 | <Modal.Content className="flex space-x-2 justify-end"> |
| 217 | <Button type="default" onClick={() => hideModal(false)}> |
| 218 | Cancel |
| 219 | </Button> |
| 220 | <Button |
| 221 | form={formId} |
| 222 | htmlType="submit" |
| 223 | type="primary" |
| 224 | disabled={!isAbleToResizeDatabase || isUpdatingDiskSize || !isDirty} |
| 225 | loading={isUpdatingDiskSize} |
| 226 | > |
| 227 | Update disk size |
| 228 | </Button> |
| 229 | </Modal.Content> |
| 230 | </> |
| 231 | )} |
| 232 | </> |
| 233 | ) : ( |
| 234 | <Alert className="border-none"> |
| 235 | <InfoIcon /> |
| 236 | <AlertTitle> |
| 237 | {hasAccessToDiskModifications === false |
| 238 | ? 'Disk size configuration is not available for projects on the Free Plan' |
| 239 | : 'Disk size configuration is only available when the spend cap has been disabled'} |
| 240 | </AlertTitle> |
| 241 | <AlertDescription> |
| 242 | {hasAccessToDiskModifications === false ? ( |
| 243 | <p> |
| 244 | If you are intending to use more than 500MB of disk space, then you will need to |
| 245 | upgrade to at least the Pro Plan. |
| 246 | </p> |
| 247 | ) : ( |
| 248 | <p> |
| 249 | If you are intending to use more than 8GB of disk space, then you will need to |
| 250 | disable your spend cap. |
| 251 | </p> |
| 252 | )} |
| 253 | <Button asChild type="default" className="mt-3"> |
| 254 | <Link |
| 255 | href={`/org/${organization?.slug}/billing?panel=${ |
| 256 | hasAccessToDiskModifications === false ? 'subscriptionPlan' : 'costControl' |
| 257 | }`} |
| 258 | target="_blank" |
| 259 | > |
| 260 | {hasAccessToDiskModifications === false |
| 261 | ? 'Upgrade subscription' |
| 262 | : 'Disable spend cap'} |
| 263 | </Link> |
| 264 | </Button> |
| 265 | </AlertDescription> |
| 266 | </Alert> |
| 267 | )} |
| 268 | </Modal> |
| 269 | ) |
| 270 | } |
| 271 | |
| 272 | export default DiskSizeConfigurationModal |