import { Check, ChevronDown, Plus, PlusIcon } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { HTMLAttributes, ReactNode, useEffect, useState } from 'react' import { toast } from 'sonner' import { Badge, Button, cn, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, Popover, PopoverContent, PopoverTrigger, } from 'ui' import { OrganizationProjectSelector } from '@/components/ui/OrganizationProjectSelector' import ShimmerLine from '@/components/ui/ShimmerLine' import { IntegrationConnectionsCreateVariables, IntegrationProjectConnection, } from '@/data/integrations/integrations.types' import { useOrgProjectsInfiniteQuery } from '@/data/projects/org-projects-infinite-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' interface Project { name: string ref: string } export interface ForeignProject { id: string name: string installation_id?: number } const Panel = ({ children, className, ...props }: HTMLAttributes) => { return (
{children}
) } interface ProjectLinkerProps { slug?: string organizationIntegrationId?: string foreignProjects: ForeignProject[] onCreateConnections: (variables: IntegrationConnectionsCreateVariables) => void installedConnections?: IntegrationProjectConnection[] isLoading?: boolean integrationIcon: ReactNode getForeignProjectIcon?: (project: ForeignProject) => ReactNode choosePrompt?: string onSkip?: () => void loadingForeignProjects?: boolean showNoEntitiesState?: boolean defaultSupabaseProject?: Project defaultForeignProjectId?: string mode: 'Vercel' | 'GitHub' variant?: 'default' | 'interstitial' } export const ProjectLinker = ({ slug, organizationIntegrationId, foreignProjects, onCreateConnections: _onCreateConnections, installedConnections = EMPTY_ARR, isLoading, integrationIcon, getForeignProjectIcon, choosePrompt = 'Choose a project', onSkip, loadingForeignProjects, showNoEntitiesState = true, defaultSupabaseProject, defaultForeignProjectId, mode, variant = 'default', }: ProjectLinkerProps) => { const router = useRouter() const projectCreationEnabled = useIsFeatureEnabled('projects:create') const [openProjectsDropdown, setOpenProjectsDropdown] = useState(false) const [openForeignProjectsComboBox, setOpenForeignProjectsComboBox] = useState(false) const [foreignProjectId, setForeignProjectId] = useState( defaultForeignProjectId ) const [selectedSupabaseProject, setSelectedSupabaseProject] = useState() const { data: selectedOrganization } = useSelectedOrganizationQuery() const { data: orgProjects, isPending: loadingSupabaseProjects } = useOrgProjectsInfiniteQuery({ slug, }) const numProjects = orgProjects?.pages[0].pagination.count ?? 0 useEffect(() => { if (defaultSupabaseProject !== undefined && selectedSupabaseProject === undefined) setSelectedSupabaseProject(defaultSupabaseProject) }, [defaultSupabaseProject, selectedSupabaseProject]) useEffect(() => { if (defaultForeignProjectId !== undefined && foreignProjectId === undefined) setForeignProjectId(defaultForeignProjectId) }, [defaultForeignProjectId, foreignProjectId]) // create a flat array of foreign project ids. ie, ["prj_MlkO6AiLG5ofS9ojKrkS3PhhlY3f", ..] const flatInstalledConnectionsIds = new Set(installedConnections.map((x) => x.foreign_project_id)) const selectedForeignProject = foreignProjectId ? foreignProjects.find((x) => x.id?.toLowerCase() === foreignProjectId?.toLowerCase()) : undefined function onCreateConnections() { const projectDetails = selectedForeignProject if (!selectedForeignProject?.id) return console.error('No Foreign project ID set') if (!selectedSupabaseProject?.ref) return console.error('No Supabase project ref set') const alreadyInstalled = flatInstalledConnectionsIds.has(foreignProjectId ?? '') if (alreadyInstalled) { return toast.error( `Unable to connect to ${selectedForeignProject.name}: Selected repository already has an installed connection to a project` ) } _onCreateConnections({ organizationIntegrationId: organizationIntegrationId!, connection: { foreign_project_id: selectedForeignProject?.id, supabase_project_ref: selectedSupabaseProject?.ref, integration_id: '0', metadata: { ...projectDetails, }, }, orgSlug: selectedOrganization?.slug, new: { installation_id: selectedForeignProject.installation_id!, project_ref: selectedSupabaseProject.ref, repository_id: Number(selectedForeignProject.id), }, }) } 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 = ( { setSelectedSupabaseProject(project) setOpenProjectsDropdown(false) }} renderRow={(project) => { return (
{variant === 'default' && (
Supabase
)}

{project.name}

{project.status === 'INACTIVE' && Paused} {project.status === 'GOING_DOWN' && Pausing}
{project.ref === selectedSupabaseProject?.ref && }
) }} renderTrigger={() => { return ( ) }} renderActions={() => { return ( projectCreationEnabled && ( { setOpenProjectsDropdown(false) router.push(`/new/${selectedOrganization?.slug}`) }} onClick={() => setOpenProjectsDropdown(false)} > { setOpenProjectsDropdown(false) }} className="w-full flex items-center gap-2" >

Create a new project

) ) }} /> ) const foreignProjectSelector = ( No results found. {foreignProjects.map((project, i) => { return ( { if (project.id) setForeignProjectId(project.id) setOpenForeignProjectsComboBox(false) }} >
{getForeignProjectIcon?.(project) ?? integrationIcon}
{project.name}
) })} {foreignProjects.length === 0 && No results found.}
{mode === 'GitHub' && ( <> openInstallGitHubIntegrationWindow('install')} > Add GitHub Repositories )}
) const actionButtons = (
{onSkip !== undefined && ( )}
) if (variant === 'interstitial') { return (
{loadingForeignProjects || loadingSupabaseProjects ? (

Loading projects

) : showNoEntitiesState && (noSupabaseProjects || noForeignProjects) ? (
No {missingEntity} projects found. Create a {missingEntity} project to link to a{' '} {oppositeMissingEntity} project, or skip and connect later.
) : ( <>

Supabase project

{supabaseProjectSelector}

Vercel project

{foreignProjectSelector}
)} {actionButtons}
) } return (
{loadingForeignProjects ? (

Loading projects

) : showNoEntitiesState && (noSupabaseProjects || noForeignProjects) ? (
No {missingEntity} Projects found

You will need to create a {missingEntity} Project to link to a {oppositeMissingEntity}{' '} Project.
You can skip this and create a Project Connection later.

) : (
Supabase
{supabaseProjectSelector}
{integrationIcon}
{foreignProjectSelector}
)}
{actionButtons}
) }