install.tsx259 lines · main
1import { useParams } from 'common'
2import { AlertTriangle, Info } from 'lucide-react'
3import Link from 'next/link'
4import { useRouter } from 'next/router'
5import { useEffect, useMemo, useState } from 'react'
6import { toast } from 'sonner'
7import { Alert, AlertDescription, AlertTitle, Button } from 'ui'
8
9import OrganizationPicker from '@/components/interfaces/Integrations/Vercel/OrganizationPicker'
10import { Markdown } from '@/components/interfaces/Markdown'
11import { getHasInstalledObject } from '@/components/layouts/IntegrationsLayout/Integrations.utils'
12import VercelIntegrationWindowLayout from '@/components/layouts/IntegrationsLayout/VercelIntegrationWindowLayout'
13import { ScaffoldColumn, ScaffoldContainer } from '@/components/layouts/Scaffold'
14import { useIntegrationsQuery } from '@/data/integrations/integrations-query'
15import { useVercelIntegrationCreateMutation } from '@/data/integrations/vercel-integration-create-mutation'
16import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
17import { useIntegrationInstallationSnapshot } from '@/state/integration-installation'
18import type { NextPageWithLayout, Organization } from '@/types'
19
20/**
21 * Variations of the Vercel integration flow.
22 * They require different UI and logic.
23 *
24 * Deploy Button - the flow that starts from the Deploy Button - https://vercel.com/docs/integrations#deploy-button
25 * Marketplace - the flow that starts from the Marketplace - https://vercel.com/integrations
26 *
27 */
28export type VercelIntegrationFlow = 'deploy-button' | 'marketing'
29
30const VercelIntegration: NextPageWithLayout = () => {
31 const router = useRouter()
32 const { code, configurationId, teamId, source } = useParams()
33 const [selectedOrg, setSelectedOrg] = useState<Organization | null>(null)
34
35 const snapshot = useIntegrationInstallationSnapshot()
36
37 /**
38 * Fetch the list of organization based integration installations for Vercel.
39 *
40 * Array of integrations installed on all
41 */
42 const { data: integrationData } = useIntegrationsQuery()
43
44 const {
45 data: organizationsData,
46 isPending: isLoadingOrganizationsQuery,
47 isSuccess: isOrganizationsDataSuccess,
48 } = useOrganizationsQuery()
49
50 useEffect(() => {
51 if (organizationsData !== undefined && integrationData !== undefined) {
52 const firstOrg = organizationsData[0]
53
54 if (firstOrg && selectedOrg === null) {
55 setSelectedOrg(firstOrg)
56 router.query.organizationSlug = firstOrg.slug
57 }
58 }
59 }, [organizationsData, integrationData])
60
61 /**
62 * Organizations with extra `installationInstalled` attribute
63 *
64 * Used to show label/badge and allow/disallow installing
65 *
66 */
67 const installed = useMemo(
68 () =>
69 integrationData && organizationsData
70 ? getHasInstalledObject({
71 integrationName: 'Vercel',
72 integrationData,
73 organizationsData,
74 installationId: configurationId,
75 })
76 : {},
77 [configurationId, integrationData, organizationsData]
78 )
79
80 /**
81 * Handle the correct route change based on whether the vercel integration
82 * is following the 'marketplace/external' flow or 'deploy button' flow.
83 * See:
84 * - https://vercel.com/docs/integrations/create-integration/submit-integration#query-parameters-for-marketplace
85 * - https://vercel.com/docs/integrations/create-integration/submit-integration#query-parameters-for-external-flow
86 * - https://vercel.com/docs/integrations/create-integration/submit-integration#query-parameters-for-deploy-button
87 */
88 function handleRouteChange() {
89 const orgSlug = selectedOrg?.slug
90
91 switch (source) {
92 case 'deploy-button': {
93 router.push({
94 pathname: `/integrations/vercel/${orgSlug}/deploy-button/new-project`,
95 query: router.query,
96 })
97 break
98 }
99 case 'marketplace':
100 case 'external': {
101 router.push({
102 pathname: `/integrations/vercel/${orgSlug}/marketplace/choose-project`,
103 query: router.query,
104 })
105 break
106 }
107 default:
108 toast.error(
109 `Unsupported Vercel installation source: ${source}. Please contact support if this error persists.`
110 )
111 }
112 }
113
114 const { mutate, isPending: isLoadingVercelIntegrationCreateMutation } =
115 useVercelIntegrationCreateMutation({
116 onMutate() {
117 snapshot.setLoading(true)
118 },
119 onSuccess() {
120 handleRouteChange()
121 snapshot.setLoading(false)
122 },
123 onError(error) {
124 toast.error(`Creating Vercel integration failed: ${error.message}`)
125 },
126 })
127
128 function onInstall() {
129 const orgSlug = selectedOrg?.slug
130
131 const isIntegrationInstalled = orgSlug ? installed[orgSlug] : false
132
133 if (!orgSlug) {
134 return toast.error('Please select an organization')
135 }
136
137 if (!code) {
138 return toast.error('Vercel code missing')
139 }
140
141 if (!configurationId) {
142 return toast.error('Vercel Configuration ID missing')
143 }
144
145 if (!source) {
146 return toast.error('Vercel Configuration source missing')
147 }
148
149 /**
150 * Only install if integration hasn't already been installed
151 */
152 if (!isIntegrationInstalled) {
153 mutate({
154 code,
155 configurationId,
156 orgSlug,
157 metadata: {},
158 source,
159 teamId: teamId,
160 })
161 } else {
162 handleRouteChange()
163 }
164 }
165
166 const dataLoading = isLoadingVercelIntegrationCreateMutation || isLoadingOrganizationsQuery
167
168 const noOrganizations = useMemo(() => {
169 return isOrganizationsDataSuccess && organizationsData?.length === 0 ? true : false
170 }, [isOrganizationsDataSuccess, organizationsData])
171
172 const alreadyInstalled = useMemo(() => {
173 return selectedOrg && installed[selectedOrg.slug] && source === 'marketplace' && !dataLoading
174 ? true
175 : false
176 }, [installed, selectedOrg, source, dataLoading])
177
178 const disableInstallationForm =
179 (isLoadingVercelIntegrationCreateMutation && !dataLoading) ||
180 // disables installation button if integration is already installed and it is Marketplace flow
181 alreadyInstalled ||
182 noOrganizations
183
184 const isLoading = useMemo(() => {
185 return isLoadingVercelIntegrationCreateMutation || isLoadingOrganizationsQuery
186 }, [isLoadingVercelIntegrationCreateMutation, isLoadingOrganizationsQuery])
187
188 return (
189 <>
190 <ScaffoldContainer className="flex flex-col gap-6 grow py-8">
191 <ScaffoldColumn className="mx-auto w-full max-w-md">
192 <h2>Choose organization</h2>
193 <>
194 <Markdown content={`Choose the Briven organization you wish to install in`} />
195 <OrganizationPicker
196 integrationName="Vercel"
197 selectedOrg={selectedOrg}
198 disabled={noOrganizations || isLoading}
199 onSelectedOrgChange={(org) => {
200 setSelectedOrg(org)
201 router.query.organizationSlug = org.slug
202 }}
203 configurationId={configurationId}
204 />
205 {alreadyInstalled && (
206 <Alert variant="warning">
207 <AlertTriangle className="h-4 w-4" strokeWidth={2} />
208 <AlertTitle>Vercel Integration is already installed.</AlertTitle>
209 <AlertDescription>
210 You will need to choose another organization to install the integration.
211 </AlertDescription>
212 </Alert>
213 )}
214 {noOrganizations && (
215 <Alert variant="warning">
216 <AlertTriangle className="h-4 w-4" strokeWidth={2} />
217 <AlertTitle>No Briven Organizations to install Integration.</AlertTitle>
218 <AlertDescription className="prose">
219 You will need to create a Briven Organization before you can install the Vercel
220 Integration. You can create a new organization{' '}
221 <Link href="https://supabase.com/dashboard/new" target="_blank">
222 here
223 </Link>
224 .
225 </AlertDescription>
226 </Alert>
227 )}
228 <div className="flex flex-row w-full justify-end">
229 <Button
230 size="medium"
231 className="self-end"
232 disabled={disableInstallationForm || isLoadingVercelIntegrationCreateMutation}
233 loading={isLoadingVercelIntegrationCreateMutation}
234 onClick={onInstall}
235 >
236 Install integration
237 </Button>
238 </div>
239 </>
240 </ScaffoldColumn>
241 </ScaffoldContainer>
242 <ScaffoldContainer className="flex flex-col gap-6 py-3">
243 <Alert variant="default">
244 <Info className="h-4 w-4" strokeWidth={2} />
245 <AlertTitle>You can uninstall this Integration at any time.</AlertTitle>
246 <AlertDescription>
247 Remove this integration at any time from Vercel or the Briven dashboard.
248 </AlertDescription>
249 </Alert>
250 </ScaffoldContainer>
251 </>
252 )
253}
254
255VercelIntegration.getLayout = (page) => (
256 <VercelIntegrationWindowLayout>{page}</VercelIntegrationWindowLayout>
257)
258
259export default VercelIntegration