VercelIntegrationConnectionForm.tsx275 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import Link from 'next/link'
3import { useForm } from 'react-hook-form'
4import { toast } from 'sonner'
5import {
6 Alert,
7 AlertDescription,
8 AlertTitle,
9 Form,
10 FormControl,
11 FormDescription,
12 FormField,
13 FormItem,
14 FormLabel,
15 FormMessage,
16 Input,
17 Switch,
18} from 'ui'
19import * as z from 'zod'
20
21import { FormActions } from '@/components/ui/Forms/FormActions'
22import type {
23 EnvironmentTargets,
24 Integration,
25 IntegrationProjectConnection,
26} from '@/data/integrations/integrations.types'
27import { useVercelConnectionUpdateMutation } from '@/data/integrations/vercel-connection-update-mutate'
28import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
29import { DOCS_URL } from '@/lib/constants'
30
31const VercelIntegrationConnectionForm = ({
32 disabled,
33 connection,
34 integration,
35}: {
36 disabled?: boolean
37 connection: IntegrationProjectConnection
38 integration: Integration
39}) => {
40 // NOTE(kamil): Ignore sync targets for Vercel Marketplace as it's not synchronized using integration,
41 // but through a separate marketplace mechanism. It's not theoretically necessary, but we might have some stale data.
42 const { data: org } = useSelectedOrganizationQuery()
43 const envSyncTargets =
44 org?.managed_by === 'vercel-marketplace' ? [] : (connection.env_sync_targets ?? [])
45
46 const FormSchema = z.object({
47 environmentVariablesProduction: z.boolean().default(envSyncTargets.includes('production')),
48 environmentVariablesPreview: z.boolean().default(envSyncTargets.includes('preview')),
49 environmentVariablesDevelopment: z.boolean().default(envSyncTargets.includes('development')),
50 publicEnvVarPrefix: z.string().optional(),
51 })
52
53 const form = useForm<z.infer<typeof FormSchema>>({
54 resolver: zodResolver(FormSchema as any),
55 defaultValues: {
56 environmentVariablesProduction: envSyncTargets.includes('production'),
57 environmentVariablesPreview: envSyncTargets.includes('preview'),
58 environmentVariablesDevelopment: envSyncTargets.includes('development'),
59 publicEnvVarPrefix: connection.public_env_var_prefix,
60 },
61 })
62
63 const { mutate: updateVercelConnection, isPending } = useVercelConnectionUpdateMutation({
64 onSuccess: () => {
65 form.reset(form.getValues())
66 toast.success(`Updated Vercel connection`)
67 },
68 })
69
70 function onSubmit(data: z.infer<typeof FormSchema>) {
71 const {
72 environmentVariablesProduction,
73 environmentVariablesPreview,
74 environmentVariablesDevelopment,
75 } = data
76
77 const envSyncTargets: string[] = []
78
79 if (environmentVariablesProduction) envSyncTargets.push('production')
80 if (environmentVariablesPreview) envSyncTargets.push('preview')
81 if (environmentVariablesDevelopment) envSyncTargets.push('development')
82
83 updateVercelConnection({
84 id: connection.id,
85 envSyncTargets: envSyncTargets as EnvironmentTargets[],
86 publicEnvVarPrefix: data.publicEnvVarPrefix?.trim(),
87 organizationIntegrationId: integration.id,
88 })
89 }
90
91 const vercelConnectionFormId = `vercel-connection-form-${connection.id}`
92
93 return (
94 <Form {...form}>
95 <form
96 id={vercelConnectionFormId}
97 onSubmit={form.handleSubmit(onSubmit)}
98 className={'w-full space-y-6'}
99 >
100 <div className="px-6 py-4 flex flex-col gap-y-4">
101 <div className="flex flex-col gap-4">
102 {org?.managed_by === 'vercel-marketplace' ? (
103 <Alert>
104 <AlertTitle className="text-sm">Vercel Marketplace managed project</AlertTitle>
105 <AlertDescription className="text-xs">
106 This project is managed via Vercel Marketplace. Environment variables are
107 automatically synchronized for your connected Vercel projects. This integration
108 purpose is synchronizing preview deployments environment variables with our{' '}
109 <Link
110 target="_blank"
111 rel="noreferrer"
112 href={`${DOCS_URL}/guides/platform/branching`}
113 className="underline"
114 >
115 Branching
116 </Link>{' '}
117 feature.
118 </AlertDescription>
119 </Alert>
120 ) : (
121 <div>
122 <h5 className="text-foreground ">
123 Sync environment variables for selected target environments
124 </h5>
125
126 <FormField
127 control={form.control}
128 name="environmentVariablesProduction"
129 render={({ field }) => (
130 <FormItem className="space-y-0 flex gap-x-4">
131 <FormControl>
132 <Switch
133 disabled={disabled}
134 className="mt-1"
135 checked={field.value}
136 onCheckedChange={field.onChange}
137 />
138 </FormControl>
139 <div>
140 <FormLabel className="!text">Production</FormLabel>
141 <FormDescription className="text-xs text-foreground-lighter">
142 Sync environment variables for <code>production</code> environment.
143 </FormDescription>
144 </div>
145 </FormItem>
146 )}
147 />
148 <FormField
149 control={form.control}
150 name="environmentVariablesPreview"
151 render={({ field }) => (
152 <FormItem className="space-y-0 flex gap-x-4">
153 <FormControl>
154 <Switch
155 disabled={disabled}
156 className="mt-1"
157 checked={field.value}
158 onCheckedChange={field.onChange}
159 />
160 </FormControl>
161 <div>
162 <FormLabel className="!text">Preview</FormLabel>
163 <FormDescription className="text-xs text-foreground-lighter">
164 Sync environment variables for <code>preview</code> environment.
165 </FormDescription>
166 </div>
167 </FormItem>
168 )}
169 />
170 <FormField
171 control={form.control}
172 name="environmentVariablesDevelopment"
173 render={({ field }) => (
174 <FormItem className="space-y-0 flex gap-x-4">
175 <FormControl>
176 <Switch
177 disabled={disabled}
178 className="mt-1"
179 checked={field.value}
180 onCheckedChange={field.onChange}
181 />
182 </FormControl>
183 <div>
184 <FormLabel className="!text">Development</FormLabel>
185 <FormDescription className="text-xs text-foreground-lighter">
186 Sync environment variables for <code>development</code> environment.
187 </FormDescription>
188 </div>
189 </FormItem>
190 )}
191 />
192 </div>
193 )}
194 </div>
195 <h5 className="mt-2 text-foreground">Customize public environment variable prefix</h5>
196 <div className="flex flex-col gap-4">
197 <FormField
198 control={form.control}
199 name="publicEnvVarPrefix"
200 render={({ field }) => (
201 <FormItem className="grid gap-2 md:grid md:grid-cols-12 space-y-0">
202 <FormLabel className="flex flex-col space-y-2 col-span-4 text-sm justify-center text-foreground-light">
203 Prefix
204 </FormLabel>
205 <FormControl className="col-span-8">
206 <Input
207 {...field}
208 className="w-full"
209 disabled={disabled}
210 placeholder="An empty prefix will result in no public env vars"
211 />
212 </FormControl>
213 <FormDescription className="col-start-5 col-span-8 text-xs">
214 e.g.{' '}
215 <code
216 className="cursor-pointer"
217 role="button"
218 onClick={() => {
219 field.onChange('NEXT_PUBLIC_')
220 }}
221 >
222 NEXT_PUBLIC_
223 </code>
224 ,{' '}
225 <code
226 className="cursor-pointer"
227 role="button"
228 onClick={() => {
229 field.onChange('VITE_PUBLIC_')
230 }}
231 >
232 VITE_PUBLIC_
233 </code>
234 ,{' '}
235 <code
236 className="cursor-pointer"
237 role="button"
238 onClick={() => {
239 field.onChange('PUBLIC_')
240 }}
241 >
242 PUBLIC_
243 </code>
244 , etc.
245 </FormDescription>
246
247 <FormMessage className="col-start-5 col-span-8" />
248 </FormItem>
249 )}
250 />
251 </div>
252
253 {form.formState.isDirty ? (
254 <p className="mt-2 text-sm text-warning">
255 Note: Changing these settings will <strong>not</strong> trigger a resync of
256 environment variables.
257 </p>
258 ) : (
259 <div className="mt-2 h-5 w-full" />
260 )}
261
262 <FormActions
263 disabled={disabled}
264 form={vercelConnectionFormId}
265 hasChanges={form.formState.isDirty}
266 isSubmitting={isPending}
267 handleReset={() => form.reset()}
268 />
269 </div>
270 </form>
271 </Form>
272 )
273}
274
275export default VercelIntegrationConnectionForm