mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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>
This commit is contained in:
10 files changed
+608
-404
No files matched your search
@@ -1,3 +1,5 @@
|
||||
import type { Integration } from '@/data/integrations/integrations.types'
|
||||
|
||||
export function isVercelUrl(url: string): boolean {
|
||||
try {
|
||||
const u = new URL(url)
|
||||
@@ -7,3 +9,17 @@ export function isVercelUrl(url: string): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function findVercelIntegrationByConfigurationId(
|
||||
integrations: Integration[] | undefined,
|
||||
configurationId: string | undefined
|
||||
): Integration | undefined {
|
||||
if (!configurationId) return undefined
|
||||
|
||||
return integrations?.find(
|
||||
(integration) =>
|
||||
integration.metadata !== undefined &&
|
||||
'configuration_id' in integration.metadata &&
|
||||
integration.metadata.configuration_id === configurationId
|
||||
)
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import Link from 'next/link'
|
||||
import { Button, HoverCard, HoverCardContent, HoverCardTrigger } from 'ui'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
|
||||
import { ENV_VAR_RAW_KEYS } from './Integrations-Vercel.constants'
|
||||
import { LogoPair, PartnerLogo, SupabaseLogo } from '@/components/layouts/InterstitialLayout'
|
||||
import { BASE_PATH } from '@/lib/constants'
|
||||
|
||||
const VERCEL_ENV_VAR_COUNT = ENV_VAR_RAW_KEYS.length
|
||||
const VERCEL_ICON_SRC = `${BASE_PATH}/img/icons/vercel-icon.svg`
|
||||
|
||||
export const VERCEL_INTEGRATION_ICON = <img src={VERCEL_ICON_SRC} alt="Vercel" className="w-4" />
|
||||
|
||||
export function VercelIntegrationLogo() {
|
||||
return (
|
||||
<LogoPair
|
||||
left={
|
||||
<PartnerLogo
|
||||
src={VERCEL_ICON_SRC}
|
||||
alt="Vercel"
|
||||
className="bg-surface-75"
|
||||
imageClassName="size-7 object-contain dark:invert"
|
||||
/>
|
||||
}
|
||||
right={<SupabaseLogo />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function VercelIntegrationFooter() {
|
||||
return (
|
||||
<p className="text-xs text-foreground-lighter">
|
||||
You can remove this integration at any time from Vercel or the Supabase dashboard.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
export function VercelEnvVarsSyncDescription() {
|
||||
return (
|
||||
<>
|
||||
<HoverCard openDelay={200}>
|
||||
<HoverCardTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
className="h-auto p-0 text-sm font-normal text-foreground-lighter underline decoration-dotted underline-offset-2"
|
||||
>
|
||||
{VERCEL_ENV_VAR_COUNT} environment variables
|
||||
</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent align="center" className="w-80">
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium text-foreground">Environment variables</h4>
|
||||
<ul className="max-h-52 space-y-1 overflow-y-auto">
|
||||
{ENV_VAR_RAW_KEYS.map((key) => (
|
||||
<li key={key} className="font-mono text-xs text-foreground-lighter">
|
||||
{key}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>{' '}
|
||||
will be synced to your Vercel project.
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface VercelIntegrationInterstitialErrorStateProps {
|
||||
title: string
|
||||
errorMessage?: string | null
|
||||
}
|
||||
|
||||
export function VercelIntegrationInterstitialErrorState({
|
||||
title,
|
||||
errorMessage,
|
||||
}: VercelIntegrationInterstitialErrorStateProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Admonition
|
||||
type="warning"
|
||||
title={title}
|
||||
description={
|
||||
<>
|
||||
Retry the installation request from Vercel.
|
||||
{errorMessage && (
|
||||
<span className="mt-1 block text-foreground-lighter">Error: {errorMessage}</span>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Button variant="default" block asChild>
|
||||
<Link href="/">Back to dashboard</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import { getIntegrationConfigurationUrl } from '@/lib/integration-utils'
|
||||
const ICON_STROKE_WIDTH = 2
|
||||
const ICON_SIZE = 14
|
||||
|
||||
export interface IntegrationInstallationProps extends RefAttributes<HTMLLIElement> {
|
||||
interface IntegrationInstallationProps extends RefAttributes<HTMLLIElement> {
|
||||
title: string
|
||||
integration: Integration
|
||||
disabled?: boolean
|
||||
@@ -72,16 +72,16 @@ const Avatar = ({ src }: { src: string | undefined }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const IntegrationIconBlock = ({ integration }: { integration: Integration }) => {
|
||||
return (
|
||||
<div className="bg-black text-white w-8 h-8 rounded-sm flex items-center justify-center">
|
||||
<HandleIcon type={integration.integration.name} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const IntegrationInstallation = forwardRef<HTMLLIElement, IntegrationInstallationProps>(
|
||||
({ integration, disabled, ...props }, ref) => {
|
||||
const IntegrationIconBlock = () => {
|
||||
return (
|
||||
<div className="bg-black text-white w-8 h-8 rounded-sm flex items-center justify-center">
|
||||
<HandleIcon type={integration.integration.name} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
ref={ref}
|
||||
@@ -92,7 +92,7 @@ export const IntegrationInstallation = forwardRef<HTMLLIElement, IntegrationInst
|
||||
<div className="flex gap-6 items-center">
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="flex -space-x-1">
|
||||
<IntegrationIconBlock />
|
||||
<IntegrationIconBlock integration={integration} />
|
||||
<Avatar src={integration?.metadata?.account.avatar} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -299,7 +299,7 @@ export const EmptyIntegrationConnection = forwardRef<
|
||||
{...props}
|
||||
className={cn(
|
||||
showNode && 'ml-6 pl-8 mt-4 border-l',
|
||||
'relative pb-2',
|
||||
'relative',
|
||||
'last:border-l-transparent',
|
||||
className
|
||||
)}
|
||||
@@ -361,7 +361,7 @@ export const IntegrationConnectionHeader = forwardRef<HTMLDivElement, Integratio
|
||||
)}
|
||||
>
|
||||
{props.title && <h5 className="text-foreground">{props.title}</h5>}
|
||||
<Markdown content={markdown} />
|
||||
<Markdown content={markdown} className="[&>p]:my-0" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Check, ChevronDown, Plus, PlusIcon } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import { HTMLAttributes, ReactNode, useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Badge,
|
||||
@@ -26,14 +26,13 @@ import {
|
||||
IntegrationProjectConnection,
|
||||
} from '@/data/integrations/integrations.types'
|
||||
import { useOrgProjectsInfiniteQuery } from '@/data/projects/org-projects-infinite-query'
|
||||
import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
|
||||
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { BASE_PATH } from '@/lib/constants'
|
||||
import { openInstallGitHubIntegrationWindow } from '@/lib/github'
|
||||
import { EMPTY_ARR } from '@/lib/void'
|
||||
|
||||
export interface Project {
|
||||
interface Project {
|
||||
name: string
|
||||
ref: string
|
||||
}
|
||||
@@ -44,7 +43,21 @@ export interface ForeignProject {
|
||||
installation_id?: number
|
||||
}
|
||||
|
||||
export interface ProjectLinkerProps {
|
||||
const Panel = ({ children, className, ...props }: HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex-1 min-w-0 flex flex-col grow gap-6 px-5 mx-auto w-full justify-center items-center',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ProjectLinkerProps {
|
||||
slug?: string
|
||||
organizationIntegrationId?: string
|
||||
foreignProjects: ForeignProject[]
|
||||
@@ -57,13 +70,13 @@ export interface ProjectLinkerProps {
|
||||
onSkip?: () => void
|
||||
loadingForeignProjects?: boolean
|
||||
showNoEntitiesState?: boolean
|
||||
|
||||
defaultSupabaseProjectRef?: string
|
||||
defaultSupabaseProject?: Project
|
||||
defaultForeignProjectId?: string
|
||||
mode: 'Vercel' | 'GitHub'
|
||||
variant?: 'default' | 'interstitial'
|
||||
}
|
||||
|
||||
const ProjectLinker = ({
|
||||
export const ProjectLinker = ({
|
||||
slug,
|
||||
organizationIntegrationId,
|
||||
foreignProjects,
|
||||
@@ -76,10 +89,10 @@ const ProjectLinker = ({
|
||||
onSkip,
|
||||
loadingForeignProjects,
|
||||
showNoEntitiesState = true,
|
||||
|
||||
defaultSupabaseProjectRef,
|
||||
defaultSupabaseProject,
|
||||
defaultForeignProjectId,
|
||||
mode,
|
||||
variant = 'default',
|
||||
}: ProjectLinkerProps) => {
|
||||
const router = useRouter()
|
||||
const projectCreationEnabled = useIsFeatureEnabled('projects:create')
|
||||
@@ -89,9 +102,7 @@ const ProjectLinker = ({
|
||||
const [foreignProjectId, setForeignProjectId] = useState<string | undefined>(
|
||||
defaultForeignProjectId
|
||||
)
|
||||
const [supabaseProjectRef, setSupabaseProjectRef] = useState<string | undefined>(
|
||||
defaultSupabaseProjectRef
|
||||
)
|
||||
const [selectedSupabaseProject, setSelectedSupabaseProject] = useState<Project>()
|
||||
|
||||
const { data: selectedOrganization } = useSelectedOrganizationQuery()
|
||||
const { data: orgProjects, isPending: loadingSupabaseProjects } = useOrgProjectsInfiniteQuery({
|
||||
@@ -100,9 +111,9 @@ const ProjectLinker = ({
|
||||
const numProjects = orgProjects?.pages[0].pagination.count ?? 0
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultSupabaseProjectRef !== undefined && supabaseProjectRef === undefined)
|
||||
setSupabaseProjectRef(defaultSupabaseProjectRef)
|
||||
}, [defaultSupabaseProjectRef, supabaseProjectRef])
|
||||
if (defaultSupabaseProject !== undefined && selectedSupabaseProject === undefined)
|
||||
setSelectedSupabaseProject(defaultSupabaseProject)
|
||||
}, [defaultSupabaseProject, selectedSupabaseProject])
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultForeignProjectId !== undefined && foreignProjectId === undefined)
|
||||
@@ -112,8 +123,6 @@ const ProjectLinker = ({
|
||||
// create a flat array of foreign project ids. ie, ["prj_MlkO6AiLG5ofS9ojKrkS3PhhlY3f", ..]
|
||||
const flatInstalledConnectionsIds = new Set(installedConnections.map((x) => x.foreign_project_id))
|
||||
|
||||
const { data: selectedSupabaseProject } = useProjectDetailQuery({ ref: supabaseProjectRef })
|
||||
|
||||
const selectedForeignProject = foreignProjectId
|
||||
? foreignProjects.find((x) => x.id?.toLowerCase() === foreignProjectId?.toLowerCase())
|
||||
: undefined
|
||||
@@ -150,25 +159,248 @@ const ProjectLinker = ({
|
||||
})
|
||||
}
|
||||
|
||||
const Panel = ({ children, className, ...props }: React.HTMLAttributes<HTMLDivElement>) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex-1 min-w-0 flex flex-col grow gap-6 px-5 mx-auto w-full justify-center items-center',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const noSupabaseProjects = numProjects === 0
|
||||
const noForeignProjects = foreignProjects.length === 0
|
||||
const missingEntity = noSupabaseProjects ? 'Supabase' : mode
|
||||
const oppositeMissingEntity = noSupabaseProjects ? mode : 'Supabase'
|
||||
|
||||
const connectDisabled =
|
||||
loadingForeignProjects ||
|
||||
loadingSupabaseProjects ||
|
||||
isLoading ||
|
||||
!selectedSupabaseProject ||
|
||||
!selectedForeignProject
|
||||
|
||||
const supabaseProjectSelector = (
|
||||
<OrganizationProjectSelector
|
||||
sameWidthAsTrigger
|
||||
open={openProjectsDropdown}
|
||||
setOpen={setOpenProjectsDropdown}
|
||||
slug={slug}
|
||||
selectedRef={selectedSupabaseProject?.ref}
|
||||
onSelect={(project) => {
|
||||
setSelectedSupabaseProject(project)
|
||||
setOpenProjectsDropdown(false)
|
||||
}}
|
||||
renderRow={(project) => {
|
||||
return (
|
||||
<div className={cn('w-full flex items-center justify-between')}>
|
||||
<div className="flex items-center gap-x-2">
|
||||
{variant === 'default' && (
|
||||
<div className="bg-white shadow-sm border rounded-sm p-1 w-6 h-6 flex justify-center items-center">
|
||||
<img src={`${BASE_PATH}/img/supabase-logo.svg`} alt="Supabase" className="w-4" />
|
||||
</div>
|
||||
)}
|
||||
<p>{project.name}</p>
|
||||
{project.status === 'INACTIVE' && <Badge>Paused</Badge>}
|
||||
{project.status === 'GOING_DOWN' && <Badge>Pausing</Badge>}
|
||||
</div>
|
||||
{project.ref === selectedSupabaseProject?.ref && <Check size={16} />}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
renderTrigger={() => {
|
||||
return (
|
||||
<Button
|
||||
variant="default"
|
||||
block
|
||||
disabled={defaultSupabaseProject !== undefined || loadingSupabaseProjects}
|
||||
loading={loadingSupabaseProjects}
|
||||
className="justify-between h-[34px]"
|
||||
iconRight={
|
||||
defaultSupabaseProject === undefined ? (
|
||||
<span className="grow flex justify-end">
|
||||
<ChevronDown />
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
{variant === 'default' && (
|
||||
<div className="bg-white shadow-sm border rounded-sm p-1 w-6 h-6 flex justify-center items-center">
|
||||
<img src={`${BASE_PATH}/img/supabase-logo.svg`} alt="Supabase" className="w-4" />
|
||||
</div>
|
||||
)}
|
||||
<span className="truncate">
|
||||
{selectedSupabaseProject ? selectedSupabaseProject.name : 'Choose Supabase project'}
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
}}
|
||||
renderActions={() => {
|
||||
return (
|
||||
projectCreationEnabled && (
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
className="cursor-pointer w-full"
|
||||
onSelect={() => {
|
||||
setOpenProjectsDropdown(false)
|
||||
router.push(`/new/${selectedOrganization?.slug}`)
|
||||
}}
|
||||
onClick={() => setOpenProjectsDropdown(false)}
|
||||
>
|
||||
<Link
|
||||
href={`/new/${selectedOrganization?.slug}`}
|
||||
onClick={() => {
|
||||
setOpenProjectsDropdown(false)
|
||||
}}
|
||||
className="w-full flex items-center gap-2"
|
||||
>
|
||||
<Plus size={14} strokeWidth={1.5} />
|
||||
<p>Create a new project</p>
|
||||
</Link>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
const foreignProjectSelector = (
|
||||
<Popover open={openForeignProjectsComboBox} onOpenChange={setOpenForeignProjectsComboBox}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
block
|
||||
disabled={loadingForeignProjects}
|
||||
loading={loadingForeignProjects}
|
||||
className={cn(
|
||||
variant === 'interstitial' ? 'h-[34px] justify-between' : 'justify-start h-[34px]'
|
||||
)}
|
||||
icon={
|
||||
variant === 'default' ? (
|
||||
<div>
|
||||
{selectedForeignProject
|
||||
? (getForeignProjectIcon?.(selectedForeignProject) ?? integrationIcon)
|
||||
: integrationIcon}
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
iconRight={
|
||||
<span className="grow flex justify-end">
|
||||
<ChevronDown />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<span className="truncate">
|
||||
{(selectedForeignProject && selectedForeignProject.name) ?? choosePrompt}
|
||||
</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" side="bottom" align="center" sameWidthAsTrigger>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search for a project" />
|
||||
<CommandList className="max-h-[170px]!">
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{foreignProjects.map((project, i) => {
|
||||
return (
|
||||
<CommandItem
|
||||
key={project.id}
|
||||
value={`${project.name.replaceAll('"', '')}-${i}`}
|
||||
className="flex gap-2 items-center"
|
||||
onSelect={() => {
|
||||
if (project.id) setForeignProjectId(project.id)
|
||||
setOpenForeignProjectsComboBox(false)
|
||||
}}
|
||||
>
|
||||
<div>{getForeignProjectIcon?.(project) ?? integrationIcon}</div>
|
||||
<span className="truncate" title={project.name}>
|
||||
{project.name}
|
||||
</span>
|
||||
</CommandItem>
|
||||
)
|
||||
})}
|
||||
{foreignProjects.length === 0 && <CommandEmpty>No results found.</CommandEmpty>}
|
||||
</CommandGroup>
|
||||
{mode === 'GitHub' && (
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
className="flex gap-2 items-center cursor-pointer"
|
||||
onSelect={() => openInstallGitHubIntegrationWindow('install')}
|
||||
>
|
||||
<PlusIcon size={16} />
|
||||
Add GitHub Repositories
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
|
||||
const actionButtons = (
|
||||
<div
|
||||
className={cn('flex w-full gap-2', variant === 'interstitial' ? 'flex-col' : 'justify-end')}
|
||||
>
|
||||
<Button
|
||||
size={variant === 'interstitial' ? undefined : 'medium'}
|
||||
variant={variant === 'interstitial' ? 'primary' : 'default'}
|
||||
block={variant === 'interstitial'}
|
||||
className={variant === 'default' ? 'self-end' : undefined}
|
||||
onClick={onCreateConnections}
|
||||
loading={isLoading}
|
||||
disabled={connectDisabled}
|
||||
>
|
||||
Connect project
|
||||
</Button>
|
||||
{onSkip !== undefined && (
|
||||
<Button
|
||||
size={variant === 'interstitial' ? undefined : 'medium'}
|
||||
variant={variant === 'interstitial' ? 'text' : 'default'}
|
||||
block={variant === 'interstitial'}
|
||||
onClick={() => {
|
||||
onSkip()
|
||||
}}
|
||||
>
|
||||
Skip
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
if (variant === 'interstitial') {
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
{loadingForeignProjects || loadingSupabaseProjects ? (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-foreground-light">Loading projects</p>
|
||||
<ShimmerLine active />
|
||||
</div>
|
||||
) : showNoEntitiesState && (noSupabaseProjects || noForeignProjects) ? (
|
||||
<div className="text-sm text-foreground-lighter text-balance">
|
||||
No {missingEntity} projects found. Create a {missingEntity} project to link to a{' '}
|
||||
{oppositeMissingEntity} project, or skip and connect later.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<section className="space-y-2" aria-label="Supabase project">
|
||||
<p className="text-xs font-medium uppercase tracking-wider text-foreground-light">
|
||||
Supabase project
|
||||
</p>
|
||||
{supabaseProjectSelector}
|
||||
</section>
|
||||
|
||||
<section className="space-y-2" aria-label="Vercel project">
|
||||
<p className="text-xs font-medium uppercase tracking-wider text-foreground-light">
|
||||
Vercel project
|
||||
</p>
|
||||
{foreignProjectSelector}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
|
||||
{actionButtons}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col bg border shadow-sm rounded-lg overflow-hidden">
|
||||
<div className="relative p-12 border-b border-muted">
|
||||
@@ -199,94 +431,7 @@ const ProjectLinker = ({
|
||||
<img src={`${BASE_PATH}/img/supabase-logo.svg`} alt="Supabase" className="w-6" />
|
||||
</div>
|
||||
|
||||
<OrganizationProjectSelector
|
||||
sameWidthAsTrigger
|
||||
open={openProjectsDropdown}
|
||||
setOpen={setOpenProjectsDropdown}
|
||||
slug={slug}
|
||||
selectedRef={supabaseProjectRef}
|
||||
onSelect={(project) => {
|
||||
setSupabaseProjectRef(project.ref)
|
||||
setOpenProjectsDropdown(false)
|
||||
}}
|
||||
renderRow={(project) => {
|
||||
return (
|
||||
<div className={cn('w-full flex items-center justify-between')}>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="bg-white shadow-sm border rounded-sm p-1 w-6 h-6 flex justify-center items-center">
|
||||
<img
|
||||
src={`${BASE_PATH}/img/supabase-logo.svg`}
|
||||
alt="Supabase"
|
||||
className="w-4"
|
||||
/>
|
||||
</div>
|
||||
<p>{project.name}</p>
|
||||
{project.status === 'INACTIVE' && <Badge>Paused</Badge>}
|
||||
{project.status === 'GOING_DOWN' && <Badge>Pausing</Badge>}
|
||||
</div>
|
||||
{project.ref === supabaseProjectRef && <Check size={16} />}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
renderTrigger={() => {
|
||||
return (
|
||||
<Button
|
||||
variant="default"
|
||||
block
|
||||
disabled={defaultSupabaseProjectRef !== undefined || loadingSupabaseProjects}
|
||||
loading={loadingSupabaseProjects}
|
||||
className="justify-between h-[34px]"
|
||||
iconRight={
|
||||
defaultSupabaseProjectRef === undefined ? (
|
||||
<span className="grow flex justify-end">
|
||||
<ChevronDown />
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="bg-white shadow-sm border rounded-sm p-1 w-6 h-6 flex justify-center items-center">
|
||||
<img
|
||||
src={`${BASE_PATH}/img/supabase-logo.svg`}
|
||||
alt="Supabase"
|
||||
className="w-4"
|
||||
/>
|
||||
</div>
|
||||
{selectedSupabaseProject
|
||||
? selectedSupabaseProject.name
|
||||
: 'Choose Supabase Project'}
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
}}
|
||||
renderActions={() => {
|
||||
return (
|
||||
projectCreationEnabled && (
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
className="cursor-pointer w-full"
|
||||
onSelect={() => {
|
||||
setOpenProjectsDropdown(false)
|
||||
router.push(`/new/${selectedOrganization?.slug}`)
|
||||
}}
|
||||
onClick={() => setOpenProjectsDropdown(false)}
|
||||
>
|
||||
<Link
|
||||
href={`/new/${selectedOrganization?.slug}`}
|
||||
onClick={() => {
|
||||
setOpenProjectsDropdown(false)
|
||||
}}
|
||||
className="w-full flex items-center gap-2"
|
||||
>
|
||||
<Plus size={14} strokeWidth={1.5} />
|
||||
<p>Create a new project</p>
|
||||
</Link>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{supabaseProjectSelector}
|
||||
</Panel>
|
||||
|
||||
<div className="border border-foreground-lighter h-px w-8 border-dashed self-end mb-4" />
|
||||
@@ -296,116 +441,13 @@ const ProjectLinker = ({
|
||||
{integrationIcon}
|
||||
</div>
|
||||
|
||||
<Popover
|
||||
open={openForeignProjectsComboBox}
|
||||
onOpenChange={setOpenForeignProjectsComboBox}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
block
|
||||
disabled={loadingForeignProjects}
|
||||
loading={loadingForeignProjects}
|
||||
className="justify-start h-[34px]"
|
||||
icon={
|
||||
<div>
|
||||
{selectedForeignProject
|
||||
? (getForeignProjectIcon?.(selectedForeignProject) ?? integrationIcon)
|
||||
: integrationIcon}
|
||||
</div>
|
||||
}
|
||||
iconRight={
|
||||
<span className="grow flex justify-end">
|
||||
<ChevronDown />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{(selectedForeignProject && selectedForeignProject.name) ?? choosePrompt}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" side="bottom" align="center" sameWidthAsTrigger>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search for a project" />
|
||||
<CommandList className="max-h-[170px]!">
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{foreignProjects.map((project, i) => {
|
||||
return (
|
||||
<CommandItem
|
||||
key={project.id}
|
||||
value={`${project.name.replaceAll('"', '')}-${i}`}
|
||||
className="flex gap-2 items-center"
|
||||
onSelect={() => {
|
||||
if (project.id) setForeignProjectId(project.id)
|
||||
setOpenForeignProjectsComboBox(false)
|
||||
}}
|
||||
>
|
||||
<div>{getForeignProjectIcon?.(project) ?? integrationIcon}</div>
|
||||
<span className="truncate" title={project.name}>
|
||||
{project.name}
|
||||
</span>
|
||||
</CommandItem>
|
||||
)
|
||||
})}
|
||||
{foreignProjects.length === 0 && (
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
)}
|
||||
</CommandGroup>
|
||||
{mode === 'GitHub' && (
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
className="flex gap-2 items-center cursor-pointer"
|
||||
onSelect={() => openInstallGitHubIntegrationWindow('install')}
|
||||
>
|
||||
<PlusIcon size={16} />
|
||||
Add GitHub Repositories
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{foreignProjectSelector}
|
||||
</Panel>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex w-full justify-end gap-2 p-4 bg-surface-75">
|
||||
{onSkip !== undefined && (
|
||||
<Button
|
||||
size="medium"
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
onSkip()
|
||||
}}
|
||||
>
|
||||
Skip
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="medium"
|
||||
className="self-end"
|
||||
onClick={onCreateConnections}
|
||||
loading={isLoading}
|
||||
disabled={
|
||||
// data loading states
|
||||
loadingForeignProjects ||
|
||||
loadingSupabaseProjects ||
|
||||
isLoading ||
|
||||
// check whether both project types are not undefined
|
||||
!selectedSupabaseProject ||
|
||||
!selectedForeignProject
|
||||
}
|
||||
>
|
||||
Connect project
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex w-full justify-end gap-2 p-4 bg-surface-75">{actionButtons}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectLinker
|
||||
+5
-4
@@ -1,12 +1,12 @@
|
||||
import { useParams } from 'common'
|
||||
import { keyBy } from 'lodash'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { SidePanel } from 'ui'
|
||||
|
||||
import { ENV_VAR_RAW_KEYS } from '@/components/interfaces/Integrations/Vercel/Integrations-Vercel.constants'
|
||||
import ProjectLinker, {
|
||||
import {
|
||||
ForeignProject,
|
||||
ProjectLinker,
|
||||
} from '@/components/interfaces/Integrations/VercelGithub/ProjectLinker'
|
||||
import { Markdown } from '@/components/interfaces/Markdown'
|
||||
import { vercelIcon } from '@/components/to-be-cleaned/ListIcons'
|
||||
@@ -14,6 +14,7 @@ import { useOrgIntegrationsQuery } from '@/data/integrations/integrations-query-
|
||||
import { useIntegrationVercelConnectionsCreateMutation } from '@/data/integrations/integrations-vercel-connections-create-mutation'
|
||||
import { useVercelProjectsQuery } from '@/data/integrations/integrations-vercel-projects-query'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { BASE_PATH } from '@/lib/constants'
|
||||
import { EMPTY_ARR } from '@/lib/void'
|
||||
import { useSidePanelsStateSnapshot } from '@/state/side-panels'
|
||||
@@ -25,7 +26,7 @@ const VERCEL_ICON = (
|
||||
)
|
||||
|
||||
export const SidePanelVercelProjectLinker = () => {
|
||||
const { ref } = useParams()
|
||||
const { data: selectedProject } = useSelectedProjectQuery()
|
||||
const { data: selectedOrganization } = useSelectedOrganizationQuery()
|
||||
const sidePanelStateSnapshot = useSidePanelsStateSnapshot()
|
||||
const organizationIntegrationId = sidePanelStateSnapshot.vercelConnectionsIntegrationId
|
||||
@@ -126,7 +127,7 @@ Check the details below before proceeding
|
||||
<SidePanel.Content className="flex flex-col gap-2">
|
||||
<ProjectLinker
|
||||
slug={selectedOrganization?.slug}
|
||||
defaultSupabaseProjectRef={ref}
|
||||
defaultSupabaseProject={selectedProject}
|
||||
organizationIntegrationId={selectedIntegration?.id}
|
||||
foreignProjects={vercelProjects}
|
||||
onCreateConnections={onCreateConnections}
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ export const GitHubSection = ({ isProjectScoped }: { isProjectScoped: boolean })
|
||||
<GitHubIntegrationConnectionForm connection={existingConnection} />
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<ul className="flex flex-col gap-y-2">
|
||||
{connections?.map((connection) => (
|
||||
<IntegrationConnectionItem
|
||||
|
||||
+35
-42
@@ -163,49 +163,42 @@ export const VercelSection = ({ isProjectScoped }: { isProjectScoped: boolean })
|
||||
<div>
|
||||
{vercelIntegration ? (
|
||||
<div key={vercelIntegration.id}>
|
||||
<IntegrationInstallation title={'Vercel'} integration={vercelIntegration} />
|
||||
{connections.length > 0 ? (
|
||||
<>
|
||||
<IntegrationConnectionHeader
|
||||
title={ConnectionHeaderTitle}
|
||||
markdown={`Repository connections for Vercel`}
|
||||
/>
|
||||
<ul className="flex flex-col">
|
||||
{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>
|
||||
<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>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
<IntegrationConnectionHeader
|
||||
title={ConnectionHeaderTitle}
|
||||
className="pb-0"
|
||||
markdown={`Repository connections for Vercel`}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<EmptyIntegrationConnection
|
||||
disabled={isBranch || !canCreateVercelConnection}
|
||||
|
||||
@@ -167,11 +167,13 @@ export const InterstitialAccountRow = ({
|
||||
displayName,
|
||||
action,
|
||||
className,
|
||||
detail,
|
||||
}: {
|
||||
avatarUrl?: string
|
||||
displayName?: string
|
||||
action?: ReactNode
|
||||
className?: string
|
||||
detail?: string
|
||||
}) => (
|
||||
<Card className={cn('shadow-none', !action && 'border-muted bg-surface-200/50', className)}>
|
||||
<CardContent
|
||||
@@ -190,6 +192,7 @@ export const InterstitialAccountRow = ({
|
||||
<p className="truncate text-sm text-foreground">
|
||||
{displayName || <span className="invisible">Loading account</span>}
|
||||
</p>
|
||||
{detail && <p className="mt-1 truncate text-xs text-foreground-light">{detail}</p>}
|
||||
</div>
|
||||
{action}
|
||||
</CardContent>
|
||||
|
||||
@@ -1,58 +1,82 @@
|
||||
import { useParams } from 'common'
|
||||
import { keyBy } from 'lodash'
|
||||
import Head from 'next/head'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { Card, CardContent } from 'ui'
|
||||
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import { ENV_VAR_RAW_KEYS } from '@/components/interfaces/Integrations/Vercel/Integrations-Vercel.constants'
|
||||
import { isVercelUrl } from '@/components/interfaces/Integrations/Vercel/VercelIntegration.utils'
|
||||
import ProjectLinker, {
|
||||
ForeignProject,
|
||||
import {
|
||||
findVercelIntegrationByConfigurationId,
|
||||
isVercelUrl,
|
||||
} from '@/components/interfaces/Integrations/Vercel/VercelIntegration.utils'
|
||||
import {
|
||||
VERCEL_INTEGRATION_ICON,
|
||||
VercelEnvVarsSyncDescription,
|
||||
VercelIntegrationFooter,
|
||||
VercelIntegrationInterstitialErrorState,
|
||||
VercelIntegrationLogo,
|
||||
} from '@/components/interfaces/Integrations/Vercel/VercelIntegrationInterstitial'
|
||||
import {
|
||||
ProjectLinker,
|
||||
type ForeignProject,
|
||||
} from '@/components/interfaces/Integrations/VercelGithub/ProjectLinker'
|
||||
import { Markdown } from '@/components/interfaces/Markdown'
|
||||
import VercelIntegrationWindowLayout from '@/components/layouts/IntegrationsLayout/VercelIntegrationWindowLayout'
|
||||
import { ScaffoldColumn, ScaffoldContainer } from '@/components/layouts/Scaffold'
|
||||
import { InterstitialAccountRow, InterstitialLayout } from '@/components/layouts/InterstitialLayout'
|
||||
import { vercelIcon } from '@/components/to-be-cleaned/ListIcons'
|
||||
import { useOrgIntegrationsQuery } from '@/data/integrations/integrations-query-org-only'
|
||||
import { useIntegrationVercelConnectionsCreateMutation } from '@/data/integrations/integrations-vercel-connections-create-mutation'
|
||||
import { useVercelProjectsQuery } from '@/data/integrations/integrations-vercel-projects-query'
|
||||
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
|
||||
import { withAuth } from '@/hooks/misc/withAuth'
|
||||
import { BASE_PATH } from '@/lib/constants'
|
||||
import { getErrorMessage } from '@/lib/get-error-message'
|
||||
import { buildStudioPageTitle } from '@/lib/page-title'
|
||||
import { useProfileNameAndPicture } from '@/lib/profile'
|
||||
import { EMPTY_ARR } from '@/lib/void'
|
||||
import { useIntegrationInstallationSnapshot } from '@/state/integration-installation'
|
||||
import type { NextPageWithLayout, Organization } from '@/types'
|
||||
|
||||
const VERCEL_ICON = (
|
||||
<img src={`${BASE_PATH}/img/icons/vercel-icon.svg`} alt="Vercel Icon" className="w-4" />
|
||||
)
|
||||
const PAGE_TITLE = buildStudioPageTitle({
|
||||
section: 'Connect Vercel Project',
|
||||
brand: 'Supabase',
|
||||
})
|
||||
|
||||
const VercelIntegration: NextPageWithLayout = () => {
|
||||
const VercelChooseProjectPage: NextPageWithLayout = () => {
|
||||
const { slug, configurationId, next } = useParams()
|
||||
const { username, primaryEmail, avatarUrl } = useProfileNameAndPicture()
|
||||
const displayName = primaryEmail ?? username ?? ''
|
||||
|
||||
/**
|
||||
* Fetch the list of organization based integration installations for Vercel.
|
||||
*
|
||||
* Array of integrations installed on all
|
||||
*/
|
||||
const { data: integrationData } = useOrgIntegrationsQuery({ orgSlug: slug })
|
||||
const {
|
||||
data: integrationData,
|
||||
isPending: isLoadingIntegrationsQuery,
|
||||
isError: isIntegrationsError,
|
||||
error: integrationsError,
|
||||
} = useOrgIntegrationsQuery({ orgSlug: slug })
|
||||
|
||||
const { data } = useOrganizationsQuery({ enabled: slug !== undefined })
|
||||
const {
|
||||
data: organizationsData,
|
||||
isPending: isLoadingOrganizationsQuery,
|
||||
isError: isOrganizationsError,
|
||||
error: organizationsError,
|
||||
} = useOrganizationsQuery({ enabled: slug !== undefined })
|
||||
|
||||
const organization = data?.find((organization: Organization) => organization.slug === slug)
|
||||
|
||||
const integration = integrationData?.find(
|
||||
(x) =>
|
||||
x.metadata !== undefined &&
|
||||
'configuration_id' in x.metadata &&
|
||||
x.metadata?.configuration_id === configurationId
|
||||
const organization = organizationsData?.find(
|
||||
(organization: Organization) => organization.slug === slug
|
||||
)
|
||||
|
||||
const { data: vercelProjectsData, isPending: isLoadingVercelProjectsData } =
|
||||
useVercelProjectsQuery(
|
||||
{
|
||||
organization_integration_id: integration?.id,
|
||||
},
|
||||
{ enabled: integration?.id !== undefined }
|
||||
)
|
||||
const integration = findVercelIntegrationByConfigurationId(integrationData, configurationId)
|
||||
|
||||
const {
|
||||
data: vercelProjectsData,
|
||||
isPending: isLoadingVercelProjectsData,
|
||||
isError: isVercelProjectsError,
|
||||
error: vercelProjectsError,
|
||||
} = useVercelProjectsQuery(
|
||||
{
|
||||
organization_integration_id: integration?.id,
|
||||
},
|
||||
{ enabled: integration?.id !== undefined }
|
||||
)
|
||||
|
||||
const vercelProjects = useMemo(() => vercelProjectsData ?? EMPTY_ARR, [vercelProjectsData])
|
||||
const vercelProjectsById = useMemo(() => keyBy(vercelProjects, 'id'), [vercelProjects])
|
||||
@@ -68,7 +92,7 @@ const VercelIntegration: NextPageWithLayout = () => {
|
||||
src={`${BASE_PATH}/img/icons/frameworks/${project.framework}.svg`}
|
||||
width={21}
|
||||
height={21}
|
||||
alt={`icon`}
|
||||
alt="Framework icon"
|
||||
/>
|
||||
)
|
||||
},
|
||||
@@ -95,7 +119,7 @@ const VercelIntegration: NextPageWithLayout = () => {
|
||||
})
|
||||
|
||||
const onCreateConnections = useCallback(
|
||||
(vars: any) => {
|
||||
(vars: Parameters<typeof createConnections>[0]) => {
|
||||
createConnections({
|
||||
...vars,
|
||||
connection: {
|
||||
@@ -114,54 +138,106 @@ const VercelIntegration: NextPageWithLayout = () => {
|
||||
[createConnections]
|
||||
)
|
||||
|
||||
const showLoadingState =
|
||||
isLoadingOrganizationsQuery ||
|
||||
isLoadingIntegrationsQuery ||
|
||||
(integration?.id !== undefined && isLoadingVercelProjectsData)
|
||||
|
||||
const isError = isOrganizationsError || isIntegrationsError || isVercelProjectsError
|
||||
const errorMessage =
|
||||
getErrorMessage(organizationsError) ??
|
||||
getErrorMessage(integrationsError) ??
|
||||
getErrorMessage(vercelProjectsError)
|
||||
const integrationNotFound =
|
||||
!isLoadingIntegrationsQuery && integrationData !== undefined && integration === undefined
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScaffoldContainer className="flex flex-col gap-6 grow py-8">
|
||||
<ScaffoldColumn className="max-w-[900px]! mx-auto w-full">
|
||||
<header>
|
||||
<h2>Create your first Project Connection</h2>
|
||||
<Markdown
|
||||
className="text-foreground-lighter"
|
||||
content={`
|
||||
This Supabase integration manages your environment variables automatically to provide the latest keys in the unlikely event that you will need to refresh your JWT token.
|
||||
`}
|
||||
/>
|
||||
</header>
|
||||
<ProjectLinker
|
||||
slug={organization?.slug}
|
||||
organizationIntegrationId={integration?.id}
|
||||
foreignProjects={vercelProjects}
|
||||
onCreateConnections={onCreateConnections}
|
||||
installedConnections={integration?.connections}
|
||||
isLoading={isCreatingConnection}
|
||||
integrationIcon={VERCEL_ICON}
|
||||
getForeignProjectIcon={getForeignProjectIcon}
|
||||
choosePrompt="Choose Vercel Project"
|
||||
onSkip={() => {
|
||||
if (next && isVercelUrl(next)) {
|
||||
window.location.href = next
|
||||
}
|
||||
}}
|
||||
loadingForeignProjects={isLoadingVercelProjectsData}
|
||||
mode="Vercel"
|
||||
/>
|
||||
<Markdown
|
||||
content={`
|
||||
The following environment variables will be added:
|
||||
<Head>
|
||||
<title>{PAGE_TITLE}</title>
|
||||
</Head>
|
||||
|
||||
${ENV_VAR_RAW_KEYS.map((x) => {
|
||||
return `\n - \`${x}\``
|
||||
})}
|
||||
`}
|
||||
/>
|
||||
</ScaffoldColumn>
|
||||
</ScaffoldContainer>
|
||||
<InterstitialLayout
|
||||
logo={<VercelIntegrationLogo />}
|
||||
title="Connect Vercel project"
|
||||
description={<VercelEnvVarsSyncDescription />}
|
||||
footer={<VercelIntegrationFooter />}
|
||||
>
|
||||
<div className="px-6 pb-6">
|
||||
{showLoadingState ? (
|
||||
<ChooseProjectLoadingState />
|
||||
) : isError ? (
|
||||
<VercelIntegrationInterstitialErrorState
|
||||
title="Unable to load project connection"
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
) : organization === undefined ? (
|
||||
<VercelIntegrationInterstitialErrorState
|
||||
title="Unable to load project connection"
|
||||
errorMessage="Organization not found. Retry the installation from Vercel."
|
||||
/>
|
||||
) : integrationNotFound ? (
|
||||
<VercelIntegrationInterstitialErrorState
|
||||
title="Unable to load project connection"
|
||||
errorMessage="Vercel integration not found for this organization. Retry the installation from Vercel."
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col gap-5">
|
||||
<InterstitialAccountRow
|
||||
avatarUrl={avatarUrl}
|
||||
displayName={displayName}
|
||||
detail={organization.name}
|
||||
/>
|
||||
|
||||
<ProjectLinker
|
||||
variant="interstitial"
|
||||
slug={organization.slug}
|
||||
organizationIntegrationId={integration?.id}
|
||||
foreignProjects={vercelProjects}
|
||||
onCreateConnections={onCreateConnections}
|
||||
installedConnections={integration?.connections}
|
||||
isLoading={isCreatingConnection}
|
||||
integrationIcon={VERCEL_INTEGRATION_ICON}
|
||||
getForeignProjectIcon={getForeignProjectIcon}
|
||||
choosePrompt="Choose Vercel project"
|
||||
onSkip={() => {
|
||||
if (next && isVercelUrl(next)) {
|
||||
window.location.href = next
|
||||
}
|
||||
}}
|
||||
loadingForeignProjects={isLoadingVercelProjectsData}
|
||||
mode="Vercel"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</InterstitialLayout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
VercelIntegration.getLayout = (page) => (
|
||||
<VercelIntegrationWindowLayout>{page}</VercelIntegrationWindowLayout>
|
||||
const ChooseProjectLoadingState = () => (
|
||||
<div className="flex flex-col gap-5">
|
||||
<Card className="shadow-none">
|
||||
<CardContent className="flex items-center gap-3 border-none px-4 py-3">
|
||||
<ShimmeringLoader className="size-8 flex-shrink-0 rounded-full py-0" />
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<ShimmeringLoader className="h-3 w-20 py-0" />
|
||||
<ShimmeringLoader className="h-4 w-40 max-w-full py-0" />
|
||||
<ShimmeringLoader className="h-3 w-32 py-0" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<section className="space-y-2" aria-label="Project loading">
|
||||
<ShimmeringLoader className="h-3 w-24 py-0" />
|
||||
<ShimmeringLoader className="h-[34px] w-full rounded-md py-0" />
|
||||
</section>
|
||||
<section className="space-y-2" aria-label="Vercel project loading">
|
||||
<ShimmeringLoader className="h-3 w-24 py-0" />
|
||||
<ShimmeringLoader className="h-[34px] w-full rounded-md py-0" />
|
||||
</section>
|
||||
<ShimmeringLoader className="h-10 w-full rounded-md py-0" />
|
||||
</div>
|
||||
)
|
||||
|
||||
export default VercelIntegration
|
||||
export default withAuth(VercelChooseProjectPage)
|
||||
@@ -18,19 +18,17 @@ import {
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import { getHasInstalledObject } from '@/components/layouts/IntegrationsLayout/Integrations.utils'
|
||||
import {
|
||||
InterstitialAccountRow,
|
||||
InterstitialLayout,
|
||||
LogoPair,
|
||||
PartnerLogo,
|
||||
SupabaseLogo,
|
||||
} from '@/components/layouts/InterstitialLayout'
|
||||
VercelIntegrationFooter,
|
||||
VercelIntegrationInterstitialErrorState,
|
||||
VercelIntegrationLogo,
|
||||
} from '@/components/interfaces/Integrations/Vercel/VercelIntegrationInterstitial'
|
||||
import { getHasInstalledObject } from '@/components/layouts/IntegrationsLayout/Integrations.utils'
|
||||
import { InterstitialAccountRow, InterstitialLayout } from '@/components/layouts/InterstitialLayout'
|
||||
import { useIntegrationsQuery } from '@/data/integrations/integrations-query'
|
||||
import { useVercelIntegrationCreateMutation } from '@/data/integrations/vercel-integration-create-mutation'
|
||||
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
|
||||
import { withAuth } from '@/hooks/misc/withAuth'
|
||||
import { BASE_PATH } from '@/lib/constants'
|
||||
import {
|
||||
buildVercelInstallRouteQuery,
|
||||
getErrorMessage,
|
||||
@@ -38,6 +36,7 @@ import {
|
||||
} from '@/lib/integrations/vercel-install.utils'
|
||||
import { buildStudioPageTitle } from '@/lib/page-title'
|
||||
import { useProfileNameAndPicture } from '@/lib/profile'
|
||||
import { useTrack } from '@/lib/telemetry/track'
|
||||
import { useIntegrationInstallationSnapshot } from '@/state/integration-installation'
|
||||
import type { NextPageWithLayout, Organization } from '@/types'
|
||||
|
||||
@@ -61,6 +60,7 @@ const VercelIntegration: NextPageWithLayout = () => {
|
||||
const { code, configurationId, currentProjectId, externalId, next, teamId, source } = useParams()
|
||||
const [selectedOrg, setSelectedOrg] = useState<Organization | null>(null)
|
||||
const { username, primaryEmail, avatarUrl } = useProfileNameAndPicture()
|
||||
const track = useTrack()
|
||||
|
||||
const snapshot = useIntegrationInstallationSnapshot()
|
||||
const displayName = primaryEmail ?? username ?? ''
|
||||
@@ -197,6 +197,11 @@ const VercelIntegration: NextPageWithLayout = () => {
|
||||
* Only install if integration hasn't already been installed
|
||||
*/
|
||||
if (!isIntegrationInstalled) {
|
||||
track(
|
||||
'integration_install_submitted',
|
||||
{ integrationName: 'Vercel', method: source },
|
||||
{ organization: orgSlug }
|
||||
)
|
||||
mutate({
|
||||
code,
|
||||
configurationId,
|
||||
@@ -243,7 +248,6 @@ const VercelIntegration: NextPageWithLayout = () => {
|
||||
!selectedOrg ||
|
||||
missingParams.length > 0 ||
|
||||
isError
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -251,27 +255,19 @@ const VercelIntegration: NextPageWithLayout = () => {
|
||||
</Head>
|
||||
|
||||
<InterstitialLayout
|
||||
logo={
|
||||
<LogoPair
|
||||
left={
|
||||
<PartnerLogo
|
||||
src={`${BASE_PATH}/img/icons/vercel-icon.svg`}
|
||||
alt="Vercel"
|
||||
className="bg-surface-75"
|
||||
imageClassName="size-7 object-contain dark:invert"
|
||||
/>
|
||||
}
|
||||
right={<SupabaseLogo />}
|
||||
/>
|
||||
}
|
||||
title="Install Vercel integration"
|
||||
description="Choose an organization to connect to Vercel"
|
||||
logo={<VercelIntegrationLogo />}
|
||||
title="Install Vercel Integration"
|
||||
description="Choose the Supabase organization Vercel can connect to"
|
||||
footer={<VercelIntegrationFooter />}
|
||||
>
|
||||
<div className="px-6 pb-6">
|
||||
{showLoadingState ? (
|
||||
<InstallationLoadingState />
|
||||
) : isError ? (
|
||||
<InstallationErrorState errorMessage={errorMessage} />
|
||||
<VercelIntegrationInterstitialErrorState
|
||||
title="Unable to load installation"
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col gap-5">
|
||||
<InterstitialAccountRow avatarUrl={avatarUrl} displayName={displayName} />
|
||||
@@ -357,26 +353,6 @@ const InstallationLoadingState = () => (
|
||||
</div>
|
||||
)
|
||||
|
||||
const InstallationErrorState = ({ errorMessage }: { errorMessage?: string }) => (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Admonition
|
||||
type="warning"
|
||||
title="Unable to load installation"
|
||||
description={
|
||||
<>
|
||||
Retry the installation request from Vercel.
|
||||
{errorMessage && (
|
||||
<span className="mt-1 block text-foreground-lighter">Error: {errorMessage}</span>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Button variant="default" block asChild>
|
||||
<Link href="/">Back to dashboard</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
interface OrganizationSelectProps {
|
||||
organizations: Organization[]
|
||||
selectedOrg: Organization | null
|
||||
|
||||
Reference in new issue
Block a user