VercelSection.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. // @ts-nocheck
  2. import { PermissionAction } from '@supabase/shared-types/out/constants'
  3. import { ExternalLink } from 'lucide-react'
  4. import Link from 'next/link'
  5. import { useCallback, useMemo } from 'react'
  6. import { toast } from 'sonner'
  7. import { Button, cn } from 'ui'
  8. import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
  9. import { IntegrationImageHandler } from '../IntegrationsSettings'
  10. import VercelIntegrationConnectionForm from './VercelIntegrationConnectionForm'
  11. import { IntegrationConnectionItem } from '@/components/interfaces/Integrations/VercelGithub/IntegrationConnection'
  12. import {
  13. EmptyIntegrationConnection,
  14. IntegrationConnectionHeader,
  15. IntegrationInstallation,
  16. } from '@/components/interfaces/Integrations/VercelGithub/IntegrationPanels'
  17. import {
  18. ScaffoldContainer,
  19. ScaffoldSection,
  20. ScaffoldSectionContent,
  21. ScaffoldSectionDetail,
  22. } from '@/components/layouts/Scaffold'
  23. import { InlineLink } from '@/components/ui/InlineLink'
  24. import NoPermission from '@/components/ui/NoPermission'
  25. import { useOrgIntegrationsQuery } from '@/data/integrations/integrations-query-org-only'
  26. import { useIntegrationsVercelInstalledConnectionDeleteMutation } from '@/data/integrations/integrations-vercel-installed-connection-delete-mutation'
  27. import { useVercelProjectsQuery } from '@/data/integrations/integrations-vercel-projects-query'
  28. import type {
  29. Integration,
  30. IntegrationName,
  31. IntegrationProjectConnection,
  32. } from '@/data/integrations/integrations.types'
  33. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  34. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  35. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  36. import { pluralize } from '@/lib/helpers'
  37. import { getIntegrationConfigurationUrl } from '@/lib/integration-utils'
  38. import { useSidePanelsStateSnapshot } from '@/state/side-panels'
  39. export const VercelSection = ({ isProjectScoped }: { isProjectScoped: boolean }) => {
  40. const { data: project } = useSelectedProjectQuery()
  41. const { data: org } = useSelectedOrganizationQuery()
  42. const { data } = useOrgIntegrationsQuery({ orgSlug: org?.slug })
  43. const sidePanelsStateSnapshot = useSidePanelsStateSnapshot()
  44. const isBranch = project?.parent_project_ref !== undefined
  45. const { can: canReadVercelConnection, isLoading: isLoadingPermissions } =
  46. useAsyncCheckPermissions(PermissionAction.READ, 'integrations.vercel_connections')
  47. const { can: canCreateVercelConnection } = useAsyncCheckPermissions(
  48. PermissionAction.CREATE,
  49. 'integrations.vercel_connections'
  50. )
  51. const { can: canUpdateVercelConnection } = useAsyncCheckPermissions(
  52. PermissionAction.UPDATE,
  53. 'integrations.vercel_connections'
  54. )
  55. const { mutate: deleteVercelConnection } = useIntegrationsVercelInstalledConnectionDeleteMutation(
  56. {
  57. onSuccess: () => {
  58. toast.success('Successfully deleted Vercel connection')
  59. },
  60. }
  61. )
  62. const vercelIntegrations = useMemo(() => {
  63. return data
  64. ?.filter((integration) => integration.integration.name === 'Vercel')
  65. .map((integration) => {
  66. if (integration.metadata && integration.integration.name === 'Vercel') {
  67. const avatarSrc =
  68. !integration.metadata.account.avatar && integration.metadata.account.type === 'Team'
  69. ? `https://vercel.com/api/www/avatar?teamId=${integration.metadata.account.team_id}&s=48&format=png`
  70. : `https://vercel.com/api/www/avatar/${integration.metadata.account.avatar}?s=48`
  71. return {
  72. ...integration,
  73. metadata: {
  74. ...integration.metadata,
  75. account: {
  76. ...integration.metadata.account,
  77. avatar: avatarSrc,
  78. },
  79. },
  80. } as Integration
  81. }
  82. return integration
  83. })
  84. }, [data])
  85. // We're only supporting one Vercel integration per org for now
  86. // this will need to be updated when we support multiple integrations
  87. const vercelIntegration = vercelIntegrations?.[0]
  88. const { data: vercelProjectsData } = useVercelProjectsQuery(
  89. {
  90. organization_integration_id: vercelIntegration?.id,
  91. },
  92. { enabled: vercelIntegration?.id !== undefined }
  93. )
  94. const vercelProjectCount = vercelProjectsData?.length ?? 0
  95. const onAddVercelConnection = useCallback(
  96. (integrationId: string) => {
  97. sidePanelsStateSnapshot.setVercelConnectionsIntegrationId(integrationId)
  98. sidePanelsStateSnapshot.setVercelConnectionsOpen(true)
  99. },
  100. [sidePanelsStateSnapshot]
  101. )
  102. const onDeleteVercelConnection = useCallback(
  103. async (connection: IntegrationProjectConnection) => {
  104. deleteVercelConnection({
  105. id: connection.id,
  106. organization_integration_id: connection.organization_integration_id,
  107. orgSlug: org?.slug,
  108. })
  109. },
  110. [deleteVercelConnection, org?.slug]
  111. )
  112. const VercelTitle = `Vercel Integration`
  113. const integrationUrl =
  114. process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod'
  115. ? 'https://vercel.com/integrations/briven'
  116. : process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
  117. ? `https://vercel.com/integrations/briven-staging`
  118. : 'https://vercel.com/integrations/briven-local'
  119. let connections =
  120. (isProjectScoped
  121. ? vercelIntegration?.connections.filter(
  122. (connection) => connection.briven_project_ref === project?.ref
  123. )
  124. : vercelIntegration?.connections) || []
  125. const ConnectionHeaderTitle = `${connections.length} project ${pluralize(
  126. connections.length,
  127. 'connection'
  128. )} `
  129. return (
  130. <ScaffoldContainer>
  131. <ScaffoldSection className="py-12">
  132. <ScaffoldSectionDetail title={VercelTitle}>
  133. <p>Connect your Vercel teams to your Briven organization.</p>
  134. <IntegrationImageHandler title="vercel" />
  135. </ScaffoldSectionDetail>
  136. <ScaffoldSectionContent>
  137. {isLoadingPermissions ? (
  138. <GenericSkeletonLoader />
  139. ) : !canReadVercelConnection ? (
  140. <NoPermission resourceText="view this organization's Vercel connections" />
  141. ) : (
  142. <div className="space-y-6">
  143. <div className="space-y-1">
  144. <h3 className="text-sm font-medium text-foreground">
  145. How does the Vercel integration work?
  146. </h3>
  147. <p className="text-sm text-foreground-light">
  148. Briven will keep your environment variables up to date in each of the projects
  149. you assign to a Briven project. You can also link multiple Vercel projects to
  150. the same Briven project.
  151. </p>
  152. </div>
  153. <div>
  154. {vercelIntegration ? (
  155. <div key={vercelIntegration.id}>
  156. <IntegrationInstallation title={'Vercel'} integration={vercelIntegration} />
  157. {connections.length > 0 ? (
  158. <>
  159. <IntegrationConnectionHeader
  160. title={ConnectionHeaderTitle}
  161. markdown={`Repository connections for Vercel`}
  162. />
  163. <ul className="flex flex-col">
  164. {connections.map((connection) => (
  165. <div
  166. key={connection.id}
  167. className={cn(
  168. isProjectScoped && 'relative flex flex-col -gap-[1px] [&>li]:pb-0'
  169. )}
  170. >
  171. <IntegrationConnectionItem
  172. connection={connection}
  173. disabled={isBranch || !canUpdateVercelConnection}
  174. type={'Vercel' as IntegrationName}
  175. onDeleteConnection={onDeleteVercelConnection}
  176. className={cn(isProjectScoped && 'rounded-b-none! mb-0!')}
  177. />
  178. {isProjectScoped ? (
  179. <div className="relative pl-8 ml-6 border-l border-muted pb-6">
  180. <div className="border-b border-l border-r rounded-b-lg">
  181. <VercelIntegrationConnectionForm
  182. connection={connection}
  183. integration={vercelIntegration}
  184. disabled={isBranch || !canUpdateVercelConnection}
  185. />
  186. </div>
  187. </div>
  188. ) : null}
  189. </div>
  190. ))}
  191. </ul>
  192. </>
  193. ) : (
  194. <IntegrationConnectionHeader
  195. title={ConnectionHeaderTitle}
  196. className="pb-0"
  197. markdown={`Repository connections for Vercel`}
  198. />
  199. )}
  200. <EmptyIntegrationConnection
  201. disabled={isBranch || !canCreateVercelConnection}
  202. onClick={() => onAddVercelConnection(vercelIntegration.id)}
  203. >
  204. Add new project connection
  205. </EmptyIntegrationConnection>
  206. </div>
  207. ) : (
  208. <div>
  209. <Button
  210. icon={<ExternalLink />}
  211. asChild={!isBranch}
  212. type="default"
  213. disabled={isBranch}
  214. >
  215. {isBranch ? (
  216. <p>Install Vercel Integration</p>
  217. ) : (
  218. <Link href={integrationUrl} target="_blank" rel="noreferrer">
  219. Install Vercel Integration
  220. </Link>
  221. )}
  222. </Button>
  223. </div>
  224. )}
  225. </div>
  226. {vercelProjectCount > 0 && vercelIntegration !== undefined && (
  227. <p className="text-sm text-foreground-light">
  228. Your Vercel connection can access {vercelProjectCount} Vercel projects. To change
  229. which projects Briven may use, open your organization’s{' '}
  230. <InlineLink href={getIntegrationConfigurationUrl(vercelIntegration)}>
  231. Vercel integration settings
  232. </InlineLink>
  233. .
  234. </p>
  235. )}
  236. </div>
  237. )}
  238. </ScaffoldSectionContent>
  239. </ScaffoldSection>
  240. </ScaffoldContainer>
  241. )
  242. }