mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 19:05:06 +03:00
368 lines
13 KiB
TypeScript
368 lines
13 KiB
TypeScript
import { ENV_VAR_RAW_KEYS } from 'components/interfaces/Integrations/Integrations-Vercel.constants'
|
|
import { Markdown } from 'components/interfaces/Markdown'
|
|
import { vercelIcon } from 'components/to-be-cleaned/ListIcons'
|
|
import { useIntegrationConnectionsCreateMutation } from 'data/integrations/integration-connections-create-mutation'
|
|
import { useIntegrationsVercelConnectionSyncEnvsMutation } from 'data/integrations/integrations-vercel-connection-sync-envs-mutation'
|
|
import { VercelProjectsResponse } from 'data/integrations/integrations-vercel-projects-query'
|
|
import { IntegrationProjectConnection } from 'data/integrations/integrations.types'
|
|
import { useSelectedOrganization } from 'hooks'
|
|
import { BASE_PATH } from 'lib/constants'
|
|
import { useRef, useState } from 'react'
|
|
import { toast } from 'react-hot-toast'
|
|
import {
|
|
Button,
|
|
CommandEmpty_Shadcn_,
|
|
CommandGroup_Shadcn_,
|
|
CommandInput_Shadcn_,
|
|
CommandItem_Shadcn_,
|
|
CommandList_Shadcn_,
|
|
Command_Shadcn_,
|
|
IconChevronDown,
|
|
PopoverContent_Shadcn_,
|
|
PopoverTrigger_Shadcn_,
|
|
Popover_Shadcn_,
|
|
cn,
|
|
} from 'ui'
|
|
|
|
interface Project {
|
|
id: string
|
|
name: string
|
|
ref: string
|
|
}
|
|
|
|
export interface ProjectLinkerProps {
|
|
organizationIntegrationId: string | undefined
|
|
foreignProjects: VercelProjectsResponse[]
|
|
supabaseProjects: Project[]
|
|
onCreateConnections?: () => void
|
|
installedConnections: IntegrationProjectConnection[] | undefined
|
|
setLoading?: (x: boolean) => void
|
|
showSkip?: boolean
|
|
loadingForeignProjects?: boolean
|
|
loadingSupabaseProjects?: boolean
|
|
}
|
|
|
|
const ProjectLinker = ({
|
|
organizationIntegrationId,
|
|
foreignProjects,
|
|
supabaseProjects,
|
|
onCreateConnections: _onCreateConnections,
|
|
installedConnections = [],
|
|
setLoading,
|
|
showSkip = false,
|
|
loadingForeignProjects,
|
|
loadingSupabaseProjects,
|
|
}: ProjectLinkerProps) => {
|
|
const [supabaseProjectsComboBoxOpen, setSupabaseProjectsComboboxOpen] = useState(false)
|
|
const [vercelProjectsComboBoxOpen, setVercelProjectsComboboxOpen] = useState(false)
|
|
const supabaseProjectsComboBoxRef = useRef<HTMLButtonElement>(null)
|
|
const vercelProjectsComboBoxRef = useRef<HTMLButtonElement>(null)
|
|
|
|
const selectedOrganization = useSelectedOrganization()
|
|
|
|
const [supabaseProjectRef, setSupabaseProjectRef] = useState<string | undefined>(undefined)
|
|
const [vercelProjectId, setVercelProjectId] = useState<string | undefined>(undefined)
|
|
|
|
const { mutateAsync: syncEnvs } = useIntegrationsVercelConnectionSyncEnvsMutation()
|
|
const { mutate: createConnections, isLoading } = useIntegrationConnectionsCreateMutation({
|
|
async onSuccess({ id }) {
|
|
try {
|
|
await syncEnvs({ connectionId: id })
|
|
} catch (error: any) {
|
|
toast.error('Failed to sync environment variables: ', error.message)
|
|
}
|
|
|
|
if (setLoading) setLoading(false)
|
|
_onCreateConnections?.()
|
|
},
|
|
onError() {
|
|
if (setLoading) setLoading(false)
|
|
},
|
|
})
|
|
|
|
// create a flat array of foreign project ids. ie, ["prj_MlkO6AiLG5ofS9ojKrkS3PhhlY3f", ..]
|
|
const flatInstalledConnectionsIds = new Set(installedConnections.map((x) => x.foreign_project_id))
|
|
|
|
// check that vercel project is not already installed
|
|
const filteredForeignProjects: VercelProjectsResponse[] = foreignProjects.filter(
|
|
(foreignProject) => {
|
|
return !flatInstalledConnectionsIds.has(foreignProject.id)
|
|
}
|
|
)
|
|
|
|
const selectedSupabaseProject = supabaseProjectRef
|
|
? supabaseProjects.find((x) => x.ref?.toLowerCase() === supabaseProjectRef?.toLowerCase())
|
|
: undefined
|
|
|
|
const selectedVercelProject = vercelProjectId
|
|
? filteredForeignProjects.find((x) => x.id?.toLowerCase() === vercelProjectId?.toLowerCase())
|
|
: undefined
|
|
|
|
function onCreateConnections() {
|
|
const projectDetails = selectedVercelProject
|
|
|
|
if (!organizationIntegrationId) return console.error('No integration ID set')
|
|
if (!selectedVercelProject?.id) return console.error('No Vercel project ID set')
|
|
if (!selectedSupabaseProject?.ref) return console.error('No Supabase project ref set')
|
|
|
|
if (setLoading) setLoading(true)
|
|
|
|
createConnections({
|
|
organizationIntegrationId,
|
|
connection: {
|
|
foreign_project_id: selectedVercelProject?.id,
|
|
supabase_project_ref: selectedSupabaseProject?.ref,
|
|
metadata: {
|
|
...projectDetails,
|
|
supabaseConfig: {
|
|
projectEnvVars: {
|
|
write: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
orgSlug: selectedOrganization?.slug,
|
|
})
|
|
}
|
|
|
|
const Panel = ({ children, className, ...props }: React.HTMLAttributes<HTMLDivElement>) => {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'flex flex-col grow gap-6 px-5 mx-auto w-full justify-center items-center',
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<div className="relative border rounded-lg p-12 bg shadow">
|
|
<div
|
|
className="absolute inset-0 bg-grid-black/5 [mask-image:linear-gradient(0deg,#fff,rgba(255,255,255,0.6))] dark:bg-grid-white/5 dark:[mask-image:linear-gradient(0deg,rgba(255,255,255,0.1),rgba(255,255,255,0.5))]"
|
|
style={{ backgroundPosition: '10px 10px' }}
|
|
></div>
|
|
<div className="flex gap-0 w-full relative">
|
|
<Panel>
|
|
<div className="bg-white shadow border rounded p-1 w-12 h-12 flex justify-center items-center">
|
|
<img src={`${BASE_PATH}/img/supabase-logo.svg`} alt="Supabase" className="w-6" />
|
|
</div>
|
|
|
|
<Popover_Shadcn_
|
|
open={supabaseProjectsComboBoxOpen}
|
|
onOpenChange={setSupabaseProjectsComboboxOpen}
|
|
>
|
|
<PopoverTrigger_Shadcn_ asChild>
|
|
<Button
|
|
ref={supabaseProjectsComboBoxRef}
|
|
type="default"
|
|
size="medium"
|
|
block
|
|
disabled={loadingSupabaseProjects}
|
|
loading={loadingSupabaseProjects}
|
|
className="justify-start"
|
|
icon={
|
|
<div className="bg-white shadow border rounded 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>
|
|
}
|
|
iconRight={
|
|
<span className="grow flex justify-end">
|
|
<IconChevronDown className={''} />
|
|
</span>
|
|
}
|
|
>
|
|
{selectedSupabaseProject ? selectedSupabaseProject.name : 'Choose Project'}
|
|
</Button>
|
|
</PopoverTrigger_Shadcn_>
|
|
<PopoverContent_Shadcn_
|
|
className="p-0 w-full"
|
|
side="bottom"
|
|
align="center"
|
|
style={{ width: supabaseProjectsComboBoxRef.current?.offsetWidth }}
|
|
>
|
|
<Command_Shadcn_>
|
|
<CommandInput_Shadcn_ placeholder="Search organization..." />
|
|
<CommandList_Shadcn_ className="!max-h-[170px]">
|
|
<CommandEmpty_Shadcn_>No results found.</CommandEmpty_Shadcn_>
|
|
<CommandGroup_Shadcn_>
|
|
{supabaseProjects.map((project) => {
|
|
return (
|
|
<CommandItem_Shadcn_
|
|
value={project.ref}
|
|
key={project.ref}
|
|
className="flex gap-2 items-center"
|
|
onSelect={(ref) => {
|
|
if (ref) setSupabaseProjectRef(ref)
|
|
setSupabaseProjectsComboboxOpen(false)
|
|
}}
|
|
>
|
|
<div className="bg-white shadow border rounded 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>{project.name}</span>
|
|
</CommandItem_Shadcn_>
|
|
)
|
|
})}
|
|
</CommandGroup_Shadcn_>
|
|
</CommandList_Shadcn_>
|
|
</Command_Shadcn_>
|
|
</PopoverContent_Shadcn_>
|
|
</Popover_Shadcn_>
|
|
</Panel>
|
|
<div className="border border-scale-1000 h-px w-16 border-dashed self-end mb-5"></div>
|
|
<Panel>
|
|
<div className="bg-black shadow rounded p-1 w-12 h-12 flex justify-center items-center">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="white"
|
|
viewBox="0 0 512 512"
|
|
className="w-6"
|
|
>
|
|
<path fillRule="evenodd" d="M256,48,496,464H16Z" />
|
|
</svg>
|
|
</div>
|
|
|
|
<Popover_Shadcn_
|
|
open={vercelProjectsComboBoxOpen}
|
|
onOpenChange={setVercelProjectsComboboxOpen}
|
|
>
|
|
<PopoverTrigger_Shadcn_ asChild>
|
|
<Button
|
|
ref={vercelProjectsComboBoxRef}
|
|
type="default"
|
|
size="medium"
|
|
block
|
|
disabled={loadingForeignProjects}
|
|
loading={loadingForeignProjects}
|
|
className="justify-start"
|
|
icon={
|
|
selectedVercelProject ? (
|
|
selectedVercelProject?.framework ? (
|
|
vercelIcon
|
|
) : (
|
|
<img
|
|
src={`${BASE_PATH}/img/icons/frameworks/${selectedVercelProject.framework}.svg`}
|
|
width={21}
|
|
height={21}
|
|
alt={`icon`}
|
|
/>
|
|
)
|
|
) : (
|
|
<></>
|
|
)
|
|
}
|
|
iconRight={
|
|
<span className="grow flex justify-end">
|
|
<IconChevronDown className={''} />
|
|
</span>
|
|
}
|
|
>
|
|
{(selectedVercelProject && selectedVercelProject.name) ??
|
|
'Choose a Vercel Project'}
|
|
</Button>
|
|
</PopoverTrigger_Shadcn_>
|
|
<PopoverContent_Shadcn_
|
|
className="p-0 w-full"
|
|
side="bottom"
|
|
align="center"
|
|
style={{ width: vercelProjectsComboBoxRef.current?.offsetWidth }}
|
|
>
|
|
<Command_Shadcn_>
|
|
<CommandInput_Shadcn_ placeholder="Search organization..." />
|
|
<CommandList_Shadcn_ className="!max-h-[170px]">
|
|
<CommandEmpty_Shadcn_>No results found.</CommandEmpty_Shadcn_>
|
|
<CommandGroup_Shadcn_>
|
|
{filteredForeignProjects.map((project) => {
|
|
return (
|
|
<CommandItem_Shadcn_
|
|
value={project.id}
|
|
key={project.id}
|
|
className="flex gap-2 items-center"
|
|
onSelect={(id) => {
|
|
if (id) setVercelProjectId(id)
|
|
setVercelProjectsComboboxOpen(false)
|
|
}}
|
|
>
|
|
{!project?.framework ? (
|
|
vercelIcon
|
|
) : (
|
|
<img
|
|
src={`${BASE_PATH}/img/icons/frameworks/${project.framework}.svg`}
|
|
width={21}
|
|
height={21}
|
|
alt={`icon`}
|
|
/>
|
|
)}
|
|
<span>{project.name}</span>
|
|
</CommandItem_Shadcn_>
|
|
)
|
|
})}
|
|
</CommandGroup_Shadcn_>
|
|
</CommandList_Shadcn_>
|
|
</Command_Shadcn_>
|
|
</PopoverContent_Shadcn_>
|
|
</Popover_Shadcn_>
|
|
</Panel>
|
|
</div>
|
|
</div>
|
|
<div className="flex w-full justify-end gap-2">
|
|
{showSkip && (
|
|
<Button
|
|
size="medium"
|
|
type="default"
|
|
onClick={() => {
|
|
_onCreateConnections?.()
|
|
}}
|
|
>
|
|
Skip
|
|
</Button>
|
|
)}
|
|
<Button
|
|
size="medium"
|
|
className="self-end"
|
|
onClick={onCreateConnections}
|
|
loading={isLoading}
|
|
disabled={
|
|
// data loading states
|
|
loadingForeignProjects ||
|
|
loadingSupabaseProjects ||
|
|
isLoading ||
|
|
// check wether both project types are not undefined
|
|
!selectedSupabaseProject ||
|
|
!selectedVercelProject
|
|
}
|
|
>
|
|
Connect project
|
|
</Button>
|
|
</div>
|
|
<Markdown
|
|
content={`
|
|
The following environment variables will be added:
|
|
|
|
${ENV_VAR_RAW_KEYS.map((x, idx) => {
|
|
return `
|
|
\n
|
|
- \`${x}\`
|
|
`
|
|
})}
|
|
`}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default ProjectLinker
|