CustomDomainsConfigureHostname.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import { zodResolver } from '@hookform/resolvers/zod'
  2. import { PermissionAction } from '@supabase/shared-types/out/constants'
  3. import { useParams } from 'common'
  4. import { useForm } from 'react-hook-form'
  5. import {
  6. Button,
  7. Card,
  8. CardContent,
  9. CardFooter,
  10. CardHeader,
  11. CardTitle,
  12. Form,
  13. FormControl,
  14. FormField,
  15. Input,
  16. } from 'ui'
  17. import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
  18. import { z } from 'zod'
  19. import CopyButton from '@/components/ui/CopyButton'
  20. import { DocsButton } from '@/components/ui/DocsButton'
  21. import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
  22. import { useCheckCNAMERecordMutation } from '@/data/custom-domains/check-cname-mutation'
  23. import { useCustomDomainCreateMutation } from '@/data/custom-domains/custom-domains-create-mutation'
  24. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  25. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  26. import { DOCS_URL } from '@/lib/constants'
  27. const schema = z.object({
  28. domain: z.string().trim().min(1, 'A value for your custom domain is required'),
  29. })
  30. export const CustomDomainsConfigureHostname = () => {
  31. const { ref } = useParams()
  32. const { data: project } = useSelectedProjectQuery()
  33. const { mutate: checkCNAMERecord, isPending: isCheckingRecord } = useCheckCNAMERecordMutation()
  34. const { mutate: createCustomDomain, isPending: isCreating } = useCustomDomainCreateMutation()
  35. const { data: settings } = useProjectSettingsV2Query({ projectRef: ref })
  36. const endpoint = settings?.app_config?.endpoint
  37. const { can: canConfigureCustomDomain } = useAsyncCheckPermissions(
  38. PermissionAction.UPDATE,
  39. 'projects',
  40. {
  41. resource: {
  42. project_id: project?.id,
  43. },
  44. }
  45. )
  46. const form = useForm<z.infer<typeof schema>>({
  47. resolver: zodResolver(schema as any),
  48. defaultValues: {
  49. domain: '',
  50. },
  51. mode: 'onSubmit',
  52. reValidateMode: 'onBlur',
  53. })
  54. const onCreateCustomDomain = async (values: z.infer<typeof schema>) => {
  55. if (!ref) return console.error('Project ref is required')
  56. checkCNAMERecord(
  57. { domain: values.domain.trim() },
  58. {
  59. onSuccess: () => {
  60. createCustomDomain({ projectRef: ref, customDomain: values.domain.trim() })
  61. },
  62. }
  63. )
  64. }
  65. const domain = form.watch('domain')
  66. const trimmedDomain = domain.trim()
  67. const isSubmitting = isCheckingRecord || isCreating
  68. return (
  69. <Form {...form}>
  70. <form onSubmit={form.handleSubmit(onCreateCustomDomain)}>
  71. <Card>
  72. <CardHeader className="flex flex-row items-center justify-between space-y-0 gap-4">
  73. <CardTitle>Add a custom domain</CardTitle>
  74. <DocsButton href={`${DOCS_URL}/guides/platform/custom-domains`} />
  75. </CardHeader>
  76. <CardContent>
  77. <div className="space-y-4">
  78. <FormField
  79. control={form.control}
  80. name="domain"
  81. render={({ field }) => (
  82. <FormItemLayout
  83. layout="flex-row-reverse"
  84. label="Custom domain"
  85. description="Enter the subdomain you want to use."
  86. className="[&>div]:md:w-1/2"
  87. >
  88. <FormControl>
  89. <Input
  90. {...field}
  91. placeholder="subdomain.example.com"
  92. disabled={!canConfigureCustomDomain || isSubmitting}
  93. autoComplete="off"
  94. />
  95. </FormControl>
  96. </FormItemLayout>
  97. )}
  98. />
  99. </div>
  100. </CardContent>
  101. <CardContent>
  102. <h4 className="text-sm mb-1">Configure a CNAME record</h4>
  103. <p className="text-sm text-foreground-light">
  104. Set up a CNAME record for{' '}
  105. {domain ? <code className="text-code-inline">{domain}</code> : 'your custom domain'}{' '}
  106. resolving to{' '}
  107. {endpoint ? (
  108. <span className="inline-flex items-center gap-x-1">
  109. <code className="text-code-inline">{endpoint}</code>
  110. <CopyButton
  111. iconOnly
  112. type="text"
  113. className="h-5 w-5 min-w-0 p-0 [&_svg]:h-3 [&_svg]:w-3"
  114. text={endpoint}
  115. />
  116. </span>
  117. ) : (
  118. "your project's API URL"
  119. )}{' '}
  120. with as low a TTL as possible. If you're using Cloudflare as your DNS provider,
  121. disable the proxy option.
  122. <br />
  123. {trimmedDomain.includes('.') ? (
  124. <>
  125. Some DNS providers expect only the subdomain label{' '}
  126. <code className="text-code-inline">{trimmedDomain.split('.')[0]}</code>, while
  127. others accept the full hostname{' '}
  128. <code className="text-code-inline whitespace-nowrap">{trimmedDomain}</code>.
  129. </>
  130. ) : (
  131. 'Some DNS providers expect only the subdomain label, while others accept the full hostname.'
  132. )}
  133. </p>
  134. </CardContent>
  135. <CardFooter className="justify-end space-x-2">
  136. {form.formState.isDirty && (
  137. <Button
  138. type="default"
  139. disabled={isSubmitting}
  140. onClick={() => form.reset({ domain: '' })}
  141. >
  142. Cancel
  143. </Button>
  144. )}
  145. <Button
  146. type="primary"
  147. htmlType="submit"
  148. loading={isSubmitting}
  149. disabled={!form.formState.isDirty || isSubmitting || !canConfigureCustomDomain}
  150. >
  151. Add
  152. </Button>
  153. </CardFooter>
  154. </Card>
  155. {!canConfigureCustomDomain && (
  156. <p className="text-xs text-foreground-light">
  157. You need additional permissions to update your project's custom domain settings.
  158. </p>
  159. )}
  160. </form>
  161. </Form>
  162. )
  163. }