install.tsx 9.0 KB

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