CreateCredentialModal.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import { zodResolver } from '@hookform/resolvers/zod'
  2. import { PermissionAction } from '@supabase/shared-types/out/constants'
  3. import { useParams } from 'common'
  4. import { Plus } from 'lucide-react'
  5. import { useState } from 'react'
  6. import { useForm } from 'react-hook-form'
  7. import {
  8. Button,
  9. Dialog,
  10. DialogContent,
  11. DialogDescription,
  12. DialogFooter,
  13. DialogHeader,
  14. DialogSection,
  15. DialogSectionSeparator,
  16. DialogTitle,
  17. DialogTrigger,
  18. Form,
  19. FormField,
  20. Tooltip,
  21. TooltipContent,
  22. TooltipTrigger,
  23. } from 'ui'
  24. import { Input } from 'ui-patterns/DataInputs/Input'
  25. import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
  26. import { z } from 'zod'
  27. import { useProjectStorageConfigQuery } from '@/data/config/project-storage-config-query'
  28. import { useS3AccessKeyCreateMutation } from '@/data/storage/s3-access-key-create-mutation'
  29. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  30. import { useIsProjectActive } from '@/hooks/misc/useSelectedProject'
  31. interface CreateCredentialModalProps {
  32. visible: boolean
  33. onOpenChange: (value: boolean) => void
  34. }
  35. export const CreateCredentialModal = ({ visible, onOpenChange }: CreateCredentialModalProps) => {
  36. const { ref: projectRef } = useParams()
  37. const isProjectActive = useIsProjectActive()
  38. const [showSuccess, setShowSuccess] = useState(false)
  39. const { can: canCreateCredentials } = useAsyncCheckPermissions(
  40. PermissionAction.STORAGE_ADMIN_WRITE,
  41. '*'
  42. )
  43. const { data: config } = useProjectStorageConfigQuery({ projectRef })
  44. const isS3ConnectionEnabled = config?.features.s3Protocol.enabled
  45. const disableCreation = !isProjectActive || !canCreateCredentials || !isS3ConnectionEnabled
  46. const FormSchema = z.object({
  47. description: z.string().min(3, {
  48. message: 'Description must be at least 3 characters long',
  49. }),
  50. })
  51. const form = useForm<z.infer<typeof FormSchema>>({
  52. resolver: zodResolver(FormSchema as any),
  53. defaultValues: {
  54. description: '',
  55. },
  56. })
  57. const {
  58. data: createS3KeyData,
  59. mutate: createS3AccessKey,
  60. isPending: isCreating,
  61. } = useS3AccessKeyCreateMutation({
  62. onSuccess: () => {
  63. setShowSuccess(true)
  64. form.reset()
  65. },
  66. })
  67. async function onSubmit(data: z.infer<typeof FormSchema>) {
  68. createS3AccessKey({ projectRef, ...data })
  69. }
  70. return (
  71. <Dialog
  72. open={visible}
  73. onOpenChange={(open) => {
  74. onOpenChange(open)
  75. if (!open) setShowSuccess(false)
  76. }}
  77. >
  78. <Tooltip>
  79. <TooltipTrigger asChild>
  80. <DialogTrigger asChild>
  81. <Button
  82. type="default"
  83. icon={<Plus size={14} />}
  84. disabled={disableCreation}
  85. className="pointer-events-auto"
  86. >
  87. New access key
  88. </Button>
  89. </DialogTrigger>
  90. </TooltipTrigger>
  91. {disableCreation && (
  92. <TooltipContent side="bottom">
  93. {!isProjectActive
  94. ? 'Restore your project to create new access keys'
  95. : !isS3ConnectionEnabled
  96. ? 'Connection via S3 protocol is currently disabled'
  97. : !canCreateCredentials
  98. ? 'You need additional permissions to create new access keys'
  99. : ''}
  100. </TooltipContent>
  101. )}
  102. </Tooltip>
  103. <DialogContent
  104. onInteractOutside={(e) => {
  105. if (showSuccess) e.preventDefault()
  106. }}
  107. >
  108. {showSuccess ? (
  109. <>
  110. <DialogHeader>
  111. <DialogTitle>Save your new S3 access keys</DialogTitle>
  112. <DialogDescription>
  113. You won't be able to see them again. If you lose these access keys, you'll need to
  114. create a new ones.
  115. </DialogDescription>
  116. </DialogHeader>
  117. <DialogSectionSeparator />
  118. <DialogSection className="flex flex-col gap-4">
  119. <FormItemLayout label="Access key ID" isReactForm={false}>
  120. <Input className="input-mono" readOnly copy value={createS3KeyData?.access_key} />
  121. </FormItemLayout>
  122. <FormItemLayout label={'Secret access key'} isReactForm={false}>
  123. <Input className="input-mono" readOnly copy value={createS3KeyData?.secret_key} />
  124. </FormItemLayout>
  125. </DialogSection>
  126. <DialogFooter>
  127. <Button
  128. onClick={() => {
  129. onOpenChange(false)
  130. setShowSuccess(false)
  131. }}
  132. >
  133. Done
  134. </Button>
  135. </DialogFooter>
  136. </>
  137. ) : (
  138. <>
  139. <DialogHeader>
  140. <DialogTitle>Create new S3 access keys</DialogTitle>
  141. <DialogDescription>
  142. S3 access keys provide full access to all S3 operations across all buckets and
  143. bypass any existing RLS policies.
  144. </DialogDescription>
  145. </DialogHeader>
  146. <DialogSectionSeparator />
  147. <Form {...form}>
  148. <form onSubmit={form.handleSubmit(onSubmit)}>
  149. <DialogSection>
  150. <FormField
  151. name="description"
  152. render={({ field }) => (
  153. <FormItemLayout label="Description">
  154. <Input
  155. autoComplete="off"
  156. placeholder="My test key"
  157. type="text"
  158. {...field}
  159. />
  160. </FormItemLayout>
  161. )}
  162. />
  163. </DialogSection>
  164. <DialogFooter>
  165. <Button htmlType="submit" loading={isCreating}>
  166. Create access key
  167. </Button>
  168. </DialogFooter>
  169. </form>
  170. </Form>
  171. </>
  172. )}
  173. </DialogContent>
  174. </Dialog>
  175. )
  176. }