CreateRolePanel.tsx189 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { SubmitHandler, useForm } from 'react-hook-form'
3import { toast } from 'sonner'
4import {
5 Form,
6 FormControl,
7 FormField,
8 FormItem,
9 FormLabel,
10 FormMessage,
11 Input,
12 SidePanel,
13 Switch,
14} from 'ui'
15import z from 'zod'
16
17import { ROLE_PERMISSIONS } from './Roles.constants'
18import { FormActions } from '@/components/ui/Forms/FormActions'
19import { useDatabaseRoleCreateMutation } from '@/data/database-roles/database-role-create-mutation'
20import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
21
22interface CreateRolePanelProps {
23 visible: boolean
24 onClose: () => void
25}
26
27const FormSchema = z.object({
28 name: z.string().trim().min(1, 'You must provide a name').default(''),
29 isSuperuser: z.boolean().default(false),
30 canLogin: z.boolean().default(false),
31 canCreateRole: z.boolean().default(false),
32 canCreateDb: z.boolean().default(false),
33 isReplicationRole: z.boolean().default(false),
34 canBypassRls: z.boolean().default(false),
35})
36
37const initialValues = {
38 name: '',
39 isSuperuser: false,
40 canLogin: false,
41 canCreateRole: false,
42 canCreateDb: false,
43 isReplicationRole: false,
44 canBypassRls: false,
45}
46
47export const CreateRolePanel = ({ visible, onClose }: CreateRolePanelProps) => {
48 const formId = 'create-new-role'
49
50 const { data: project } = useSelectedProjectQuery()
51
52 const form = useForm<z.infer<typeof FormSchema>>({
53 resolver: zodResolver(FormSchema as any),
54 })
55
56 const { mutate: createDatabaseRole, isPending: isCreating } = useDatabaseRoleCreateMutation({
57 onSuccess: (_, vars) => {
58 toast.success(`Successfully created new role: ${vars.payload.name}`)
59 handleClose()
60 },
61 })
62
63 const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (values) => {
64 if (!project) return console.error('Project is required')
65 createDatabaseRole({
66 projectRef: project.ref,
67 connectionString: project.connectionString,
68 payload: values,
69 })
70 }
71
72 const handleClose = () => {
73 onClose()
74 form.reset(initialValues)
75 }
76
77 return (
78 <SidePanel
79 size="large"
80 visible={visible}
81 header="Create a new role"
82 className="mr-0 transform transition-all duration-300 ease-in-out"
83 loading={false}
84 onCancel={handleClose}
85 customFooter={
86 <div className="flex w-full justify-end space-x-3 border-t border-default px-3 py-4">
87 <FormActions
88 form={formId}
89 isSubmitting={isCreating}
90 hasChanges={form.formState.isDirty}
91 handleReset={handleClose}
92 />
93 </div>
94 }
95 >
96 <Form {...form}>
97 <form
98 id={formId}
99 className="grid gap-6 w-full px-8 py-8"
100 onSubmit={form.handleSubmit(onSubmit)}
101 >
102 <FormField
103 control={form.control}
104 name="name"
105 render={({ field }) => (
106 <FormItem className="grid gap-2 md:grid md:grid-cols-12 space-y-0">
107 <FormLabel className="flex flex-col space-y-2 col-span-4 text-sm justify-center text-foreground-light">
108 Name
109 </FormLabel>
110 <FormControl className="col-span-8">
111 <Input {...field} className="w-full" />
112 </FormControl>
113 <FormMessage className="col-start-5 col-span-8" />
114 </FormItem>
115 )}
116 />
117 <div className="grid gap-2 mt-4 md:grid md:grid-cols-12">
118 <div className="col-span-4">
119 <FormLabel className="flex flex-col space-y-2 col-span-4 text-sm justify-center text-foreground-light">
120 Role privileges
121 </FormLabel>
122 </div>
123 <div className="col-span-8 grid gap-4">
124 {(Object.keys(ROLE_PERMISSIONS) as (keyof typeof ROLE_PERMISSIONS)[])
125 .filter((permissionKey) => ROLE_PERMISSIONS[permissionKey].grant_by_dashboard)
126 .map((permissionKey) => {
127 const permission = ROLE_PERMISSIONS[permissionKey]
128
129 return (
130 <FormField
131 key={permissionKey}
132 control={form.control}
133 name={permissionKey}
134 render={({ field }) => (
135 <FormItem className="grid gap-2 md:grid md:grid-cols-12 space-y-0">
136 <FormControl className="col-span-8 flex items-center gap-4">
137 <div className="w-full text-sm">
138 <Switch checked={field.value} onCheckedChange={field.onChange} />
139 <FormLabel>{permission.description}</FormLabel>
140 </div>
141 </FormControl>
142 <FormMessage className="col-start-5 col-span-8" />
143 </FormItem>
144 )}
145 />
146 )
147 })}
148
149 <SidePanel.Separator />
150
151 <div className="grid gap-4">
152 <p className="text-sm">These privileges cannot be granted via the Dashboard:</p>
153 {(Object.keys(ROLE_PERMISSIONS) as (keyof typeof ROLE_PERMISSIONS)[])
154 .filter((permissionKey) => !ROLE_PERMISSIONS[permissionKey].grant_by_dashboard)
155 .map((permissionKey) => {
156 const permission = ROLE_PERMISSIONS[permissionKey]
157
158 return (
159 <FormField
160 key={permissionKey}
161 control={form.control}
162 name={permissionKey}
163 render={({ field }) => (
164 <FormItem className="space-y-0 opacity-70">
165 <FormControl className="flex items-center gap-4">
166 <div className="w-full text-sm">
167 <Switch
168 checked={field.value}
169 onCheckedChange={field.onChange}
170 disabled
171 aria-readonly
172 />
173 <FormLabel>{permission.description}</FormLabel>
174 </div>
175 </FormControl>
176 <FormMessage className="col-start-5 col-span-8" />
177 </FormItem>
178 )}
179 />
180 )
181 })}
182 </div>
183 </div>
184 </div>
185 </form>
186 </Form>
187 </SidePanel>
188 )
189}