DiskSizeConfigurationModal.tsx272 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { SupportCategories } from '@supabase/shared-types/out/constants'
3import { useParams } from 'common'
4import dayjs from 'dayjs'
5import { ExternalLink, Info } from 'lucide-react'
6import Link from 'next/link'
7import { SetStateAction, useEffect, useMemo } from 'react'
8import { SubmitHandler, useForm } from 'react-hook-form'
9import { toast } from 'sonner'
10import {
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'
26import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
27import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
28import * as z from 'zod'
29
30import { SupportLink } from '@/components/interfaces/Support/SupportLink'
31import { useProjectDiskResizeMutation } from '@/data/config/project-disk-resize-mutation'
32import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
33import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
34import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
35import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
36import { DOCS_URL } from '@/lib/constants'
37
38export interface DiskSizeConfigurationProps {
39 visible: boolean
40 hideModal: (value: SetStateAction<boolean>) => void
41 loading: boolean
42}
43
44const formId = 'disk-size-form'
45const maxDiskSize = 200
46
47const 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
272export default DiskSizeConfigurationModal