| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640 |
- // @ts-nocheck
- import { zodResolver } from '@hookform/resolvers/zod'
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import { useParams } from 'common'
- import { AnimatePresence, motion } from 'framer-motion'
- import { ChevronRight } from 'lucide-react'
- import { useEffect, useRef, useState } from 'react'
- import { useForm } from 'react-hook-form'
- import { CloudProvider } from 'shared-data'
- import { toast } from 'sonner'
- import {
- Button,
- cn,
- Collapsible,
- CollapsibleContent,
- CollapsibleTrigger,
- DialogSectionSeparator,
- Form,
- Separator,
- } from 'ui'
- import { Admonition } from 'ui-patterns'
- import { FormFooterChangeBadge } from '../DataWarehouse/FormFooterChangeBadge'
- import { CreateDiskStorageSchema, DiskStorageSchemaType } from './DiskManagement.schema'
- import { DiskManagementMessage } from './DiskManagement.types'
- import {
- calculateDiskSizeRequiredForIopsWithGp3,
- mapComputeSizeNameToAddonVariantId,
- } from './DiskManagement.utils'
- import { DiskMangementRestartRequiredSection } from './DiskManagementRestartRequiredSection'
- import { DiskManagementReviewAndSubmitDialog } from './DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog'
- import { AutoScaleFields } from './fields/AutoScaleFields'
- import { ComputeSizeField } from './fields/ComputeSizeField'
- import { DiskSizeField } from './fields/DiskSizeField'
- import { IOPSField } from './fields/IOPSField'
- import { StorageTypeField } from './fields/StorageTypeField'
- import { ThroughputField } from './fields/ThroughputField'
- import { DiskCountdownRadial } from './ui/DiskCountdownRadial'
- import {
- DISK_LIMITS,
- DiskType,
- PLAN_DETAILS,
- RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3,
- } from './ui/DiskManagement.constants'
- import { NoticeBar } from './ui/NoticeBar'
- import { SpendCapDisabledSection } from './ui/SpendCapDisabledSection'
- import {
- MAX_WIDTH_CLASSES,
- PADDING_CLASSES,
- ScaffoldContainer,
- } from '@/components/layouts/Scaffold'
- import { DocsButton } from '@/components/ui/DocsButton'
- import { RequestUpgradeToBillingOwners } from '@/components/ui/RequestUpgradeToBillingOwners'
- import { UpgradeToPro } from '@/components/ui/UpgradeToPro'
- import {
- useDiskAttributesQuery,
- useRemainingDurationForDiskAttributeUpdate,
- } from '@/data/config/disk-attributes-query'
- import { useUpdateDiskAttributesMutation } from '@/data/config/disk-attributes-update-mutation'
- import { useDiskAutoscaleCustomConfigQuery } from '@/data/config/disk-autoscale-config-query'
- import { useUpdateDiskAutoscaleConfigMutation } from '@/data/config/disk-autoscale-config-update-mutation'
- import { useDiskUtilizationQuery } from '@/data/config/disk-utilization-query'
- import { useSetProjectStatus } from '@/data/projects/project-detail-query'
- import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
- import { useProjectAddonUpdateMutation } from '@/data/subscriptions/project-addon-update-mutation'
- import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
- import { AddonVariantId } from '@/data/subscriptions/types'
- import { useResourceWarningsQuery } from '@/data/usage/resource-warnings-query'
- import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
- import {
- useIsAwsCloudProvider,
- useIsAwsK8sCloudProvider,
- useIsAwsNimbusCloudProvider,
- useSelectedProjectQuery,
- } from '@/hooks/misc/useSelectedProject'
- import { DOCS_URL, GB, PROJECT_STATUS } from '@/lib/constants'
- export function DiskManagementForm() {
- const { ref: projectRef } = useParams()
- const { data: project, isPending: isProjectPending } = useSelectedProjectQuery()
- const { data: org } = useSelectedOrganizationQuery()
- const { setProjectStatus } = useSetProjectStatus()
- const advancedSettingsRef = useRef<HTMLDivElement>(null)
- const isSpendCapEnabled =
- org?.plan.id !== 'free' && !org?.usage_billing_enabled && project?.cloud_provider !== 'FLY'
- const { data: resourceWarnings } = useResourceWarningsQuery({ ref: projectRef })
- // [Joshen Cleanup] JFYI this client side filtering can be cleaned up once BE changes are live which will only return the warnings based on the provided ref
- const projectResourceWarnings = (resourceWarnings ?? [])?.find(
- (warning) => warning.project === project?.ref
- )
- const isReadOnlyMode = projectResourceWarnings?.is_readonly_mode_enabled
- const isAws = useIsAwsCloudProvider()
- const isAwsK8s = useIsAwsK8sCloudProvider()
- const isAwsNimbus = useIsAwsNimbusCloudProvider()
- const { can: canUpdateDiskConfiguration, isSuccess: isPermissionsLoaded } =
- useAsyncCheckPermissions(PermissionAction.UPDATE, 'projects', {
- resource: {
- project_id: project?.id,
- },
- })
- const { hasAccess, isSuccess: isEntitlementsLoaded } = useCheckEntitlements(
- 'instances.compute_update_available_sizes'
- )
- const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false)
- const [refetchInterval, setRefetchInterval] = useState<number | false>(false)
- const [message, setMessageState] = useState<DiskManagementMessage | null>(null)
- const [advancedSettingsOpen, setAdvancedSettingsOpenState] = useState(false)
- const { data: databases, isSuccess: isReadReplicasSuccess } = useReadReplicasQuery({ projectRef })
- const { data, isSuccess: isDiskAttributesSuccess } = useDiskAttributesQuery(
- { projectRef },
- {
- refetchInterval,
- refetchOnWindowFocus: false,
- enabled: project != null && isAws,
- }
- )
- const { isSuccess: isAddonsSuccess } = useProjectAddonsQuery({ projectRef })
- const { isWithinCooldownWindow, isSuccess: isCooldownSuccess } =
- useRemainingDurationForDiskAttributeUpdate({
- projectRef,
- enabled: project != null && isAws,
- })
- const { data: diskUtil, isSuccess: isDiskUtilizationSuccess } = useDiskUtilizationQuery(
- {
- projectRef,
- },
- { enabled: project != null && isAws }
- )
- const { data: diskAutoscaleConfig, isSuccess: isDiskAutoscaleConfigSuccess } =
- useDiskAutoscaleCustomConfigQuery({ projectRef }, { enabled: project != null && isAws })
- const computeSize = project?.infra_compute_size
- ? mapComputeSizeNameToAddonVariantId(project?.infra_compute_size)
- : undefined
- // @ts-ignore
- const { type, iops, throughput_mbps, size_gb } = data?.attributes ?? { size_gb: 0, iops: 0 }
- const { growth_percent, max_size_gb, min_increment_gb } = diskAutoscaleConfig ?? {}
- const defaultValues = {
- storageType: type ?? DiskType.GP3,
- provisionedIOPS: iops,
- throughput: throughput_mbps,
- totalSize: size_gb,
- computeSize: computeSize ?? 'ci_micro',
- growthPercent: growth_percent,
- minIncrementGb: min_increment_gb,
- maxSizeGb: max_size_gb,
- }
- const form = useForm<DiskStorageSchemaType>({
- resolver: zodResolver(
- CreateDiskStorageSchema({
- defaultTotalSize: defaultValues.totalSize,
- cloudProvider: project?.cloud_provider as CloudProvider,
- isSpendCapEnabled,
- } as any)
- ),
- defaultValues,
- mode: 'onBlur',
- reValidateMode: 'onChange',
- })
- const { computeSize: modifiedComputeSize } = form.watch()
- const isSuccess =
- isAddonsSuccess &&
- isDiskAttributesSuccess &&
- isDiskUtilizationSuccess &&
- isReadReplicasSuccess &&
- isDiskAutoscaleConfigSuccess &&
- isCooldownSuccess
- const isRequestingChanges = data?.requested_modification !== undefined
- const readReplicas = (databases ?? []).filter((db) => db.identifier !== projectRef)
- const isPlanUpgradeRequired = !hasAccess
- const { formState } = form
- const errors = formState.errors
- const usedSize = Math.round(((diskUtil?.metrics.fs_used_bytes ?? 0) / GB) * 100) / 100
- const totalSize = formState.defaultValues?.totalSize || 0
- const usedPercentage = (usedSize / totalSize) * 100
- const disableIopsThroughputConfig =
- modifiedComputeSize &&
- !isSpendCapEnabled &&
- RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(modifiedComputeSize)
- const watchedTotalSize = form.watch('totalSize') ?? 0
- const watchedStorageType = form.watch('storageType')
- // Minimum disk size where the platform API will accept an IOPS payload (500 IOPS/GB rule).
- const minDiskSizeForCustomIops = calculateDiskSizeRequiredForIopsWithGp3(
- DISK_LIMITS[DiskType.GP3].minIops
- )
- // Suggested target when prompting a resize, sits above the floor so users
- // aren't pinned at the minimum during the 4-hour disk-config cooldown.
- const suggestedDiskSizeForCustomIops = PLAN_DETAILS.pro.includedDiskGB.gp3
- const isDiskTooSmallForCustomIops =
- watchedStorageType === 'gp3' && watchedTotalSize < minDiskSizeForCustomIops
- const isBranch = project?.parent_project_ref !== undefined
- const disableDiskSizeInput =
- isRequestingChanges ||
- isPlanUpgradeRequired ||
- isWithinCooldownWindow ||
- !canUpdateDiskConfiguration ||
- !isAws
- const disableDiskInputs = disableDiskSizeInput || isSpendCapEnabled
- const disableComputeInputs = isPlanUpgradeRequired
- const isDirty = !!Object.keys(form.formState.dirtyFields).length
- const isProjectResizing = project?.status === PROJECT_STATUS.RESIZING
- const isProjectRequestingDiskChanges = isRequestingChanges && !isProjectResizing
- const noPermissions = isPermissionsLoaded && !canUpdateDiskConfiguration
- const isDiskNoticeVisible = !isProjectPending && !(isAws || isAwsNimbus)
- const { mutateAsync: updateDiskConfiguration, isPending: isUpdatingDisk } =
- useUpdateDiskAttributesMutation({
- // this is to suppress to toast message
- onError: () => {},
- onSuccess: () => setRefetchInterval(2000),
- })
- const { mutateAsync: updateSubscriptionAddon, isPending: isUpdatingCompute } =
- useProjectAddonUpdateMutation({
- // this is to suppress to toast message
- onError: () => {},
- onSuccess: () => {
- //Manually set project status to RESIZING, Project status should be RESIZING on next project status request.
- if (projectRef) setProjectStatus({ ref: projectRef, status: PROJECT_STATUS.RESIZING })
- },
- })
- const { mutateAsync: updateDiskAutoscaleConfig, isPending: isUpdatingDiskAutoscaleConfig } =
- useUpdateDiskAutoscaleConfigMutation({
- // this is to suppress to toast message
- onError: () => {},
- })
- const isUpdatingConfig = isUpdatingDisk || isUpdatingCompute || isUpdatingDiskAutoscaleConfig
- const onSubmit = async (data: DiskStorageSchemaType) => {
- let payload = data
- let willUpdateDiskConfiguration = false
- setMessageState(null)
- // [Joshen] Skip disk configuration related stuff for AWS Nimbus
- try {
- if (
- !isAwsK8s &&
- !isAwsNimbus &&
- (payload.storageType !== form.formState.defaultValues?.storageType ||
- payload.provisionedIOPS !== form.formState.defaultValues?.provisionedIOPS ||
- payload.throughput !== form.formState.defaultValues?.throughput ||
- payload.totalSize !== form.formState.defaultValues?.totalSize)
- ) {
- willUpdateDiskConfiguration = true
- await updateDiskConfiguration({
- ref: projectRef,
- provisionedIOPS: payload.provisionedIOPS!,
- storageType: payload.storageType,
- totalSize: payload.totalSize!,
- throughput: payload.throughput,
- })
- }
- if (
- !isAwsK8s &&
- !isAwsNimbus &&
- (payload.growthPercent !== form.formState.defaultValues?.growthPercent ||
- payload.minIncrementGb !== form.formState.defaultValues?.minIncrementGb ||
- payload.maxSizeGb !== form.formState.defaultValues?.maxSizeGb)
- ) {
- await updateDiskAutoscaleConfig({
- projectRef,
- growthPercent: payload.growthPercent,
- minIncrementGb: payload.minIncrementGb,
- maxSizeGb: payload.maxSizeGb,
- })
- }
- if (payload.computeSize !== form.formState.defaultValues?.computeSize) {
- await updateSubscriptionAddon({
- projectRef: projectRef,
- // cast variant to AddonVariantId to satisfy type
- variant: payload.computeSize as AddonVariantId,
- type: 'compute_instance',
- suppressToast: true,
- })
- }
- setIsDialogOpen(false)
- form.reset(data as DiskStorageSchemaType)
- toast.success(
- `Successfully updated disk settings!${willUpdateDiskConfiguration ? ' The requested changes will be applied to your disk shortly.' : ''}`
- )
- } catch (error: unknown) {
- setMessageState({
- message: error instanceof Error ? error.message : 'An unknown error occurred',
- type: 'error',
- })
- }
- }
- useEffect(() => {
- if (!isDiskAttributesSuccess) return
- // @ts-ignore
- const { type, iops, throughput_mbps, size_gb } = data?.attributes ?? { size_gb: 0 }
- const formValues = {
- storageType: type,
- provisionedIOPS: iops,
- throughput: throughput_mbps,
- totalSize: size_gb,
- computeSize: form.getValues('computeSize'),
- }
- if (!('requested_modification' in data)) {
- if (refetchInterval !== false) {
- form.reset(formValues)
- setRefetchInterval(false)
- toast.success('Disk configuration changes have been successfully applied!')
- }
- } else {
- setRefetchInterval(2000)
- }
- }, [data, isDiskAttributesSuccess, form, refetchInterval])
- // We only support disk configurations for >=Large instances
- // If a customer downgrades back to <Large, we should reset the storage settings to avoid incurring unnecessary costs
- useEffect(() => {
- if (modifiedComputeSize && project?.infra_compute_size && isDialogOpen) {
- if (RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(modifiedComputeSize)) {
- form.setValue('storageType', DiskType.GP3)
- form.setValue('throughput', DISK_LIMITS['gp3'].minThroughput)
- form.setValue('provisionedIOPS', DISK_LIMITS['gp3'].minIops)
- }
- }
- }, [modifiedComputeSize, isDialogOpen, project])
- useEffect(() => {
- // Initialize field values properly when data has been loaded, preserving any user changes
- if (isDiskAttributesSuccess || isSuccess) {
- form.reset(defaultValues, {})
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isSuccess, isDiskAttributesSuccess])
- useEffect(() => {
- const fieldErrors = Object.keys(errors)
- if (fieldErrors.length > 0) {
- if (
- fieldErrors.includes('throughput') ||
- fieldErrors.includes('provisionedIOPS') ||
- fieldErrors.includes('maxSizeGb')
- ) {
- setAdvancedSettingsOpenState(true)
- // [Joshen] The timeout is to let the collapsible open prior to scrolling
- const timeoutId = setTimeout(() => {
- advancedSettingsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
- }, 100)
- return () => clearTimeout(timeoutId)
- }
- }
- }, [errors])
- return (
- <>
- <ScaffoldContainer className="relative flex flex-col gap-10" bottomPadding>
- {isEntitlementsLoaded && isPlanUpgradeRequired && (
- <UpgradeToPro
- featureProposition="configure compute and disk"
- primaryText="Only available on Pro Plan and above"
- secondaryText="Upgrade to the Pro Plan to configure compute and disk settings."
- />
- )}
- {(isProjectResizing ||
- isProjectRequestingDiskChanges ||
- (isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions)) && (
- <div className="relative flex flex-col gap-10">
- <DiskMangementRestartRequiredSection
- visible={isProjectResizing}
- title="Your project will now automatically restart."
- description="Your project will be unavailable for up to 2 mins."
- />
- <NoticeBar
- type="default"
- visible={isProjectRequestingDiskChanges}
- title="Disk configuration changes have been requested"
- description="The requested changes will be applied to your disk shortly"
- />
- <NoticeBar
- type="default"
- visible={isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions}
- title="You do not have permission to update disk configuration"
- description="Please contact your organization administrator to update your disk configuration"
- />
- </div>
- )}
- <Separator />
- </ScaffoldContainer>
- <Form {...form}>
- <form
- id="disk-compute-form"
- onSubmit={form.handleSubmit(onSubmit)}
- className="flex flex-col gap-8"
- >
- <ScaffoldContainer className="relative flex flex-col gap-10" bottomPadding>
- <ComputeSizeField form={form} disabled={disableComputeInputs} />
- {isDiskNoticeVisible && <Separator />}
- <SpendCapDisabledSection currentDiskSizeGb={defaultValues.totalSize} />
- <div className="flex flex-col gap-y-4">
- <NoticeBar
- type="default"
- visible={isDiskNoticeVisible}
- title="Disk configuration is only available for projects in the AWS cloud provider"
- description={
- isAwsK8s
- ? 'Configuring your disk for AWS (Revamped) projects is unavailable for now.'
- : isBranch
- ? 'Delete and recreate your Preview Branch to configure disk size. It was deployed on an older branching infrastructure.'
- : 'The Fly Postgres offering is deprecated - please migrate your instance to the AWS cloud prov to configure your disk.'
- }
- />
- {isAws && (
- <>
- <div className="flex flex-col gap-y-3">
- <DiskCountdownRadial />
- {!isReadOnlyMode && usedPercentage >= 90 && isWithinCooldownWindow && (
- <Admonition
- type="destructive"
- title="Database size is currently over 90% of disk size"
- description="Your project will enter read-only mode once you reach 95% of the disk space to prevent your database from exceeding the disk limitations"
- >
- <DocsButton
- abbrev={false}
- className="mt-2"
- href={`${DOCS_URL}/guides/platform/database-size#read-only-mode`}
- />
- </Admonition>
- )}
- {isReadOnlyMode && (
- <Admonition
- type="destructive"
- title="Project is currently in read-only mode"
- description="You will need to manually override read-only mode and reduce the database size to below 95% of the disk size"
- >
- <DocsButton
- abbrev={false}
- className="mt-2"
- href={`${DOCS_URL}/guides/platform/database-size#disabling-read-only-mode`}
- />
- </Admonition>
- )}
- </div>
- <DiskSizeField
- form={form}
- disableInput={disableDiskSizeInput}
- setAdvancedSettingsOpenState={setAdvancedSettingsOpenState}
- />
- </>
- )}
- </div>
- {isAws && (
- <>
- <Separator />
- <Collapsible
- open={advancedSettingsOpen}
- onOpenChange={() => setAdvancedSettingsOpenState((prev) => !prev)}
- >
- <CollapsibleTrigger className="px-card py-3 w-full border flex items-center gap-6 rounded-t data-closed:rounded-b group justify-between">
- <div className="flex flex-col items-start">
- <span className="text-sm text-foreground">Advanced disk settings</span>
- <span className="text-sm text-foreground-light text-left">
- Specify additional settings for your disk, including autoscaling
- configuration, IOPS, throughput, and disk type.
- </span>
- </div>
- <ChevronRight
- size={16}
- className="text-foreground-light transition-all group-data-open:rotate-90"
- strokeWidth={1}
- />
- </CollapsibleTrigger>
- <CollapsibleContent
- ref={advancedSettingsRef}
- className={cn(
- 'transition-all rounded-b',
- 'border border-t-0 data-closed:animate-collapsible-up data-open:animate-collapsible-down'
- )}
- >
- <div className="flex flex-col gap-y-8 py-8">
- <div className="px-card flex flex-col gap-y-8">
- <AutoScaleFields form={form} />
- </div>
- <DialogSectionSeparator />
- <div className="px-card flex flex-col gap-y-8">
- <NoticeBar
- type="default"
- visible={!!disableIopsThroughputConfig}
- title="Adjusting disk configuration requires LARGE Compute size or above"
- description={`Increase your compute size to adjust your disk's storage type, ${form.getValues('storageType') === 'gp3' ? 'IOPS, ' : ''} and throughput`}
- actions={
- canUpdateDiskConfiguration ? (
- <Button
- type="default"
- onClick={() => {
- form.setValue('computeSize', 'ci_large')
- }}
- >
- Change to LARGE Compute
- </Button>
- ) : (
- <RequestUpgradeToBillingOwners
- addon="computeSize"
- featureProposition="adjust disk configuration"
- />
- )
- }
- />
- <NoticeBar
- type="default"
- visible={
- isDiskTooSmallForCustomIops &&
- !disableIopsThroughputConfig &&
- !disableDiskInputs
- }
- title={`Increase disk size to adjust IOPS or throughput`}
- description={`This disk is too small to update IOPS or throughput, since gp3 volumes are capped at 500 IOPS per GB with a 3,000 IOPS minimum. Resizing to ${suggestedDiskSizeForCustomIops} GB unlocks custom IOPS and throughput, and leaves headroom for further adjustments (disk config changes are locked for 4 hours after each resize).`}
- actions={
- !disableDiskSizeInput ? (
- <Button
- type="default"
- onClick={() => {
- form.setValue('totalSize', suggestedDiskSizeForCustomIops, {
- shouldDirty: true,
- shouldValidate: true,
- })
- }}
- >
- Increase to {suggestedDiskSizeForCustomIops} GB
- </Button>
- ) : undefined
- }
- />
- <StorageTypeField
- form={form}
- disableInput={disableIopsThroughputConfig || disableDiskInputs}
- />
- <IOPSField
- form={form}
- disableInput={
- disableIopsThroughputConfig ||
- disableDiskInputs ||
- isDiskTooSmallForCustomIops
- }
- />
- <ThroughputField
- form={form}
- disableInput={
- disableIopsThroughputConfig ||
- disableDiskInputs ||
- isDiskTooSmallForCustomIops
- }
- />
- </div>
- </div>
- </CollapsibleContent>
- </Collapsible>
- </>
- )}
- </ScaffoldContainer>
- <AnimatePresence>
- {isDirty ? (
- <motion.div
- initial={{ opacity: 0, y: 20 }}
- animate={{ opacity: 1, y: 0 }}
- exit={{ opacity: 0, y: 20 }}
- transition={{ duration: 0.1, delay: 0.2 }}
- className="z-10 w-full left-0 right-0 sticky bottom-0 bg-surface-100 border-t h-16 items-center flex"
- >
- <div
- className={cn(
- MAX_WIDTH_CLASSES,
- PADDING_CLASSES,
- 'flex items-center gap-3 justify-end'
- )}
- >
- <FormFooterChangeBadge formState={formState} />
- <Button
- type="default"
- onClick={() => form.reset()}
- disabled={!isDirty}
- size="medium"
- >
- Cancel
- </Button>
- <DiskManagementReviewAndSubmitDialog
- loading={isUpdatingConfig}
- disabled={noPermissions}
- form={form}
- numReplicas={readReplicas.length}
- isDialogOpen={isDialogOpen}
- onSubmit={onSubmit}
- setIsDialogOpen={setIsDialogOpen}
- message={message}
- />
- </div>
- </motion.div>
- ) : null}
- </AnimatePresence>
- </form>
- </Form>
- </>
- )
- }
|