Files
4fd5f8adf1 Vercel project connect layout (#47623)
Bring the Vercel project connect layout up to date with other connect
screens. Follows up from https://github.com/supabase/supabase/pull/47550
which is required.

| Before | After |
| --- | --- |
| <img width="2368" height="1680" alt="image"
src="https://github.com/user-attachments/assets/f5c0b8ef-8fb5-4176-b0cf-98e33958dcc8"
/> | <img width="848" height="808" alt="image"
src="https://github.com/user-attachments/assets/e64c289f-0b73-4605-ad38-4552d3d6f934"
/> |

<img width="883" height="733" alt="image"
src="https://github.com/user-attachments/assets/7ccd9b72-de6a-4c21-ac8e-abe773bb8116"
/>


## Testing

- Open the deploy preview or staging URL for
`/dashboard/integrations/vercel/acme-production/marketplace/choose-project`
with callback params from a real Vercel Marketplace install redirect
(see _Vercel_ subheading below).

### Vercel

- Run through the [install
flow](https://github.com/supabase/supabase/pull/47550) on a real project
until you get to the project connection screen, copy the url and paste
into staging preview


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

* **New Features**
* Introduced interstitial-style Vercel integration experiences for both
install and choose-project flows, with improved page titles and clearer
environment-variable guidance (including tooltip details).

* **Bug Fixes**
* Improved Vercel integration matching by configuration id and
strengthened loading/error handling with more specific “not found”
messaging.

* **Refactor**
* Enhanced the Vercel/GitHub project chooser with a dedicated
interstitial mode.
* Updated partner logo rendering to support optional styling
customization.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
Co-authored-by: Alaister Young <alaister@users.noreply.github.com>
2026-07-10 18:22:36 +08:00

238 lines
9.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { ExternalLink } from 'lucide-react'
import { useCallback, useMemo } from 'react'
import { toast } from 'sonner'
import { cn } from 'ui'
import {
PageSection,
PageSectionContent,
PageSectionDescription,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { IntegrationSectionIcon } from '../IntegrationsSettings'
import VercelIntegrationConnectionForm from './VercelIntegrationConnectionForm'
import { IntegrationConnectionItem } from '@/components/interfaces/Integrations/VercelGithub/IntegrationConnection'
import {
EmptyIntegrationConnection,
IntegrationConnectionHeader,
IntegrationInstallation,
} from '@/components/interfaces/Integrations/VercelGithub/IntegrationPanels'
import { InlineLink } from '@/components/ui/InlineLink'
import { NoPermission } from '@/components/ui/NoPermission'
import { useOrgIntegrationsQuery } from '@/data/integrations/integrations-query-org-only'
import { useIntegrationsVercelInstalledConnectionDeleteMutation } from '@/data/integrations/integrations-vercel-installed-connection-delete-mutation'
import { useVercelProjectsQuery } from '@/data/integrations/integrations-vercel-projects-query'
import type {
Integration,
IntegrationName,
IntegrationProjectConnection,
} from '@/data/integrations/integrations.types'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { pluralize } from '@/lib/helpers'
import { getIntegrationConfigurationUrl } from '@/lib/integration-utils'
import { useSidePanelsStateSnapshot } from '@/state/side-panels'
export const VercelSection = ({ isProjectScoped }: { isProjectScoped: boolean }) => {
const { data: project } = useSelectedProjectQuery()
const { data: org } = useSelectedOrganizationQuery()
const { data } = useOrgIntegrationsQuery({ orgSlug: org?.slug })
const sidePanelsStateSnapshot = useSidePanelsStateSnapshot()
const isBranch = project?.parent_project_ref !== undefined
const { can: canReadVercelConnection, isLoading: isLoadingPermissions } =
useAsyncCheckPermissions(PermissionAction.READ, 'integrations.vercel_connections')
const { can: canCreateVercelConnection } = useAsyncCheckPermissions(
PermissionAction.CREATE,
'integrations.vercel_connections'
)
const { can: canUpdateVercelConnection } = useAsyncCheckPermissions(
PermissionAction.UPDATE,
'integrations.vercel_connections'
)
const { mutate: deleteVercelConnection } = useIntegrationsVercelInstalledConnectionDeleteMutation(
{
onSuccess: () => {
toast.success('Successfully deleted Vercel connection')
},
}
)
const vercelIntegrations = useMemo(() => {
return data
?.filter((integration) => integration.integration.name === 'Vercel')
.map((integration) => {
if (integration.metadata && integration.integration.name === 'Vercel') {
const avatarSrc =
!integration.metadata.account.avatar && integration.metadata.account.type === 'Team'
? `https://vercel.com/api/www/avatar?teamId=${integration.metadata.account.team_id}&s=48&format=png`
: `https://vercel.com/api/www/avatar/${integration.metadata.account.avatar}?s=48`
return {
...integration,
metadata: {
...integration.metadata,
account: {
...integration.metadata.account,
avatar: avatarSrc,
},
},
} as Integration
}
return integration
})
}, [data])
// We're only supporting one Vercel integration per org for now
// this will need to be updated when we support multiple integrations
const vercelIntegration = vercelIntegrations?.[0]
const { data: vercelProjectsData } = useVercelProjectsQuery(
{
organization_integration_id: vercelIntegration?.id,
},
{ enabled: vercelIntegration?.id !== undefined }
)
const vercelProjectCount = vercelProjectsData?.length ?? 0
const onAddVercelConnection = useCallback(
(integrationId: string) => {
sidePanelsStateSnapshot.setVercelConnectionsIntegrationId(integrationId)
sidePanelsStateSnapshot.setVercelConnectionsOpen(true)
},
[sidePanelsStateSnapshot]
)
const onDeleteVercelConnection = useCallback(
async (connection: IntegrationProjectConnection) => {
deleteVercelConnection({
id: connection.id,
organization_integration_id: connection.organization_integration_id,
orgSlug: org?.slug,
})
},
[deleteVercelConnection, org?.slug]
)
const integrationUrl =
process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod'
? 'https://vercel.com/integrations/supabase'
: process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
? `https://vercel.com/integrations/supabase-staging`
: 'https://vercel.com/integrations/supabase-local'
let connections =
(isProjectScoped
? vercelIntegration?.connections.filter(
(connection) => connection.supabase_project_ref === project?.ref
)
: vercelIntegration?.connections) || []
const ConnectionHeaderTitle = `${connections.length} project ${pluralize(
connections.length,
'connection'
)} `
const description = isProjectScoped
? 'Connect Vercel projects to this Supabase project. Supabase keeps environment variables up to date in each connected Vercel project.'
: 'Connect your Vercel teams to this Supabase organization. Supabase keeps environment variables up to date in each connected project. You can also link multiple Vercel projects to the same Supabase project.'
return (
<PageSection>
<PageSectionMeta>
<div className="flex flex-1 items-start gap-6">
<IntegrationSectionIcon title="vercel" />
<PageSectionSummary>
<PageSectionTitle>Vercel</PageSectionTitle>
<PageSectionDescription>{description}</PageSectionDescription>
</PageSectionSummary>
</div>
</PageSectionMeta>
<PageSectionContent>
{isLoadingPermissions ? (
<GenericSkeletonLoader />
) : !canReadVercelConnection ? (
<NoPermission resourceText="view this organization's Vercel connections" />
) : (
<div className="space-y-6">
<div>
{vercelIntegration ? (
<div key={vercelIntegration.id}>
<IntegrationInstallation title="Vercel" integration={vercelIntegration} />
<IntegrationConnectionHeader
title={ConnectionHeaderTitle}
className={connections.length === 0 ? 'pb-0' : undefined}
markdown="Project connections for Vercel"
/>
{connections.length > 0 && (
<ul className="flex flex-col gap-y-2">
{connections.map((connection) => (
<div
key={connection.id}
className={cn(
isProjectScoped && 'relative flex flex-col -gap-[1px] [&>li]:pb-0'
)}
>
<IntegrationConnectionItem
connection={connection}
disabled={isBranch || !canUpdateVercelConnection}
type={'Vercel' as IntegrationName}
onDeleteConnection={onDeleteVercelConnection}
className={cn(isProjectScoped && 'rounded-b-none! mb-0!')}
/>
{isProjectScoped ? (
<div className="relative pl-8 ml-6 border-l border-muted pb-6">
<div className="border-b border-l border-r rounded-b-lg">
<VercelIntegrationConnectionForm
connection={connection}
integration={vercelIntegration}
disabled={isBranch || !canUpdateVercelConnection}
/>
</div>
</div>
) : null}
</div>
))}
</ul>
)}
<EmptyIntegrationConnection
disabled={isBranch || !canCreateVercelConnection}
onClick={() => onAddVercelConnection(vercelIntegration.id)}
>
Add new project connection
</EmptyIntegrationConnection>
</div>
) : (
<EmptyIntegrationConnection
showNode={false}
disabled={isBranch}
href={integrationUrl}
icon={<ExternalLink />}
disabledTooltip="Install the Vercel integration on your project's main branch"
>
Install Vercel integration
</EmptyIntegrationConnection>
)}
</div>
{vercelProjectCount > 0 && vercelIntegration !== undefined && (
<p className="text-sm text-foreground-light">
Your Vercel connection can access {vercelProjectCount} Vercel projects. To change
which projects Supabase may use, open your organization’s{' '}
<InlineLink href={getIntegrationConfigurationUrl(vercelIntegration)}>
Vercel integration settings
</InlineLink>
.
</p>
)}
</div>
)}
</PageSectionContent>
</PageSection>
)
}