General.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import { zodResolver } from '@hookform/resolvers/zod'
  2. import { PermissionAction } from '@supabase/shared-types/out/constants'
  3. import { useForm } from 'react-hook-form'
  4. import { toast } from 'sonner'
  5. import { Button, Card, CardContent, CardFooter, Form, FormControl, FormField, Input } from 'ui'
  6. import { Admonition } from 'ui-patterns'
  7. import { Input as PasswordInput } from 'ui-patterns/DataInputs/Input'
  8. import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
  9. import {
  10. PageSection,
  11. PageSectionContent,
  12. PageSectionMeta,
  13. PageSectionSummary,
  14. PageSectionTitle,
  15. } from 'ui-patterns/PageSection'
  16. import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
  17. import * as z from 'zod'
  18. import { AVAILABLE_REPLICA_REGIONS } from '../Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants'
  19. import { ProjectAccessSection } from './ProjectAccessSection'
  20. import { InlineLink } from '@/components/ui/InlineLink'
  21. import { useProjectUpdateMutation } from '@/data/projects/project-update-mutation'
  22. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  23. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  24. export const General = () => {
  25. const { data: project } = useSelectedProjectQuery()
  26. const isBranch = Boolean(project?.parent_project_ref)
  27. const { can: canUpdateProject } = useAsyncCheckPermissions(PermissionAction.UPDATE, 'projects', {
  28. resource: {
  29. project_id: project?.id,
  30. },
  31. })
  32. const { mutate: updateProject, isPending: isUpdating } = useProjectUpdateMutation()
  33. const formSchema = z.object({
  34. name: z.string().trim().min(3, 'Project name must be at least 3 characters long'),
  35. })
  36. const defaultValues = { name: project?.name ?? '' }
  37. const form = useForm<z.infer<typeof formSchema>>({
  38. resolver: zodResolver(formSchema as any),
  39. defaultValues,
  40. values: defaultValues,
  41. mode: 'onSubmit',
  42. reValidateMode: 'onBlur',
  43. })
  44. const regionLabel = AVAILABLE_REPLICA_REGIONS.find((region) =>
  45. project?.region?.includes(region.region)
  46. )
  47. const onSubmit = async (values: z.infer<typeof formSchema>) => {
  48. if (!project?.ref) return console.error('Ref is required')
  49. updateProject(
  50. { ref: project.ref, name: values.name.trim() },
  51. {
  52. onSuccess: ({ name }) => {
  53. form.reset({ name })
  54. toast.success('Successfully saved settings')
  55. },
  56. }
  57. )
  58. }
  59. return (
  60. <>
  61. <PageSection>
  62. <PageSectionMeta>
  63. <PageSectionSummary>
  64. <PageSectionTitle>General settings</PageSectionTitle>
  65. </PageSectionSummary>
  66. </PageSectionMeta>
  67. <PageSectionContent>
  68. {isBranch && (
  69. <Admonition
  70. type="default"
  71. className="mb-4"
  72. title="You are currently on a preview branch of your project"
  73. >
  74. Certain settings are not available while you're on a preview branch. To adjust your
  75. project settings, you may return to your{' '}
  76. <InlineLink href={`/project/${project?.parent_project_ref}/settings/general`}>
  77. main branch
  78. </InlineLink>
  79. .
  80. </Admonition>
  81. )}
  82. {project === undefined ? (
  83. <Card>
  84. <CardContent>
  85. <GenericSkeletonLoader />
  86. </CardContent>
  87. </Card>
  88. ) : (
  89. <Form {...form}>
  90. <form onSubmit={form.handleSubmit(onSubmit)}>
  91. <Card>
  92. <CardContent>
  93. <FormField
  94. control={form.control}
  95. name="name"
  96. render={({ field }) => (
  97. <FormItemLayout
  98. layout="flex-row-reverse"
  99. label="Project name"
  100. description="Displayed throughout the dashboard."
  101. className="[&>div]:md:w-1/2"
  102. >
  103. <FormControl>
  104. <Input
  105. {...field}
  106. disabled={isBranch || !canUpdateProject}
  107. autoComplete="off"
  108. />
  109. </FormControl>
  110. </FormItemLayout>
  111. )}
  112. />
  113. </CardContent>
  114. <CardContent>
  115. <FormItemLayout
  116. layout="flex-row-reverse"
  117. label="Project ID"
  118. description="Reference used in APIs and URLs."
  119. className="[&>div]:md:w-1/2 [&>div>div]:md:w-full"
  120. >
  121. <FormControl>
  122. <PasswordInput copy readOnly size="small" value={project.ref} />
  123. </FormControl>
  124. </FormItemLayout>
  125. </CardContent>
  126. <CardContent>
  127. <FormItemLayout
  128. layout="flex-row-reverse"
  129. label="Project region"
  130. description={regionLabel?.name}
  131. className="[&>div]:md:w-1/2 [&>div>div]:md:w-full"
  132. >
  133. <FormControl>
  134. <PasswordInput copy readOnly size="small" value={project.region} />
  135. </FormControl>
  136. </FormItemLayout>
  137. </CardContent>
  138. <CardFooter className="justify-end space-x-2">
  139. {form.formState.isDirty && (
  140. <Button
  141. type="default"
  142. htmlType="button"
  143. disabled={isUpdating}
  144. onClick={() => form.reset({ name: project?.name ?? '' })}
  145. >
  146. Cancel
  147. </Button>
  148. )}
  149. <Button
  150. type="primary"
  151. htmlType="submit"
  152. disabled={
  153. !form.formState.isDirty || isUpdating || !canUpdateProject || isBranch
  154. }
  155. loading={isUpdating}
  156. >
  157. Save changes
  158. </Button>
  159. </CardFooter>
  160. </Card>
  161. </form>
  162. </Form>
  163. )}
  164. </PageSectionContent>
  165. </PageSection>
  166. <ProjectAccessSection />
  167. </>
  168. )
  169. }