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