CreateRolePanel.tsx 6.6 KB

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