install.tsx259 lines · main
| 1 | import { useParams } from 'common' |
| 2 | import { AlertTriangle, Info } from 'lucide-react' |
| 3 | import Link from 'next/link' |
| 4 | import { useRouter } from 'next/router' |
| 5 | import { useEffect, useMemo, useState } from 'react' |
| 6 | import { toast } from 'sonner' |
| 7 | import { Alert, AlertDescription, AlertTitle, Button } from 'ui' |
| 8 | |
| 9 | import OrganizationPicker from '@/components/interfaces/Integrations/Vercel/OrganizationPicker' |
| 10 | import { Markdown } from '@/components/interfaces/Markdown' |
| 11 | import { getHasInstalledObject } from '@/components/layouts/IntegrationsLayout/Integrations.utils' |
| 12 | import VercelIntegrationWindowLayout from '@/components/layouts/IntegrationsLayout/VercelIntegrationWindowLayout' |
| 13 | import { ScaffoldColumn, ScaffoldContainer } from '@/components/layouts/Scaffold' |
| 14 | import { useIntegrationsQuery } from '@/data/integrations/integrations-query' |
| 15 | import { useVercelIntegrationCreateMutation } from '@/data/integrations/vercel-integration-create-mutation' |
| 16 | import { useOrganizationsQuery } from '@/data/organizations/organizations-query' |
| 17 | import { useIntegrationInstallationSnapshot } from '@/state/integration-installation' |
| 18 | import 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 | */ |
| 28 | export type VercelIntegrationFlow = 'deploy-button' | 'marketing' |
| 29 | |
| 30 | const 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 | |
| 255 | VercelIntegration.getLayout = (page) => ( |
| 256 | <VercelIntegrationWindowLayout>{page}</VercelIntegrationWindowLayout> |
| 257 | ) |
| 258 | |
| 259 | export default VercelIntegration |