DiskSizeField.tsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import { useParams } from 'common'
  2. import dayjs from 'dayjs'
  3. import { RotateCcw } from 'lucide-react'
  4. import { UseFormReturn } from 'react-hook-form'
  5. import {
  6. Button,
  7. FormControl,
  8. FormField,
  9. FormInputGroupInput,
  10. InputGroup,
  11. InputGroupAddon,
  12. InputGroupButton,
  13. InputGroupText,
  14. } from 'ui'
  15. import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
  16. import { DiskStorageSchemaType } from '../DiskManagement.schema'
  17. import { calculateDiskSizePrice } from '../DiskManagement.utils'
  18. import { BillingChangeBadge } from '../ui/BillingChangeBadge'
  19. import { DiskType, PLAN_DETAILS } from '../ui/DiskManagement.constants'
  20. import { DiskManagementDiskSizeReadReplicas } from '../ui/DiskManagementReadReplicas'
  21. import { DiskSpaceBar } from '../ui/DiskSpaceBar'
  22. import { DiskTypeRecommendationSection } from '../ui/DiskTypeRecommendationSection'
  23. import FormMessage from '../ui/FormMessage'
  24. import { DocsButton } from '@/components/ui/DocsButton'
  25. import { useDiskAttributesQuery } from '@/data/config/disk-attributes-query'
  26. import { useDiskUtilizationQuery } from '@/data/config/disk-utilization-query'
  27. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  28. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  29. import { DOCS_URL, GB } from '@/lib/constants'
  30. type DiskSizeFieldProps = {
  31. form: UseFormReturn<DiskStorageSchemaType>
  32. disableInput: boolean
  33. setAdvancedSettingsOpenState: (state: boolean) => void
  34. }
  35. export function DiskSizeField({
  36. form,
  37. disableInput,
  38. setAdvancedSettingsOpenState,
  39. }: DiskSizeFieldProps) {
  40. const { ref: projectRef } = useParams()
  41. const { control, formState, setValue, trigger, getValues, resetField, watch } = form
  42. const { data: org } = useSelectedOrganizationQuery()
  43. const { data: project } = useSelectedProjectQuery()
  44. const { error: diskAttributesError, isError: isDiskAttributesError } = useDiskAttributesQuery(
  45. { projectRef },
  46. { enabled: project && project.cloud_provider !== 'FLY' }
  47. )
  48. const {
  49. data: diskUtil,
  50. error: diskUtilError,
  51. isError: isDiskUtilizationError,
  52. } = useDiskUtilizationQuery(
  53. {
  54. projectRef: projectRef,
  55. },
  56. { enabled: project && project.cloud_provider !== 'FLY' }
  57. )
  58. const error = diskUtilError || diskAttributesError
  59. const isError = isDiskUtilizationError || isDiskAttributesError
  60. // coming up typically takes 5 minutes, and the request is cached for 5 mins
  61. // so doing less than 10 mins to account for both
  62. const isProjectNew =
  63. dayjs.utc().diff(dayjs.utc(project?.inserted_at), 'minute') < 10 ||
  64. project?.status === 'COMING_UP'
  65. const watchedStorageType = watch('storageType')
  66. const watchedTotalSize = watch('totalSize')
  67. const planId = org?.plan.id ?? 'free'
  68. const { includedDiskGB: includedDiskGBMeta } =
  69. PLAN_DETAILS?.[planId as keyof typeof PLAN_DETAILS] ?? {}
  70. const includedDiskGB = includedDiskGBMeta[watchedStorageType]
  71. const { defaultValues, dirtyFields, isDirty, errors } = formState
  72. const diskSizePrice = calculateDiskSizePrice({
  73. planId,
  74. oldSize: defaultValues?.totalSize || 0,
  75. oldStorageType: defaultValues?.storageType as DiskType,
  76. newSize: getValues('totalSize'),
  77. newStorageType: getValues('storageType') as DiskType,
  78. })
  79. const mainDiskUsed = Math.round(((diskUtil?.metrics.fs_used_bytes ?? 0) / GB) * 100) / 100
  80. return (
  81. <div id="disk-size" className="grid @xl:grid-cols-12 gap-5">
  82. <div className="col-span-4">
  83. <FormField
  84. name="totalSize"
  85. control={control}
  86. render={({ field, fieldState: { isDirty } }) => (
  87. <FormItemLayout label="Disk Size" layout="vertical" id={field.name}>
  88. <FormControl className="max-w-32">
  89. <InputGroup>
  90. <FormInputGroupInput
  91. type="number"
  92. id={field.name}
  93. {...field}
  94. disabled={disableInput || isError}
  95. onWheel={(e) => e.currentTarget.blur()}
  96. onChange={(e) => {
  97. setValue('totalSize', e.target.valueAsNumber, {
  98. shouldDirty: true,
  99. shouldValidate: true,
  100. })
  101. trigger('provisionedIOPS')
  102. trigger('throughput')
  103. }}
  104. min={includedDiskGB}
  105. />
  106. <InputGroupAddon align="inline-end">
  107. <InputGroupText>GB</InputGroupText>
  108. {isDirty ? (
  109. <InputGroupButton
  110. htmlType="button"
  111. type="default"
  112. size="tiny"
  113. className="px-2 text-foreground-light"
  114. onClick={() => {
  115. resetField('totalSize')
  116. trigger('provisionedIOPS')
  117. }}
  118. title="Reset"
  119. >
  120. <RotateCcw className="h-4 w-4" aria-hidden="true" />
  121. </InputGroupButton>
  122. ) : null}
  123. </InputGroupAddon>
  124. </InputGroup>
  125. </FormControl>
  126. </FormItemLayout>
  127. )}
  128. />
  129. <div className="flex flex-col gap-1">
  130. <BillingChangeBadge
  131. className="mt-1"
  132. beforePrice={Number(diskSizePrice.oldPrice)}
  133. afterPrice={Number(diskSizePrice.newPrice)}
  134. show={isDirty && !errors.totalSize && diskSizePrice.oldPrice !== diskSizePrice.newPrice}
  135. />
  136. <span className="text-foreground-lighter text-sm">
  137. {includedDiskGB > 0 &&
  138. org?.plan.id &&
  139. `Your plan includes up to ${includedDiskGB} GB of ${watchedStorageType} storage.`}
  140. <div className="mt-3">
  141. <DocsButton abbrev={false} href={`${DOCS_URL}/guides/platform/database-size`} />
  142. </div>
  143. </span>
  144. <DiskTypeRecommendationSection
  145. form={form}
  146. actions={
  147. <Button
  148. type="default"
  149. onClick={() => {
  150. setValue('storageType', 'io2')
  151. trigger('provisionedIOPS')
  152. trigger('totalSize')
  153. setAdvancedSettingsOpenState(true)
  154. }}
  155. >
  156. Change to High Performance SSD
  157. </Button>
  158. }
  159. />
  160. </div>
  161. </div>
  162. <div className="col-span-8">
  163. <DiskSpaceBar form={form} />
  164. {isProjectNew ? (
  165. <FormMessage
  166. message="Disk size data is not available for ~10 minutes after project creation"
  167. type="error"
  168. />
  169. ) : (
  170. error && (
  171. <FormMessage message="Failed to load disk size data" type="error">
  172. {error?.message}
  173. </FormMessage>
  174. )
  175. )}
  176. <DiskManagementDiskSizeReadReplicas
  177. isDirty={dirtyFields.totalSize !== undefined}
  178. totalSize={(defaultValues?.totalSize || 0) * 1.25}
  179. usedSize={mainDiskUsed}
  180. newTotalSize={watchedTotalSize * 1.25}
  181. oldStorageType={defaultValues?.storageType as DiskType}
  182. newStorageType={getValues('storageType') as DiskType}
  183. />
  184. </div>
  185. </div>
  186. )
  187. }