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