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