Files
supabase/apps/studio/components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog.hooks.ts
kemal.earth 7d369571ce feat(studio): revamp compute upgrade review dialog (#44639)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

This takes our dialog to review compute upgrade's and gives it a bit
more visual clarity (as it can get confusing). Simplification and clear
communication of what differences there are.

| Before | After |
|--------|--------|
| <img width="663" height="722" alt="Screenshot 2026-04-07 at 16 58 33"
src="https://github.com/user-attachments/assets/dbb699b4-89ad-4172-8c23-e5d1ca5045f8"
/> | <img width="608" height="635" alt="Screenshot 2026-04-08 at 12 28
32"
src="https://github.com/user-attachments/assets/9a4a5952-6049-4cda-86e6-73773f001010"
/> |




<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **UI Improvements**
* Redesigned disk review dialog to a stacked "Before/After" layout with
a clear vertical breakdown of changes.
* Replaced badges/tooltips with inline per-line monthly deltas and
explicit summed totals.
* Replica note moved inline; cooldown and throughput explanations
consolidated and more broadly shown when storage type changes.
  * Arrow between totals updated and "After" state emphasized.

* **Chores**
* Updated dialog spacing, padding, min-width, confirm button styling,
and dialog wording.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-08 16:58:58 +01:00

187 lines
6.1 KiB
TypeScript

import { useMemo } from 'react'
import { UseFormReturn } from 'react-hook-form'
import { DiskStorageSchemaType } from '../DiskManagement.schema'
import {
calculateComputeSizePrice,
calculateDiskSizePrice,
calculateIOPSPrice,
calculateThroughputPrice,
getAvailableComputeOptions,
mapAddOnVariantIdToComputeSize,
} from '../DiskManagement.utils'
import { DiskType } from '../ui/DiskManagement.constants'
import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import {
useIsAwsNimbusCloudProvider,
useSelectedProjectQuery,
} from '@/hooks/misc/useSelectedProject'
export function useDiskManagementReviewChanges(
form: UseFormReturn<DiskStorageSchemaType>,
numReplicas: number
) {
const { data: project } = useSelectedProjectQuery()
const { data: org } = useSelectedOrganizationQuery()
const isAwsNimbus = useIsAwsNimbusCloudProvider()
const { data: addons } = useProjectAddonsQuery({ projectRef: project?.ref })
const isAwsK8sProject = project?.cloud_provider === 'AWS_K8S'
const planId = org?.plan.id ?? 'free'
const availableAddons = useMemo(() => addons?.available_addons ?? [], [addons])
const availableOptions = useMemo(
() => getAvailableComputeOptions(availableAddons, project?.cloud_provider),
[availableAddons, project?.cloud_provider]
)
// --- Prices ---
const computeSizePrice = calculateComputeSizePrice({
availableOptions,
oldComputeSize: form.formState.defaultValues?.computeSize || 'ci_micro',
newComputeSize: form.getValues('computeSize'),
plan: planId,
})
const diskSizePrice = calculateDiskSizePrice({
planId,
oldSize: form.formState.defaultValues?.totalSize || 0,
oldStorageType: form.formState.defaultValues?.storageType as DiskType,
newSize: form.getValues('totalSize'),
newStorageType: form.getValues('storageType') as DiskType,
numReplicas,
})
const iopsPrice = calculateIOPSPrice({
oldStorageType: form.formState.defaultValues?.storageType as DiskType,
oldProvisionedIOPS: form.formState.defaultValues?.provisionedIOPS || 0,
newStorageType: form.getValues('storageType') as DiskType,
newProvisionedIOPS: form.getValues('provisionedIOPS'),
numReplicas,
})
const throughputPrice = calculateThroughputPrice({
storageType: form.getValues('storageType') as DiskType,
newThroughput: form.getValues('throughput') || 0,
oldThroughput: form.formState.defaultValues?.throughput || 0,
numReplicas,
})
const totalBeforePrice =
Number(computeSizePrice.oldPrice) +
Number(diskSizePrice.oldPrice) +
Number(iopsPrice.oldPrice) +
Number(throughputPrice.oldPrice)
const totalAfterPrice =
Number(computeSizePrice.newPrice) +
Number(diskSizePrice.newPrice) +
Number(iopsPrice.newPrice) +
Number(throughputPrice.newPrice)
// --- Change flags ---
const hasComputeChanges =
form.formState.defaultValues?.computeSize !== form.getValues('computeSize')
const hasTotalSizeChanges =
!isAwsK8sProject &&
!isAwsNimbus &&
form.formState.defaultValues?.totalSize !== form.getValues('totalSize')
const hasStorageTypeChanges =
!isAwsK8sProject &&
!isAwsNimbus &&
form.formState.defaultValues?.storageType !== form.getValues('storageType')
const hasThroughputChanges =
!isAwsK8sProject &&
!isAwsNimbus &&
form.formState.defaultValues?.throughput !== form.getValues('throughput')
const hasIOPSChanges =
!isAwsK8sProject &&
!isAwsNimbus &&
form.formState.defaultValues?.provisionedIOPS !== form.getValues('provisionedIOPS')
const hasGrowthPercentChanges =
!isAwsK8sProject &&
!isAwsNimbus &&
form.formState.defaultValues?.growthPercent !== form.getValues('growthPercent')
const hasMinIncrementChanges =
!isAwsK8sProject &&
!isAwsNimbus &&
form.formState.defaultValues?.minIncrementGb !== form.getValues('minIncrementGb')
const hasMaxSizeChanges =
!isAwsK8sProject &&
!isAwsNimbus &&
form.formState.defaultValues?.maxSizeGb !== form.getValues('maxSizeGb')
// --- Derived predicates ---
const storageTypeBefore = (form.formState.defaultValues?.storageType ?? '') as DiskType
const storageTypeAfter = form.getValues('storageType') as DiskType
// Show hero whenever any line-item price actually changes, not just compute
const anyBillableDiskChange =
Number(diskSizePrice.newPrice) !== Number(diskSizePrice.oldPrice) ||
Number(iopsPrice.newPrice) !== Number(iopsPrice.oldPrice) ||
Number(throughputPrice.newPrice) !== Number(throughputPrice.oldPrice)
// Show cooldown warning whenever any disk attribute that enforces the 4-hour lock changes
const anyDiskAttributeChange = hasIOPSChanges || hasStorageTypeChanges || hasTotalSizeChanges
// Show throughput row whenever either the before or after storage type is GP3
// (covers GP3→IO2 where the throughput charge drops to zero)
const showThroughputRow =
!isAwsK8sProject &&
!isAwsNimbus &&
(storageTypeBefore === 'gp3' || storageTypeAfter === 'gp3') &&
(hasThroughputChanges || hasStorageTypeChanges)
const hasAnyBreakdownRows =
hasComputeChanges ||
hasStorageTypeChanges ||
hasIOPSChanges ||
showThroughputRow ||
hasTotalSizeChanges ||
hasGrowthPercentChanges ||
hasMinIncrementChanges ||
hasMaxSizeChanges
// --- Labels ---
const oldComputeLabel = mapAddOnVariantIdToComputeSize(
form.formState.defaultValues?.computeSize ?? 'ci_nano'
)
const newComputeLabel = mapAddOnVariantIdToComputeSize(form.getValues('computeSize'))
return {
// prices
computeSizePrice,
diskSizePrice,
iopsPrice,
throughputPrice,
totalBeforePrice,
totalAfterPrice,
// change flags
hasComputeChanges,
hasTotalSizeChanges,
hasStorageTypeChanges,
hasThroughputChanges,
hasIOPSChanges,
hasGrowthPercentChanges,
hasMinIncrementChanges,
hasMaxSizeChanges,
// derived predicates
anyBillableDiskChange,
anyDiskAttributeChange,
showThroughputRow,
hasAnyBreakdownRows,
// labels
oldComputeLabel,
newComputeLabel,
}
}