PerformanceSettingsForm.tsx441 lines · main
1// @ts-nocheck
2import { zodResolver } from '@hookform/resolvers/zod'
3import { PermissionAction } from '@supabase/shared-types/out/constants'
4import { useEffect, useState } from 'react'
5import { useForm } from 'react-hook-form'
6import { toast } from 'sonner'
7import {
8 Button,
9 Card,
10 CardContent,
11 CardFooter,
12 Form,
13 FormControl,
14 FormField,
15 FormInputGroupInput,
16 InputGroup,
17 InputGroupAddon,
18 InputGroupText,
19 Select,
20 SelectContent,
21 SelectItem,
22 SelectTrigger,
23 SelectValue,
24} from 'ui'
25import { GenericSkeletonLoader, ShimmeringLoader } from 'ui-patterns'
26import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
27import * as z from 'zod'
28
29import { ScaffoldSection, ScaffoldSectionTitle } from '@/components/layouts/Scaffold'
30import AlertError from '@/components/ui/AlertError'
31import NoPermission from '@/components/ui/NoPermission'
32import { UpgradeToPro } from '@/components/ui/UpgradeToPro'
33import { useAuthConfigQuery } from '@/data/auth/auth-config-query'
34import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-mutation'
35import { useMaxConnectionsQuery } from '@/data/database/max-connections-query'
36import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
37import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
38import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
39import { IS_PLATFORM } from '@/lib/constants'
40
41const FormSchema = z.object({
42 API_MAX_REQUEST_DURATION: z.coerce
43 .number()
44 .min(5, 'Must be 5 or larger')
45 .max(30, 'Must be a value no greater than 30'),
46 DB_MAX_POOL_SIZE: z.coerce.number().min(1),
47 DB_MAX_POOL_SIZE_UNIT: z.enum(['percent', 'connections']),
48})
49
50export const DatabaseFormSchema = z
51 .object({
52 DB_MAX_POOL_SIZE: z.coerce.number().min(1),
53 DB_MAX_POOL_SIZE_UNIT: z.enum(['percent', 'connections']),
54 })
55 .superRefine((data, ctx) => {
56 if (data.DB_MAX_POOL_SIZE_UNIT === 'percent') {
57 if (data.DB_MAX_POOL_SIZE < 1 || data.DB_MAX_POOL_SIZE > 100) {
58 ctx.addIssue({
59 code: z.ZodIssueCode.custom,
60 path: ['DB_MAX_POOL_SIZE'],
61 message: 'Percentage must be between 1 and 100',
62 })
63 }
64 }
65 })
66
67export const PerformanceSettingsForm = () => {
68 const { data: project } = useSelectedProjectQuery()
69 const { hasAccess: hasAccessToPerformance, isLoading: isLoadingEntitlement } =
70 useCheckEntitlements('auth.performance_settings')
71 const { can: canReadConfig } = useAsyncCheckPermissions(
72 PermissionAction.READ,
73 'custom_config_gotrue'
74 )
75 const { can: canUpdateConfig } = useAsyncCheckPermissions(
76 PermissionAction.UPDATE,
77 'custom_config_gotrue'
78 )
79
80 const [isUpdatingRequestDurationForm, setIsUpdatingRequestDurationForm] = useState(false)
81 const [isUpdatingDatabaseForm, setIsUpdatingDatabaseForm] = useState(false)
82
83 const {
84 data: authConfig,
85 error: authConfigError,
86 isError,
87 isLoading: isLoadingAuthConfig,
88 } = useAuthConfigQuery({ projectRef: project?.ref })
89
90 const { data: maxConnData, isLoading: isLoadingMaxConns } = useMaxConnectionsQuery({
91 projectRef: project?.ref,
92 connectionString: project?.connectionString,
93 })
94 const maxConnectionLimit = maxConnData?.maxConnections ?? 60
95
96 const promptUpgrade = IS_PLATFORM && !isLoadingEntitlement && !hasAccessToPerformance
97
98 const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation()
99
100 const requestDurationForm = useForm({
101 resolver: zodResolver(
102 z.object({ API_MAX_REQUEST_DURATION: FormSchema.shape.API_MAX_REQUEST_DURATION } as any)
103 ),
104 defaultValues: { API_MAX_REQUEST_DURATION: 10 },
105 })
106
107 const databaseForm = useForm({
108 resolver: zodResolver(DatabaseFormSchema as any),
109 defaultValues: {
110 DB_MAX_POOL_SIZE: 10,
111 DB_MAX_POOL_SIZE_UNIT: 'connections',
112 },
113 })
114
115 const chosenUnit = databaseForm.watch('DB_MAX_POOL_SIZE_UNIT')
116
117 const onSubmitRequestDurationForm = (values: any) => {
118 if (!project?.ref) return console.error('Project ref is required')
119 if (!hasAccessToPerformance) return
120
121 setIsUpdatingRequestDurationForm(true)
122
123 updateAuthConfig(
124 { projectRef: project?.ref, config: values },
125 {
126 onError: (error) => {
127 toast.error(`Failed to update request duration settings: ${error?.message}`)
128 setIsUpdatingRequestDurationForm(false)
129 },
130 onSuccess: () => {
131 toast.success('Successfully updated request duration settings')
132 setIsUpdatingRequestDurationForm(false)
133 },
134 }
135 )
136 }
137
138 const onSubmitDatabaseForm = (values: any) => {
139 if (!project?.ref) return console.error('Project ref is required')
140
141 setIsUpdatingDatabaseForm(true)
142
143 const config = {
144 DB_MAX_POOL_SIZE: values.DB_MAX_POOL_SIZE,
145 DB_MAX_POOL_SIZE_UNIT: values.DB_MAX_POOL_SIZE_UNIT,
146 }
147
148 updateAuthConfig(
149 { projectRef: project?.ref, config },
150 {
151 onError: () => {
152 setIsUpdatingDatabaseForm(false)
153 },
154 onSuccess: () => {
155 toast.success('Successfully updated connection settings')
156 setIsUpdatingDatabaseForm(false)
157 },
158 }
159 )
160 }
161
162 useEffect(() => {
163 if (authConfig) {
164 if (!isUpdatingRequestDurationForm) {
165 requestDurationForm.reset({
166 API_MAX_REQUEST_DURATION: authConfig?.API_MAX_REQUEST_DURATION ?? 10,
167 })
168 }
169
170 if (!isUpdatingDatabaseForm) {
171 databaseForm.reset({
172 DB_MAX_POOL_SIZE:
173 authConfig?.DB_MAX_POOL_SIZE !== null ? (authConfig?.DB_MAX_POOL_SIZE ?? 10) : 10,
174 DB_MAX_POOL_SIZE_UNIT:
175 authConfig?.DB_MAX_POOL_SIZE_UNIT !== null
176 ? authConfig?.DB_MAX_POOL_SIZE_UNIT
177 : 'connections',
178 })
179 }
180 }
181 }, [authConfig, isUpdatingRequestDurationForm, isUpdatingDatabaseForm])
182
183 if (isError) {
184 return (
185 <ScaffoldSection isFullWidth>
186 <AlertError error={authConfigError} subject="Failed to retrieve auth configuration" />
187 </ScaffoldSection>
188 )
189 }
190
191 if (!canReadConfig) {
192 return (
193 <ScaffoldSection isFullWidth>
194 <NoPermission resourceText="view auth configuration settings" />
195 </ScaffoldSection>
196 )
197 }
198
199 if (isLoadingAuthConfig || isLoadingEntitlement) {
200 return (
201 <ScaffoldSection isFullWidth>
202 <GenericSkeletonLoader />
203 </ScaffoldSection>
204 )
205 }
206
207 return (
208 <>
209 <ScaffoldSection isFullWidth>
210 {promptUpgrade && (
211 <UpgradeToPro
212 source="authPerformance"
213 featureProposition="configure advanced Auth server settings"
214 primaryText="Only available on the Pro Plan and above"
215 secondaryText="Upgrade to the Pro Plan to configure Auth server performance settings."
216 />
217 )}
218 </ScaffoldSection>
219
220 <ScaffoldSection isFullWidth>
221 <ScaffoldSectionTitle className="mb-4">Request duration</ScaffoldSectionTitle>
222
223 <Form {...requestDurationForm}>
224 <form onSubmit={requestDurationForm.handleSubmit(onSubmitRequestDurationForm)}>
225 <Card>
226 <CardContent className="pt-6">
227 <FormField
228 control={requestDurationForm.control}
229 name="API_MAX_REQUEST_DURATION"
230 render={({ field }) => (
231 <FormItemLayout
232 layout="flex-row-reverse"
233 label="Maximum allowed duration for an Auth request"
234 description={
235 <p className="text-balance">
236 Requests that exceed this time limit are terminated to help manage server
237 load.
238 </p>
239 }
240 >
241 <div className="flex flex-col gap-2">
242 <div className="relative">
243 <FormControl>
244 <InputGroup>
245 <FormInputGroupInput
246 type="number"
247 min={5}
248 max={30}
249 {...field}
250 disabled={!canUpdateConfig || promptUpgrade}
251 />
252 <InputGroupAddon align="inline-end">
253 <InputGroupText>seconds</InputGroupText>
254 </InputGroupAddon>
255 </InputGroup>
256 </FormControl>
257 </div>
258
259 <p className="text-xs text-right text-foreground-muted">
260 10+ seconds recommended
261 </p>
262 </div>
263 </FormItemLayout>
264 )}
265 />
266 </CardContent>
267
268 <CardFooter className="justify-end space-x-2">
269 {requestDurationForm.formState.isDirty && (
270 <Button type="default" onClick={() => requestDurationForm.reset()}>
271 Cancel
272 </Button>
273 )}
274 <Button
275 type={promptUpgrade ? 'default' : 'primary'}
276 htmlType="submit"
277 disabled={
278 !canUpdateConfig ||
279 isUpdatingRequestDurationForm ||
280 !requestDurationForm.formState.isDirty ||
281 promptUpgrade
282 }
283 loading={isUpdatingRequestDurationForm}
284 >
285 Save changes
286 </Button>
287 </CardFooter>
288 </Card>
289 </form>
290 </Form>
291 </ScaffoldSection>
292
293 <ScaffoldSection isFullWidth>
294 <ScaffoldSectionTitle className="mb-4">Connection management</ScaffoldSectionTitle>
295
296 <Form {...databaseForm}>
297 <form onSubmit={databaseForm.handleSubmit(onSubmitDatabaseForm)} className="space-y-4">
298 <Card>
299 <CardContent className="pt-6 flex flex-col gap-4">
300 <FormField
301 control={databaseForm.control}
302 name="DB_MAX_POOL_SIZE_UNIT"
303 render={({ field }) => (
304 <FormItemLayout
305 layout="flex-row-reverse"
306 label="Allocation strategy"
307 description={
308 <p className="text-balance">
309 Choose whether to allocate a percentage or a fixed number of connections
310 to the Auth server. We recommend a percentage, as it scales automatically
311 with your instance size.
312 </p>
313 }
314 >
315 <FormControl>
316 <Select
317 value={field.value}
318 onValueChange={(value) => {
319 const values = databaseForm.getValues()
320 field.onChange(value)
321
322 if (values.DB_MAX_POOL_SIZE_UNIT !== value) {
323 const currentValue = values.DB_MAX_POOL_SIZE!
324
325 let preservedPoolSize: number
326 if (value === 'percent') {
327 // convert from absolute number to roughly the same percentage
328 preservedPoolSize = Math.ceil(
329 (Math.min(maxConnectionLimit, currentValue) /
330 maxConnectionLimit) *
331 100
332 )
333 } else {
334 // convert from percentage to roughly the same connection number
335 preservedPoolSize = Math.floor(
336 maxConnectionLimit * (Math.min(100, currentValue) / 100)
337 )
338 }
339
340 databaseForm.setValue('DB_MAX_POOL_SIZE', preservedPoolSize)
341 }
342 }}
343 >
344 <SelectTrigger size="small" disabled={!canUpdateConfig || promptUpgrade}>
345 <SelectValue>
346 {field.value === 'percent' ? 'Percentage' : 'Absolute'}
347 </SelectValue>
348 </SelectTrigger>
349 <SelectContent align="end">
350 <SelectItem value="connections" className="text-xs">
351 Absolute number of connections
352 </SelectItem>
353 <SelectItem value="percent" className="text-xs">
354 Percent of max connections
355 </SelectItem>
356 </SelectContent>
357 </Select>
358 </FormControl>
359 </FormItemLayout>
360 )}
361 />
362 </CardContent>
363 <CardContent>
364 <FormField
365 control={databaseForm.control}
366 name="DB_MAX_POOL_SIZE"
367 render={({ field }) => (
368 <FormItemLayout
369 layout="flex-row-reverse"
370 label="Maximum connections"
371 description={
372 <p className="text-balance">
373 The maximum number of connections the Auth server can use under peak load.{' '}
374 <em className="text-foreground-light font-medium not-italic">
375 Connections are not reserved
376 </em>{' '}
377 and are returned to Postgres after a short idle period.
378 </p>
379 }
380 >
381 <div className="flex flex-col gap-2">
382 <div className="relative">
383 <FormControl>
384 <InputGroup>
385 <FormInputGroupInput
386 type="number"
387 {...field}
388 disabled={!canUpdateConfig || promptUpgrade}
389 />
390 <InputGroupAddon align="inline-end">
391 <InputGroupText>
392 {chosenUnit === 'percent' ? '%' : 'connections'}
393 </InputGroupText>
394 </InputGroupAddon>
395 </InputGroup>
396 </FormControl>
397 </div>
398 {isLoadingMaxConns ? (
399 <ShimmeringLoader className="py-2 w-16 ml-auto" />
400 ) : (
401 <p className="text-xs text-right text-foreground-muted">
402 <span className="text-foreground-light">
403 {chosenUnit === 'percent'
404 ? Math.floor(
405 maxConnectionLimit * (Math.min(100, field.value!) / 100)
406 ).toString()
407 : Math.min(maxConnectionLimit, field.value!)}
408 </span>{' '}
409 / {maxConnectionLimit}
410 </p>
411 )}
412 </div>
413 </FormItemLayout>
414 )}
415 />
416 </CardContent>
417
418 <CardFooter className="justify-end space-x-2">
419 {databaseForm.formState.isDirty && (
420 <Button type="default" onClick={() => databaseForm.reset()}>
421 Cancel
422 </Button>
423 )}
424 <Button
425 type={promptUpgrade ? 'default' : 'primary'}
426 htmlType="submit"
427 disabled={
428 !canUpdateConfig || isUpdatingDatabaseForm || !databaseForm.formState.isDirty
429 }
430 loading={isUpdatingDatabaseForm}
431 >
432 Save changes
433 </Button>
434 </CardFooter>
435 </Card>
436 </form>
437 </Form>
438 </ScaffoldSection>
439 </>
440 )
441}