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