PerformanceSettingsForm.tsx 16 KB

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