DiskManagementForm.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. // @ts-nocheck
  2. import { zodResolver } from '@hookform/resolvers/zod'
  3. import { PermissionAction } from '@supabase/shared-types/out/constants'
  4. import { useParams } from 'common'
  5. import { AnimatePresence, motion } from 'framer-motion'
  6. import { ChevronRight } from 'lucide-react'
  7. import { useEffect, useRef, useState } from 'react'
  8. import { useForm } from 'react-hook-form'
  9. import { CloudProvider } from 'shared-data'
  10. import { toast } from 'sonner'
  11. import {
  12. Button,
  13. cn,
  14. Collapsible,
  15. CollapsibleContent,
  16. CollapsibleTrigger,
  17. DialogSectionSeparator,
  18. Form,
  19. Separator,
  20. } from 'ui'
  21. import { Admonition } from 'ui-patterns'
  22. import { FormFooterChangeBadge } from '../DataWarehouse/FormFooterChangeBadge'
  23. import { CreateDiskStorageSchema, DiskStorageSchemaType } from './DiskManagement.schema'
  24. import { DiskManagementMessage } from './DiskManagement.types'
  25. import {
  26. calculateDiskSizeRequiredForIopsWithGp3,
  27. mapComputeSizeNameToAddonVariantId,
  28. } from './DiskManagement.utils'
  29. import { DiskMangementRestartRequiredSection } from './DiskManagementRestartRequiredSection'
  30. import { DiskManagementReviewAndSubmitDialog } from './DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog'
  31. import { AutoScaleFields } from './fields/AutoScaleFields'
  32. import { ComputeSizeField } from './fields/ComputeSizeField'
  33. import { DiskSizeField } from './fields/DiskSizeField'
  34. import { IOPSField } from './fields/IOPSField'
  35. import { StorageTypeField } from './fields/StorageTypeField'
  36. import { ThroughputField } from './fields/ThroughputField'
  37. import { DiskCountdownRadial } from './ui/DiskCountdownRadial'
  38. import {
  39. DISK_LIMITS,
  40. DiskType,
  41. PLAN_DETAILS,
  42. RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3,
  43. } from './ui/DiskManagement.constants'
  44. import { NoticeBar } from './ui/NoticeBar'
  45. import { SpendCapDisabledSection } from './ui/SpendCapDisabledSection'
  46. import {
  47. MAX_WIDTH_CLASSES,
  48. PADDING_CLASSES,
  49. ScaffoldContainer,
  50. } from '@/components/layouts/Scaffold'
  51. import { DocsButton } from '@/components/ui/DocsButton'
  52. import { RequestUpgradeToBillingOwners } from '@/components/ui/RequestUpgradeToBillingOwners'
  53. import { UpgradeToPro } from '@/components/ui/UpgradeToPro'
  54. import {
  55. useDiskAttributesQuery,
  56. useRemainingDurationForDiskAttributeUpdate,
  57. } from '@/data/config/disk-attributes-query'
  58. import { useUpdateDiskAttributesMutation } from '@/data/config/disk-attributes-update-mutation'
  59. import { useDiskAutoscaleCustomConfigQuery } from '@/data/config/disk-autoscale-config-query'
  60. import { useUpdateDiskAutoscaleConfigMutation } from '@/data/config/disk-autoscale-config-update-mutation'
  61. import { useDiskUtilizationQuery } from '@/data/config/disk-utilization-query'
  62. import { useSetProjectStatus } from '@/data/projects/project-detail-query'
  63. import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
  64. import { useProjectAddonUpdateMutation } from '@/data/subscriptions/project-addon-update-mutation'
  65. import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
  66. import { AddonVariantId } from '@/data/subscriptions/types'
  67. import { useResourceWarningsQuery } from '@/data/usage/resource-warnings-query'
  68. import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
  69. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  70. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  71. import {
  72. useIsAwsCloudProvider,
  73. useIsAwsK8sCloudProvider,
  74. useIsAwsNimbusCloudProvider,
  75. useSelectedProjectQuery,
  76. } from '@/hooks/misc/useSelectedProject'
  77. import { DOCS_URL, GB, PROJECT_STATUS } from '@/lib/constants'
  78. export function DiskManagementForm() {
  79. const { ref: projectRef } = useParams()
  80. const { data: project, isPending: isProjectPending } = useSelectedProjectQuery()
  81. const { data: org } = useSelectedOrganizationQuery()
  82. const { setProjectStatus } = useSetProjectStatus()
  83. const advancedSettingsRef = useRef<HTMLDivElement>(null)
  84. const isSpendCapEnabled =
  85. org?.plan.id !== 'free' && !org?.usage_billing_enabled && project?.cloud_provider !== 'FLY'
  86. const { data: resourceWarnings } = useResourceWarningsQuery({ ref: projectRef })
  87. // [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
  88. const projectResourceWarnings = (resourceWarnings ?? [])?.find(
  89. (warning) => warning.project === project?.ref
  90. )
  91. const isReadOnlyMode = projectResourceWarnings?.is_readonly_mode_enabled
  92. const isAws = useIsAwsCloudProvider()
  93. const isAwsK8s = useIsAwsK8sCloudProvider()
  94. const isAwsNimbus = useIsAwsNimbusCloudProvider()
  95. const { can: canUpdateDiskConfiguration, isSuccess: isPermissionsLoaded } =
  96. useAsyncCheckPermissions(PermissionAction.UPDATE, 'projects', {
  97. resource: {
  98. project_id: project?.id,
  99. },
  100. })
  101. const { hasAccess, isSuccess: isEntitlementsLoaded } = useCheckEntitlements(
  102. 'instances.compute_update_available_sizes'
  103. )
  104. const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false)
  105. const [refetchInterval, setRefetchInterval] = useState<number | false>(false)
  106. const [message, setMessageState] = useState<DiskManagementMessage | null>(null)
  107. const [advancedSettingsOpen, setAdvancedSettingsOpenState] = useState(false)
  108. const { data: databases, isSuccess: isReadReplicasSuccess } = useReadReplicasQuery({ projectRef })
  109. const { data, isSuccess: isDiskAttributesSuccess } = useDiskAttributesQuery(
  110. { projectRef },
  111. {
  112. refetchInterval,
  113. refetchOnWindowFocus: false,
  114. enabled: project != null && isAws,
  115. }
  116. )
  117. const { isSuccess: isAddonsSuccess } = useProjectAddonsQuery({ projectRef })
  118. const { isWithinCooldownWindow, isSuccess: isCooldownSuccess } =
  119. useRemainingDurationForDiskAttributeUpdate({
  120. projectRef,
  121. enabled: project != null && isAws,
  122. })
  123. const { data: diskUtil, isSuccess: isDiskUtilizationSuccess } = useDiskUtilizationQuery(
  124. {
  125. projectRef,
  126. },
  127. { enabled: project != null && isAws }
  128. )
  129. const { data: diskAutoscaleConfig, isSuccess: isDiskAutoscaleConfigSuccess } =
  130. useDiskAutoscaleCustomConfigQuery({ projectRef }, { enabled: project != null && isAws })
  131. const computeSize = project?.infra_compute_size
  132. ? mapComputeSizeNameToAddonVariantId(project?.infra_compute_size)
  133. : undefined
  134. // @ts-ignore
  135. const { type, iops, throughput_mbps, size_gb } = data?.attributes ?? { size_gb: 0, iops: 0 }
  136. const { growth_percent, max_size_gb, min_increment_gb } = diskAutoscaleConfig ?? {}
  137. const defaultValues = {
  138. storageType: type ?? DiskType.GP3,
  139. provisionedIOPS: iops,
  140. throughput: throughput_mbps,
  141. totalSize: size_gb,
  142. computeSize: computeSize ?? 'ci_micro',
  143. growthPercent: growth_percent,
  144. minIncrementGb: min_increment_gb,
  145. maxSizeGb: max_size_gb,
  146. }
  147. const form = useForm<DiskStorageSchemaType>({
  148. resolver: zodResolver(
  149. CreateDiskStorageSchema({
  150. defaultTotalSize: defaultValues.totalSize,
  151. cloudProvider: project?.cloud_provider as CloudProvider,
  152. isSpendCapEnabled,
  153. } as any)
  154. ),
  155. defaultValues,
  156. mode: 'onBlur',
  157. reValidateMode: 'onChange',
  158. })
  159. const { computeSize: modifiedComputeSize } = form.watch()
  160. const isSuccess =
  161. isAddonsSuccess &&
  162. isDiskAttributesSuccess &&
  163. isDiskUtilizationSuccess &&
  164. isReadReplicasSuccess &&
  165. isDiskAutoscaleConfigSuccess &&
  166. isCooldownSuccess
  167. const isRequestingChanges = data?.requested_modification !== undefined
  168. const readReplicas = (databases ?? []).filter((db) => db.identifier !== projectRef)
  169. const isPlanUpgradeRequired = !hasAccess
  170. const { formState } = form
  171. const errors = formState.errors
  172. const usedSize = Math.round(((diskUtil?.metrics.fs_used_bytes ?? 0) / GB) * 100) / 100
  173. const totalSize = formState.defaultValues?.totalSize || 0
  174. const usedPercentage = (usedSize / totalSize) * 100
  175. const disableIopsThroughputConfig =
  176. modifiedComputeSize &&
  177. !isSpendCapEnabled &&
  178. RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(modifiedComputeSize)
  179. const watchedTotalSize = form.watch('totalSize') ?? 0
  180. const watchedStorageType = form.watch('storageType')
  181. // Minimum disk size where the platform API will accept an IOPS payload (500 IOPS/GB rule).
  182. const minDiskSizeForCustomIops = calculateDiskSizeRequiredForIopsWithGp3(
  183. DISK_LIMITS[DiskType.GP3].minIops
  184. )
  185. // Suggested target when prompting a resize, sits above the floor so users
  186. // aren't pinned at the minimum during the 4-hour disk-config cooldown.
  187. const suggestedDiskSizeForCustomIops = PLAN_DETAILS.pro.includedDiskGB.gp3
  188. const isDiskTooSmallForCustomIops =
  189. watchedStorageType === 'gp3' && watchedTotalSize < minDiskSizeForCustomIops
  190. const isBranch = project?.parent_project_ref !== undefined
  191. const disableDiskSizeInput =
  192. isRequestingChanges ||
  193. isPlanUpgradeRequired ||
  194. isWithinCooldownWindow ||
  195. !canUpdateDiskConfiguration ||
  196. !isAws
  197. const disableDiskInputs = disableDiskSizeInput || isSpendCapEnabled
  198. const disableComputeInputs = isPlanUpgradeRequired
  199. const isDirty = !!Object.keys(form.formState.dirtyFields).length
  200. const isProjectResizing = project?.status === PROJECT_STATUS.RESIZING
  201. const isProjectRequestingDiskChanges = isRequestingChanges && !isProjectResizing
  202. const noPermissions = isPermissionsLoaded && !canUpdateDiskConfiguration
  203. const isDiskNoticeVisible = !isProjectPending && !(isAws || isAwsNimbus)
  204. const { mutateAsync: updateDiskConfiguration, isPending: isUpdatingDisk } =
  205. useUpdateDiskAttributesMutation({
  206. // this is to suppress to toast message
  207. onError: () => {},
  208. onSuccess: () => setRefetchInterval(2000),
  209. })
  210. const { mutateAsync: updateSubscriptionAddon, isPending: isUpdatingCompute } =
  211. useProjectAddonUpdateMutation({
  212. // this is to suppress to toast message
  213. onError: () => {},
  214. onSuccess: () => {
  215. //Manually set project status to RESIZING, Project status should be RESIZING on next project status request.
  216. if (projectRef) setProjectStatus({ ref: projectRef, status: PROJECT_STATUS.RESIZING })
  217. },
  218. })
  219. const { mutateAsync: updateDiskAutoscaleConfig, isPending: isUpdatingDiskAutoscaleConfig } =
  220. useUpdateDiskAutoscaleConfigMutation({
  221. // this is to suppress to toast message
  222. onError: () => {},
  223. })
  224. const isUpdatingConfig = isUpdatingDisk || isUpdatingCompute || isUpdatingDiskAutoscaleConfig
  225. const onSubmit = async (data: DiskStorageSchemaType) => {
  226. let payload = data
  227. let willUpdateDiskConfiguration = false
  228. setMessageState(null)
  229. // [Joshen] Skip disk configuration related stuff for AWS Nimbus
  230. try {
  231. if (
  232. !isAwsK8s &&
  233. !isAwsNimbus &&
  234. (payload.storageType !== form.formState.defaultValues?.storageType ||
  235. payload.provisionedIOPS !== form.formState.defaultValues?.provisionedIOPS ||
  236. payload.throughput !== form.formState.defaultValues?.throughput ||
  237. payload.totalSize !== form.formState.defaultValues?.totalSize)
  238. ) {
  239. willUpdateDiskConfiguration = true
  240. await updateDiskConfiguration({
  241. ref: projectRef,
  242. provisionedIOPS: payload.provisionedIOPS!,
  243. storageType: payload.storageType,
  244. totalSize: payload.totalSize!,
  245. throughput: payload.throughput,
  246. })
  247. }
  248. if (
  249. !isAwsK8s &&
  250. !isAwsNimbus &&
  251. (payload.growthPercent !== form.formState.defaultValues?.growthPercent ||
  252. payload.minIncrementGb !== form.formState.defaultValues?.minIncrementGb ||
  253. payload.maxSizeGb !== form.formState.defaultValues?.maxSizeGb)
  254. ) {
  255. await updateDiskAutoscaleConfig({
  256. projectRef,
  257. growthPercent: payload.growthPercent,
  258. minIncrementGb: payload.minIncrementGb,
  259. maxSizeGb: payload.maxSizeGb,
  260. })
  261. }
  262. if (payload.computeSize !== form.formState.defaultValues?.computeSize) {
  263. await updateSubscriptionAddon({
  264. projectRef: projectRef,
  265. // cast variant to AddonVariantId to satisfy type
  266. variant: payload.computeSize as AddonVariantId,
  267. type: 'compute_instance',
  268. suppressToast: true,
  269. })
  270. }
  271. setIsDialogOpen(false)
  272. form.reset(data as DiskStorageSchemaType)
  273. toast.success(
  274. `Successfully updated disk settings!${willUpdateDiskConfiguration ? ' The requested changes will be applied to your disk shortly.' : ''}`
  275. )
  276. } catch (error: unknown) {
  277. setMessageState({
  278. message: error instanceof Error ? error.message : 'An unknown error occurred',
  279. type: 'error',
  280. })
  281. }
  282. }
  283. useEffect(() => {
  284. if (!isDiskAttributesSuccess) return
  285. // @ts-ignore
  286. const { type, iops, throughput_mbps, size_gb } = data?.attributes ?? { size_gb: 0 }
  287. const formValues = {
  288. storageType: type,
  289. provisionedIOPS: iops,
  290. throughput: throughput_mbps,
  291. totalSize: size_gb,
  292. computeSize: form.getValues('computeSize'),
  293. }
  294. if (!('requested_modification' in data)) {
  295. if (refetchInterval !== false) {
  296. form.reset(formValues)
  297. setRefetchInterval(false)
  298. toast.success('Disk configuration changes have been successfully applied!')
  299. }
  300. } else {
  301. setRefetchInterval(2000)
  302. }
  303. }, [data, isDiskAttributesSuccess, form, refetchInterval])
  304. // We only support disk configurations for >=Large instances
  305. // If a customer downgrades back to <Large, we should reset the storage settings to avoid incurring unnecessary costs
  306. useEffect(() => {
  307. if (modifiedComputeSize && project?.infra_compute_size && isDialogOpen) {
  308. if (RESTRICTED_COMPUTE_FOR_THROUGHPUT_ON_GP3.includes(modifiedComputeSize)) {
  309. form.setValue('storageType', DiskType.GP3)
  310. form.setValue('throughput', DISK_LIMITS['gp3'].minThroughput)
  311. form.setValue('provisionedIOPS', DISK_LIMITS['gp3'].minIops)
  312. }
  313. }
  314. }, [modifiedComputeSize, isDialogOpen, project])
  315. useEffect(() => {
  316. // Initialize field values properly when data has been loaded, preserving any user changes
  317. if (isDiskAttributesSuccess || isSuccess) {
  318. form.reset(defaultValues, {})
  319. }
  320. // eslint-disable-next-line react-hooks/exhaustive-deps
  321. }, [isSuccess, isDiskAttributesSuccess])
  322. useEffect(() => {
  323. const fieldErrors = Object.keys(errors)
  324. if (fieldErrors.length > 0) {
  325. if (
  326. fieldErrors.includes('throughput') ||
  327. fieldErrors.includes('provisionedIOPS') ||
  328. fieldErrors.includes('maxSizeGb')
  329. ) {
  330. setAdvancedSettingsOpenState(true)
  331. // [Joshen] The timeout is to let the collapsible open prior to scrolling
  332. const timeoutId = setTimeout(() => {
  333. advancedSettingsRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
  334. }, 100)
  335. return () => clearTimeout(timeoutId)
  336. }
  337. }
  338. }, [errors])
  339. return (
  340. <>
  341. <ScaffoldContainer className="relative flex flex-col gap-10" bottomPadding>
  342. {isEntitlementsLoaded && isPlanUpgradeRequired && (
  343. <UpgradeToPro
  344. featureProposition="configure compute and disk"
  345. primaryText="Only available on Pro Plan and above"
  346. secondaryText="Upgrade to the Pro Plan to configure compute and disk settings."
  347. />
  348. )}
  349. {(isProjectResizing ||
  350. isProjectRequestingDiskChanges ||
  351. (isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions)) && (
  352. <div className="relative flex flex-col gap-10">
  353. <DiskMangementRestartRequiredSection
  354. visible={isProjectResizing}
  355. title="Your project will now automatically restart."
  356. description="Your project will be unavailable for up to 2 mins."
  357. />
  358. <NoticeBar
  359. type="default"
  360. visible={isProjectRequestingDiskChanges}
  361. title="Disk configuration changes have been requested"
  362. description="The requested changes will be applied to your disk shortly"
  363. />
  364. <NoticeBar
  365. type="default"
  366. visible={isEntitlementsLoaded && !isPlanUpgradeRequired && noPermissions}
  367. title="You do not have permission to update disk configuration"
  368. description="Please contact your organization administrator to update your disk configuration"
  369. />
  370. </div>
  371. )}
  372. <Separator />
  373. </ScaffoldContainer>
  374. <Form {...form}>
  375. <form
  376. id="disk-compute-form"
  377. onSubmit={form.handleSubmit(onSubmit)}
  378. className="flex flex-col gap-8"
  379. >
  380. <ScaffoldContainer className="relative flex flex-col gap-10" bottomPadding>
  381. <ComputeSizeField form={form} disabled={disableComputeInputs} />
  382. {isDiskNoticeVisible && <Separator />}
  383. <SpendCapDisabledSection currentDiskSizeGb={defaultValues.totalSize} />
  384. <div className="flex flex-col gap-y-4">
  385. <NoticeBar
  386. type="default"
  387. visible={isDiskNoticeVisible}
  388. title="Disk configuration is only available for projects in the AWS cloud provider"
  389. description={
  390. isAwsK8s
  391. ? 'Configuring your disk for AWS (Revamped) projects is unavailable for now.'
  392. : isBranch
  393. ? 'Delete and recreate your Preview Branch to configure disk size. It was deployed on an older branching infrastructure.'
  394. : 'The Fly Postgres offering is deprecated - please migrate your instance to the AWS cloud prov to configure your disk.'
  395. }
  396. />
  397. {isAws && (
  398. <>
  399. <div className="flex flex-col gap-y-3">
  400. <DiskCountdownRadial />
  401. {!isReadOnlyMode && usedPercentage >= 90 && isWithinCooldownWindow && (
  402. <Admonition
  403. type="destructive"
  404. title="Database size is currently over 90% of disk size"
  405. 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"
  406. >
  407. <DocsButton
  408. abbrev={false}
  409. className="mt-2"
  410. href={`${DOCS_URL}/guides/platform/database-size#read-only-mode`}
  411. />
  412. </Admonition>
  413. )}
  414. {isReadOnlyMode && (
  415. <Admonition
  416. type="destructive"
  417. title="Project is currently in read-only mode"
  418. description="You will need to manually override read-only mode and reduce the database size to below 95% of the disk size"
  419. >
  420. <DocsButton
  421. abbrev={false}
  422. className="mt-2"
  423. href={`${DOCS_URL}/guides/platform/database-size#disabling-read-only-mode`}
  424. />
  425. </Admonition>
  426. )}
  427. </div>
  428. <DiskSizeField
  429. form={form}
  430. disableInput={disableDiskSizeInput}
  431. setAdvancedSettingsOpenState={setAdvancedSettingsOpenState}
  432. />
  433. </>
  434. )}
  435. </div>
  436. {isAws && (
  437. <>
  438. <Separator />
  439. <Collapsible
  440. open={advancedSettingsOpen}
  441. onOpenChange={() => setAdvancedSettingsOpenState((prev) => !prev)}
  442. >
  443. <CollapsibleTrigger className="px-card py-3 w-full border flex items-center gap-6 rounded-t data-closed:rounded-b group justify-between">
  444. <div className="flex flex-col items-start">
  445. <span className="text-sm text-foreground">Advanced disk settings</span>
  446. <span className="text-sm text-foreground-light text-left">
  447. Specify additional settings for your disk, including autoscaling
  448. configuration, IOPS, throughput, and disk type.
  449. </span>
  450. </div>
  451. <ChevronRight
  452. size={16}
  453. className="text-foreground-light transition-all group-data-open:rotate-90"
  454. strokeWidth={1}
  455. />
  456. </CollapsibleTrigger>
  457. <CollapsibleContent
  458. ref={advancedSettingsRef}
  459. className={cn(
  460. 'transition-all rounded-b',
  461. 'border border-t-0 data-closed:animate-collapsible-up data-open:animate-collapsible-down'
  462. )}
  463. >
  464. <div className="flex flex-col gap-y-8 py-8">
  465. <div className="px-card flex flex-col gap-y-8">
  466. <AutoScaleFields form={form} />
  467. </div>
  468. <DialogSectionSeparator />
  469. <div className="px-card flex flex-col gap-y-8">
  470. <NoticeBar
  471. type="default"
  472. visible={!!disableIopsThroughputConfig}
  473. title="Adjusting disk configuration requires LARGE Compute size or above"
  474. description={`Increase your compute size to adjust your disk's storage type, ${form.getValues('storageType') === 'gp3' ? 'IOPS, ' : ''} and throughput`}
  475. actions={
  476. canUpdateDiskConfiguration ? (
  477. <Button
  478. type="default"
  479. onClick={() => {
  480. form.setValue('computeSize', 'ci_large')
  481. }}
  482. >
  483. Change to LARGE Compute
  484. </Button>
  485. ) : (
  486. <RequestUpgradeToBillingOwners
  487. addon="computeSize"
  488. featureProposition="adjust disk configuration"
  489. />
  490. )
  491. }
  492. />
  493. <NoticeBar
  494. type="default"
  495. visible={
  496. isDiskTooSmallForCustomIops &&
  497. !disableIopsThroughputConfig &&
  498. !disableDiskInputs
  499. }
  500. title={`Increase disk size to adjust IOPS or throughput`}
  501. 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).`}
  502. actions={
  503. !disableDiskSizeInput ? (
  504. <Button
  505. type="default"
  506. onClick={() => {
  507. form.setValue('totalSize', suggestedDiskSizeForCustomIops, {
  508. shouldDirty: true,
  509. shouldValidate: true,
  510. })
  511. }}
  512. >
  513. Increase to {suggestedDiskSizeForCustomIops} GB
  514. </Button>
  515. ) : undefined
  516. }
  517. />
  518. <StorageTypeField
  519. form={form}
  520. disableInput={disableIopsThroughputConfig || disableDiskInputs}
  521. />
  522. <IOPSField
  523. form={form}
  524. disableInput={
  525. disableIopsThroughputConfig ||
  526. disableDiskInputs ||
  527. isDiskTooSmallForCustomIops
  528. }
  529. />
  530. <ThroughputField
  531. form={form}
  532. disableInput={
  533. disableIopsThroughputConfig ||
  534. disableDiskInputs ||
  535. isDiskTooSmallForCustomIops
  536. }
  537. />
  538. </div>
  539. </div>
  540. </CollapsibleContent>
  541. </Collapsible>
  542. </>
  543. )}
  544. </ScaffoldContainer>
  545. <AnimatePresence>
  546. {isDirty ? (
  547. <motion.div
  548. initial={{ opacity: 0, y: 20 }}
  549. animate={{ opacity: 1, y: 0 }}
  550. exit={{ opacity: 0, y: 20 }}
  551. transition={{ duration: 0.1, delay: 0.2 }}
  552. className="z-10 w-full left-0 right-0 sticky bottom-0 bg-surface-100 border-t h-16 items-center flex"
  553. >
  554. <div
  555. className={cn(
  556. MAX_WIDTH_CLASSES,
  557. PADDING_CLASSES,
  558. 'flex items-center gap-3 justify-end'
  559. )}
  560. >
  561. <FormFooterChangeBadge formState={formState} />
  562. <Button
  563. type="default"
  564. onClick={() => form.reset()}
  565. disabled={!isDirty}
  566. size="medium"
  567. >
  568. Cancel
  569. </Button>
  570. <DiskManagementReviewAndSubmitDialog
  571. loading={isUpdatingConfig}
  572. disabled={noPermissions}
  573. form={form}
  574. numReplicas={readReplicas.length}
  575. isDialogOpen={isDialogOpen}
  576. onSubmit={onSubmit}
  577. setIsDialogOpen={setIsDialogOpen}
  578. message={message}
  579. />
  580. </div>
  581. </motion.div>
  582. ) : null}
  583. </AnimatePresence>
  584. </form>
  585. </Form>
  586. </>
  587. )
  588. }