mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
## Context Reverts copy changes from the following PRs: - Docs: https://github.com/supabase/supabase/pull/42184 - FE: https://github.com/supabase/supabase/pull/47646 Our platform's disk management configuration limitation still follows the 4 hour cooldown at the moment, doesn't align with AWS's 4 changes in 24 hours rule just yet. This is just to prevent any confusion for now, and we'll need to update the copy again once behaviour matches AWS on our BE <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Updates** * Disk changes are now subject to an approximately four-hour cooldown after each modification, replacing the previous limit of four changes in a rolling 24-hour period. * Disk management screens now show the cooldown status, remaining wait time, and next available update time. * Updated platform guides and troubleshooting instructions to reflect the cooldown and explain available recovery options. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
283 lines
11 KiB
TypeScript
283 lines
11 KiB
TypeScript
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { SupportCategories } from '@supabase/shared-types/out/constants'
|
|
import { useParams } from 'common'
|
|
import dayjs from 'dayjs'
|
|
import { ExternalLink, Info } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { SetStateAction, useEffect, useMemo } from 'react'
|
|
import { SubmitHandler, useForm } from 'react-hook-form'
|
|
import { toast } from 'sonner'
|
|
import {
|
|
Alert,
|
|
AlertDescription,
|
|
AlertTitle,
|
|
Button,
|
|
Dialog,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogSection,
|
|
DialogSectionSeparator,
|
|
DialogTitle,
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormInputGroupInput,
|
|
InfoIcon,
|
|
InputGroup,
|
|
InputGroupAddon,
|
|
InputGroupText,
|
|
WarningIcon,
|
|
} from 'ui'
|
|
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
|
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
|
import * as z from 'zod'
|
|
|
|
import { SupportLink } from '@/components/interfaces/Support/SupportLink'
|
|
import { COOLDOWN_DURATION } from '@/data/config/disk-attributes-update-mutation'
|
|
import { useProjectDiskResizeMutation } from '@/data/config/project-disk-resize-mutation'
|
|
import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
|
|
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
|
|
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import { DOCS_URL } from '@/lib/constants'
|
|
|
|
export interface DiskSizeConfigurationProps {
|
|
visible: boolean
|
|
hideModal: (value: SetStateAction<boolean>) => void
|
|
loading: boolean
|
|
}
|
|
|
|
const formId = 'disk-size-form'
|
|
const maxDiskSize = 200
|
|
|
|
const DiskSizeConfigurationModal = ({
|
|
visible,
|
|
loading,
|
|
hideModal,
|
|
}: DiskSizeConfigurationProps) => {
|
|
const { ref: projectRef } = useParams()
|
|
const { data: organization } = useSelectedOrganizationQuery()
|
|
const { data: project, isPending: isLoadingProject } = useSelectedProjectQuery()
|
|
const { lastDatabaseResizeAt } = project ?? {}
|
|
|
|
const { data: projectSubscriptionData, isPending: isLoadingSubscription } =
|
|
useOrgSubscriptionQuery({ orgSlug: organization?.slug }, { enabled: visible })
|
|
|
|
const { hasAccess: hasAccessToDiskModifications, isLoading: isLoadingDiskEntitlement } =
|
|
useCheckEntitlements('instances.disk_modifications')
|
|
|
|
const isLoading = isLoadingProject || isLoadingSubscription || isLoadingDiskEntitlement
|
|
|
|
// COOLDOWN_DURATION is in seconds; convert to minutes to match the diff unit below.
|
|
const cooldownMinutes = COOLDOWN_DURATION / 60
|
|
const timeTillNextAvailableDatabaseResize =
|
|
lastDatabaseResizeAt == null
|
|
? 0
|
|
: Math.max(0, cooldownMinutes - dayjs().diff(lastDatabaseResizeAt, 'minutes'))
|
|
const isAbleToResizeDatabase = timeTillNextAvailableDatabaseResize <= 0
|
|
const formattedTimeTillNextAvailableResize =
|
|
timeTillNextAvailableDatabaseResize < 60
|
|
? `${timeTillNextAvailableDatabaseResize} minute(s)`
|
|
: `${Math.floor(timeTillNextAvailableDatabaseResize / 60)} hours and ${
|
|
timeTillNextAvailableDatabaseResize % 60
|
|
} minute(s)`
|
|
|
|
const { mutate: updateProjectUsage, isPending: isUpdatingDiskSize } =
|
|
useProjectDiskResizeMutation({
|
|
onSuccess: (_res, variables) => {
|
|
toast.success(`Successfully updated disk size to ${variables.volumeSize} GB`)
|
|
hideModal(false)
|
|
},
|
|
})
|
|
|
|
const currentDiskSize = project?.volumeSizeGb ?? 0
|
|
|
|
const INITIAL_VALUES = useMemo(
|
|
() => ({
|
|
'new-disk-size': currentDiskSize,
|
|
}),
|
|
[currentDiskSize]
|
|
)
|
|
|
|
const diskSizeValidationSchema = useMemo(
|
|
() =>
|
|
z.object({
|
|
'new-disk-size': z
|
|
.union([
|
|
z.literal(''),
|
|
z.coerce
|
|
.number()
|
|
.min(Number(currentDiskSize ?? 0), `Must be at least ${currentDiskSize} GB`)
|
|
// to do, update with max_disk_volume_size_gb
|
|
.max(Number(maxDiskSize), `Must not be more than ${maxDiskSize} GB`),
|
|
])
|
|
.refine(
|
|
(value) => value !== '',
|
|
'Please enter a GB amount you want to resize the disk up to'
|
|
),
|
|
}),
|
|
[currentDiskSize]
|
|
)
|
|
|
|
const handleSubmit: SubmitHandler<z.infer<typeof diskSizeValidationSchema>> = async (values) => {
|
|
if (!projectRef) return console.error('Project ref is required')
|
|
const volumeSize = values['new-disk-size']
|
|
updateProjectUsage({ projectRef, volumeSize })
|
|
}
|
|
|
|
const form = useForm<z.infer<typeof diskSizeValidationSchema>>({
|
|
resolver: zodResolver(diskSizeValidationSchema),
|
|
defaultValues: INITIAL_VALUES,
|
|
})
|
|
const { reset, formState } = form
|
|
const { isDirty } = formState
|
|
|
|
useEffect(() => {
|
|
if (isDirty) return
|
|
reset(INITIAL_VALUES)
|
|
}, [INITIAL_VALUES, isDirty, reset])
|
|
|
|
return (
|
|
<Dialog open={visible} onOpenChange={(open) => hideModal(open)}>
|
|
<DialogContent size="medium">
|
|
<DialogHeader>
|
|
<DialogTitle>Increase Disk Storage Size</DialogTitle>
|
|
</DialogHeader>
|
|
<DialogSectionSeparator />
|
|
{isLoading ? (
|
|
<div className="flex flex-col gap-4 p-4">
|
|
<ShimmeringLoader />
|
|
<ShimmeringLoader />
|
|
</div>
|
|
) : projectSubscriptionData?.usage_billing_enabled === true &&
|
|
hasAccessToDiskModifications ? (
|
|
<>
|
|
{currentDiskSize >= maxDiskSize ? (
|
|
<Alert variant="warning" className="rounded-t-none border-0">
|
|
<WarningIcon />
|
|
<AlertTitle>Maximum manual disk size increase reached</AlertTitle>
|
|
<AlertDescription>
|
|
<p>
|
|
You cannot manually expand the disk size any more than {maxDiskSize}GB. If you
|
|
need more than this, contact us via support for help.
|
|
</p>
|
|
<Button asChild className="mt-3">
|
|
<SupportLink
|
|
queryParams={{
|
|
projectRef,
|
|
category: SupportCategories.PERFORMANCE_ISSUES,
|
|
subject: 'Increase disk size beyond 200GB',
|
|
}}
|
|
>
|
|
Contact support
|
|
</SupportLink>
|
|
</Button>
|
|
</AlertDescription>
|
|
</Alert>
|
|
) : (
|
|
<>
|
|
<DialogSection className="w-full space-y-4">
|
|
<Alert variant={isAbleToResizeDatabase ? 'default' : 'warning'}>
|
|
<Info size={16} />
|
|
<AlertTitle>This operation is only possible every 4 hours</AlertTitle>
|
|
<AlertDescription>
|
|
<div className="mb-4">
|
|
{isAbleToResizeDatabase
|
|
? `Upon updating your disk size, the next disk size update will only be available from ${dayjs().format(
|
|
'DD MMM YYYY, HH:mm (ZZ)'
|
|
)}`
|
|
: `Your database was last resized at ${dayjs(lastDatabaseResizeAt).format(
|
|
'DD MMM YYYY, HH:mm (ZZ)'
|
|
)}. You can resize your database again in approximately ${formattedTimeTillNextAvailableResize}`}
|
|
</div>
|
|
<Button asChild iconRight={<ExternalLink size={14} />}>
|
|
<Link href={`${DOCS_URL}/guides/platform/database-size#disk-management`}>
|
|
Read more about disk management
|
|
</Link>
|
|
</Button>
|
|
</AlertDescription>
|
|
</Alert>
|
|
<Form {...form}>
|
|
<form id={formId} onSubmit={form.handleSubmit(handleSubmit)} noValidate>
|
|
<FormField
|
|
control={form.control}
|
|
name="new-disk-size"
|
|
disabled={!isAbleToResizeDatabase}
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
name="new-disk-size"
|
|
layout="vertical"
|
|
label="New disk size"
|
|
>
|
|
<FormControl>
|
|
<InputGroup>
|
|
<FormInputGroupInput {...field} id="new-disk-size" type="number" />
|
|
<InputGroupAddon align="inline-end">
|
|
<InputGroupText>GB</InputGroupText>
|
|
</InputGroupAddon>
|
|
</InputGroup>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
</form>
|
|
</Form>
|
|
</DialogSection>
|
|
<DialogFooter>
|
|
<Button onClick={() => hideModal(false)}>Cancel</Button>
|
|
<Button
|
|
form={formId}
|
|
type="submit"
|
|
variant="primary"
|
|
disabled={!isAbleToResizeDatabase || isUpdatingDiskSize || !isDirty || loading}
|
|
loading={isUpdatingDiskSize || loading}
|
|
>
|
|
Update disk size
|
|
</Button>
|
|
</DialogFooter>
|
|
</>
|
|
)}
|
|
</>
|
|
) : (
|
|
<Alert className="border-none">
|
|
<InfoIcon />
|
|
<AlertTitle>
|
|
{hasAccessToDiskModifications === false
|
|
? 'Disk size configuration is not available for projects on the Free Plan'
|
|
: 'Disk size configuration is only available when the spend cap has been disabled'}
|
|
</AlertTitle>
|
|
<AlertDescription>
|
|
{hasAccessToDiskModifications === false ? (
|
|
<p>
|
|
If you are intending to use more than 500MB of disk space, then you will need to
|
|
upgrade to at least the Pro Plan.
|
|
</p>
|
|
) : (
|
|
<p>
|
|
If you are intending to use more than 8GB of disk space, then you will need to
|
|
disable your spend cap.
|
|
</p>
|
|
)}
|
|
<Button asChild className="mt-3">
|
|
<Link
|
|
href={`/org/${organization?.slug}/billing?panel=${
|
|
hasAccessToDiskModifications === false ? 'subscriptionPlan' : 'costControl'
|
|
}`}
|
|
target="_blank"
|
|
>
|
|
{hasAccessToDiskModifications === false
|
|
? 'Upgrade subscription'
|
|
: 'Disable spend cap'}
|
|
</Link>
|
|
</Button>
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
export default DiskSizeConfigurationModal
|