mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 06:09:08 +03:00
Merge pull request #16398 from supabase/chore/vercel-integration-empty-states
chore: fix empty states for Vercel Integration
This commit is contained in:
10 files changed
+367
-283
No files matched your search
@@ -6,6 +6,9 @@ import { useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { Organization } from 'types'
|
||||
import {
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Alert_Shadcn_,
|
||||
Badge,
|
||||
Button,
|
||||
CommandEmpty_Shadcn_,
|
||||
@@ -14,11 +17,13 @@ import {
|
||||
CommandItem_Shadcn_,
|
||||
CommandList_Shadcn_,
|
||||
Command_Shadcn_,
|
||||
IconAlertCircle,
|
||||
IconChevronDown,
|
||||
IconHexagon,
|
||||
PopoverContent_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
cn,
|
||||
} from 'ui'
|
||||
|
||||
export interface OrganizationPickerProps {
|
||||
@@ -26,6 +31,7 @@ export interface OrganizationPickerProps {
|
||||
configurationId?: string
|
||||
selectedOrg: Organization | null
|
||||
onSelectedOrgChange: (organization: Organization) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const OrganizationPicker = ({
|
||||
@@ -33,6 +39,7 @@ const OrganizationPicker = ({
|
||||
configurationId,
|
||||
selectedOrg,
|
||||
onSelectedOrgChange,
|
||||
disabled,
|
||||
}: OrganizationPickerProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const ref = useRef<HTMLButtonElement>(null)
|
||||
@@ -63,21 +70,22 @@ const OrganizationPicker = ({
|
||||
size="medium"
|
||||
block
|
||||
className="justify-start"
|
||||
icon={<IconHexagon />}
|
||||
loading={isLoadingOrganization}
|
||||
disabled={isLoadingOrganization}
|
||||
disabled={disabled}
|
||||
iconRight={
|
||||
<span className="grow flex justify-end">
|
||||
<IconChevronDown className={''} />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<span className="flex gap-2">
|
||||
<span className="truncate">{selectedOrg?.name}</span>
|
||||
<div className="flex gap-2">
|
||||
<span className={cn('truncate', !selectedOrg && 'text-light')}>
|
||||
{selectedOrg?.name ? selectedOrg?.name : 'Choose an organization'}
|
||||
</span>
|
||||
{selectedOrg && configurationId && installed[selectedOrg.slug] && (
|
||||
<Badge color="scale">Integration Installed</Badge>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_
|
||||
@@ -108,7 +116,6 @@ const OrganizationPicker = ({
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<IconHexagon />
|
||||
<span className="truncate">{org.name}</span>{' '}
|
||||
{configurationId && installed[org.slug] && (
|
||||
<Badge color="scale" className="!flex-none">
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
Popover_Shadcn_,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useVercelProjectsQuery } from 'data/integrations/integrations-vercel-projects-query'
|
||||
|
||||
export interface Project {
|
||||
id: string
|
||||
@@ -132,6 +134,11 @@ const ProjectLinker = ({
|
||||
)
|
||||
}
|
||||
|
||||
const noSupabaseProjects = supabaseProjects.length === 0
|
||||
const noForeignProjects = foreignProjects.length === 0
|
||||
const missingEntity = noSupabaseProjects ? 'Supabase' : 'Vercel'
|
||||
const oppositeMissingEntity = noSupabaseProjects ? 'Vercel' : 'Supabase'
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="relative border rounded-lg p-12 bg shadow">
|
||||
@@ -139,151 +146,164 @@ const ProjectLinker = ({
|
||||
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>
|
||||
{(noSupabaseProjects || noForeignProjects) &&
|
||||
(!loadingForeignProjects || !loadingSupabaseProjects) ? (
|
||||
<div className="text-center">
|
||||
<h5 className="text">No {missingEntity} Projects found</h5>
|
||||
<p className="text-light text-sm">
|
||||
You will need to create a {missingEntity} Project to link to a {oppositeMissingEntity}{' '}
|
||||
Project.
|
||||
<br />
|
||||
You can skip this and create a Project Connection later.
|
||||
</p>
|
||||
</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={defaultSupabaseProjectRef !== undefined || 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={
|
||||
defaultSupabaseProjectRef === undefined ? (
|
||||
<Popover_Shadcn_
|
||||
open={supabaseProjectsComboBoxOpen}
|
||||
onOpenChange={setSupabaseProjectsComboboxOpen}
|
||||
>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
ref={supabaseProjectsComboBoxRef}
|
||||
type="default"
|
||||
size="medium"
|
||||
block
|
||||
disabled={defaultSupabaseProjectRef !== undefined || 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={
|
||||
defaultSupabaseProjectRef === undefined ? (
|
||||
<span className="grow flex justify-end">
|
||||
<IconChevronDown className={''} />
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{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, i) => {
|
||||
return (
|
||||
<CommandItem_Shadcn_
|
||||
value={`${project.name}-${i}`}
|
||||
key={project.ref}
|
||||
className="flex gap-2 items-center"
|
||||
onSelect={() => {
|
||||
if (project.ref) setSupabaseProjectRef(project.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">
|
||||
{integrationIcon}
|
||||
</div>
|
||||
|
||||
<Popover_Shadcn_
|
||||
open={foreignProjectsComboBoxOpen}
|
||||
onOpenChange={setForeignProjectsComboboxOpen}
|
||||
>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
ref={foreignProjectsComboBoxRef}
|
||||
type="default"
|
||||
size="medium"
|
||||
block
|
||||
disabled={loadingForeignProjects}
|
||||
loading={loadingForeignProjects}
|
||||
className="justify-start"
|
||||
icon={
|
||||
selectedForeignProject
|
||||
? getForeignProjectIcon?.(selectedForeignProject)
|
||||
: integrationIcon
|
||||
}
|
||||
iconRight={
|
||||
<span className="grow flex justify-end">
|
||||
<IconChevronDown className={''} />
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
}
|
||||
>
|
||||
{(selectedForeignProject && selectedForeignProject.name) ?? choosePrompt}
|
||||
</Button>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_
|
||||
className="p-0 w-full"
|
||||
side="bottom"
|
||||
align="center"
|
||||
style={{ width: foreignProjectsComboBoxRef.current?.offsetWidth }}
|
||||
>
|
||||
{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, i) => {
|
||||
return (
|
||||
<CommandItem_Shadcn_
|
||||
value={`${project.name}-${i}`}
|
||||
key={project.ref}
|
||||
className="flex gap-2 items-center"
|
||||
onSelect={() => {
|
||||
if (project.ref) setSupabaseProjectRef(project.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">
|
||||
{integrationIcon}
|
||||
</div>
|
||||
|
||||
<Popover_Shadcn_
|
||||
open={foreignProjectsComboBoxOpen}
|
||||
onOpenChange={setForeignProjectsComboboxOpen}
|
||||
>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
ref={foreignProjectsComboBoxRef}
|
||||
type="default"
|
||||
size="medium"
|
||||
block
|
||||
disabled={loadingForeignProjects}
|
||||
loading={loadingForeignProjects}
|
||||
className="justify-start"
|
||||
icon={
|
||||
selectedForeignProject
|
||||
? getForeignProjectIcon?.(selectedForeignProject)
|
||||
: integrationIcon
|
||||
}
|
||||
iconRight={
|
||||
<span className="grow flex justify-end">
|
||||
<IconChevronDown className={''} />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{(selectedForeignProject && selectedForeignProject.name) ?? choosePrompt}
|
||||
</Button>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_
|
||||
className="p-0 w-full"
|
||||
side="bottom"
|
||||
align="center"
|
||||
style={{ width: foreignProjectsComboBoxRef.current?.offsetWidth }}
|
||||
>
|
||||
<Command_Shadcn_>
|
||||
<CommandInput_Shadcn_ placeholder="Search organization..." />
|
||||
<CommandList_Shadcn_ className="!max-h-[170px]">
|
||||
<CommandEmpty_Shadcn_>No results found.</CommandEmpty_Shadcn_>
|
||||
<CommandGroup_Shadcn_>
|
||||
{foreignProjects.map((project, i) => {
|
||||
return (
|
||||
<CommandItem_Shadcn_
|
||||
key={project.id}
|
||||
value={`${project.name}-${i}`}
|
||||
className="flex gap-2 items-center"
|
||||
onSelect={() => {
|
||||
if (project.id) setForeignProjectId(project.id)
|
||||
setForeignProjectsComboboxOpen(false)
|
||||
}}
|
||||
>
|
||||
{getForeignProjectIcon?.(project) ?? integrationIcon}
|
||||
<span>{project.name}</span>
|
||||
</CommandItem_Shadcn_>
|
||||
)
|
||||
})}
|
||||
</CommandGroup_Shadcn_>
|
||||
</CommandList_Shadcn_>
|
||||
</Command_Shadcn_>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
</Panel>
|
||||
</div>
|
||||
<Command_Shadcn_>
|
||||
<CommandInput_Shadcn_ placeholder="Search organization..." />
|
||||
<CommandList_Shadcn_ className="!max-h-[170px]">
|
||||
<CommandEmpty_Shadcn_>No results found.</CommandEmpty_Shadcn_>
|
||||
<CommandGroup_Shadcn_>
|
||||
{foreignProjects.map((project, i) => {
|
||||
return (
|
||||
<CommandItem_Shadcn_
|
||||
key={project.id}
|
||||
value={`${project.name}-${i}`}
|
||||
className="flex gap-2 items-center"
|
||||
onSelect={() => {
|
||||
if (project.id) setForeignProjectId(project.id)
|
||||
setForeignProjectsComboboxOpen(false)
|
||||
}}
|
||||
>
|
||||
{getForeignProjectIcon?.(project) ?? integrationIcon}
|
||||
<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">
|
||||
{onSkip !== undefined && (
|
||||
|
||||
+2
-5
@@ -120,7 +120,7 @@ const SidePanelVercelProjectLinker = ({
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
header={'Add GitHub repository'}
|
||||
header={'Add new Vercel Project Connection'}
|
||||
size="large"
|
||||
visible={isOpen}
|
||||
hideFooter
|
||||
@@ -153,10 +153,7 @@ Check the details below before proceeding
|
||||
The following environment variables will be added:
|
||||
|
||||
${ENV_VAR_RAW_KEYS.map((x) => {
|
||||
return `
|
||||
\n
|
||||
- \`${x}\`
|
||||
`
|
||||
return `\n - \`${x}\``
|
||||
})}
|
||||
`}
|
||||
/>
|
||||
|
||||
@@ -2,18 +2,23 @@ import { useParams } from 'common'
|
||||
import { useFlag, withAuth } from 'hooks'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { PropsWithChildren, ReactNode, forwardRef } from 'react'
|
||||
import { IconX, cn } from 'ui'
|
||||
import { IconX, cn, LoadingLine, IconBook, IconLifeBuoy } from 'ui'
|
||||
import { ScaffoldContainer } from '../Scaffold'
|
||||
import Link from 'next/link'
|
||||
|
||||
export type IntegrationWindowLayoutProps = {
|
||||
title: string
|
||||
integrationIcon: ReactNode
|
||||
loading?: boolean
|
||||
docsHref?: string
|
||||
}
|
||||
|
||||
const IntegrationWindowLayout = ({
|
||||
title,
|
||||
integrationIcon,
|
||||
children,
|
||||
loading = false,
|
||||
docsHref,
|
||||
}: PropsWithChildren<IntegrationWindowLayoutProps>) => {
|
||||
const ongoingIncident = useFlag('ongoingIncident')
|
||||
const maxHeight = ongoingIncident ? 'calc(100vh - 44px)' : '100vh'
|
||||
@@ -21,8 +26,30 @@ const IntegrationWindowLayout = ({
|
||||
return (
|
||||
<div className="flex w-full flex-col" style={{ height: maxHeight, maxHeight }}>
|
||||
<Header title={title} integrationIcon={integrationIcon} />
|
||||
|
||||
{children}
|
||||
<LoadingLine loading={loading} />
|
||||
<main className="overflow-auto flex flex-col h-full bg">{children}</main>
|
||||
<ScaffoldContainer className="bg-body flex flex-row gap-6 py-6 border-t">
|
||||
{docsHref && (
|
||||
<Link href={docsHref} passHref>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-xs text-light hover:text"
|
||||
>
|
||||
<IconBook size={16} /> Docs
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
<Link href={'https://supabase.com/support'} passHref>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-xs text-light hover:text"
|
||||
>
|
||||
<IconLifeBuoy size={16} /> Support
|
||||
</a>
|
||||
</Link>
|
||||
</ScaffoldContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { PropsWithChildren } from 'react'
|
||||
import { useParams } from 'common'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import IntegrationWindowLayout from './IntegrationWindowLayout'
|
||||
import { useIntegrationInstallationSnapshot } from 'state/integration-installation'
|
||||
|
||||
const VERCEL_ICON = (
|
||||
<div className="bg-black shadow rounded p-1 w-8 h-8 flex justify-center items-center">
|
||||
@@ -13,12 +14,19 @@ const VERCEL_ICON = (
|
||||
const VercelIntegrationWindowLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
const { externalId } = useParams()
|
||||
|
||||
const snapshot = useIntegrationInstallationSnapshot()
|
||||
|
||||
const title = externalId
|
||||
? 'Supabase + Vercel Deploy Button'
|
||||
: 'Supabase + Vercel Integration Marketplace Connector'
|
||||
|
||||
return (
|
||||
<IntegrationWindowLayout title={title} integrationIcon={VERCEL_ICON}>
|
||||
<IntegrationWindowLayout
|
||||
title={title}
|
||||
integrationIcon={VERCEL_ICON}
|
||||
loading={snapshot.loading}
|
||||
docsHref="https://supabase.com/partners/integrations/vercel"
|
||||
>
|
||||
{children}
|
||||
</IntegrationWindowLayout>
|
||||
)
|
||||
|
||||
@@ -10,8 +10,7 @@ const VercelIntegrationLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
<Head>
|
||||
<title>Supabase + Vercel</title>
|
||||
</Head>
|
||||
|
||||
<main className="flex flex-col w-full h-full overflow-y-auto">
|
||||
<main className="flex flex-col w-full h-full">
|
||||
<div>
|
||||
<div className="mx-auto px-4 sm:px-6">
|
||||
<div className="max-w-xl flex justify-between items-center mx-auto py-4">
|
||||
@@ -29,7 +28,7 @@ const VercelIntegrationLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
</div>
|
||||
</div>
|
||||
<Divider light />
|
||||
<div className="flex-grow">
|
||||
<div className="flex-grow overflow-y-auto">
|
||||
<div className="max-w-7xl h-full mx-auto px-4 sm:px-6">
|
||||
<div className="max-w-xl h-full mx-auto py-10">{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { post } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { integrationKeys } from './keys'
|
||||
import { IntegrationConnectionsCreateVariables } from './types'
|
||||
import { useIntegrationInstallationSnapshot } from 'state/integration-installation'
|
||||
|
||||
export async function createIntegrationVercelConnections({
|
||||
organizationIntegrationId,
|
||||
@@ -44,6 +45,7 @@ export const useIntegrationVercelConnectionsCreateMutation = ({
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
const snapshot = useIntegrationInstallationSnapshot()
|
||||
return useMutation<
|
||||
IntegrationVercelConnectionsCreateData,
|
||||
ResponseError,
|
||||
@@ -63,6 +65,7 @@ export const useIntegrationVercelConnectionsCreateMutation = ({
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
snapshot.setLoading(false)
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to create connection: ${data.message}`)
|
||||
} else {
|
||||
|
||||
@@ -19,6 +19,8 @@ import { BASE_PATH } from 'lib/constants'
|
||||
import { EMPTY_ARR } from 'lib/void'
|
||||
import { NextPageWithLayout, Organization } from 'types'
|
||||
import { IconBook, IconLifeBuoy, LoadingLine } from 'ui'
|
||||
import VercelIntegrationWindowLayout from 'components/layouts/IntegrationsLayout/VercelIntegrationWindowLayout'
|
||||
import { useIntegrationInstallationSnapshot } from 'state/integration-installation'
|
||||
|
||||
const VERCEL_ICON = (
|
||||
<img src={`${BASE_PATH}/img/icons/vercel-icon.svg`} alt="Vercel Icon" className="w-4" />
|
||||
@@ -93,6 +95,8 @@ const VercelIntegration: NextPageWithLayout = () => {
|
||||
[vercelProjectsById]
|
||||
)
|
||||
|
||||
const snapshot = useIntegrationInstallationSnapshot()
|
||||
|
||||
const { mutateAsync: syncEnvs } = useIntegrationsVercelConnectionSyncEnvsMutation()
|
||||
const { mutate: createConnections, isLoading: isCreatingConnection } =
|
||||
useIntegrationVercelConnectionsCreateMutation({
|
||||
@@ -100,13 +104,22 @@ const VercelIntegration: NextPageWithLayout = () => {
|
||||
try {
|
||||
await syncEnvs({ connectionId: id })
|
||||
} catch (error: any) {
|
||||
snapshot.setLoading(false)
|
||||
toast.error('Failed to sync environment variables: ', error.message)
|
||||
}
|
||||
|
||||
if (next) {
|
||||
snapshot.setLoading(false)
|
||||
window.location.href = next
|
||||
}
|
||||
},
|
||||
onMutate() {
|
||||
snapshot.setLoading(true)
|
||||
},
|
||||
onError(error) {
|
||||
snapshot.setLoading(false)
|
||||
toast.error(`Creating connection failed: ${error.message}`)
|
||||
},
|
||||
})
|
||||
|
||||
const onCreateConnections = useCallback(
|
||||
@@ -131,71 +144,52 @@ const VercelIntegration: NextPageWithLayout = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingLine loading={isCreatingConnection} />
|
||||
<main className="overflow-auto flex flex-col h-full">
|
||||
<>
|
||||
<ScaffoldContainer className="flex flex-col gap-6 grow py-8">
|
||||
<ScaffoldColumn className="!max-w-[900px] mx-auto w-full">
|
||||
<header>
|
||||
<h1 className="text-xl text-scale-1200">
|
||||
Link a Supabase project to a Vercel project
|
||||
</h1>
|
||||
<Markdown
|
||||
className="text-scale-900"
|
||||
content={`
|
||||
<ScaffoldContainer className="flex flex-col gap-6 grow py-8">
|
||||
<ScaffoldColumn className="!max-w-[900px] mx-auto w-full">
|
||||
<header>
|
||||
<h1 className="text-xl text-scale-1200">Create your first Project Connection</h1>
|
||||
<Markdown
|
||||
className="text-scale-900"
|
||||
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
|
||||
organizationIntegrationId={integration?.id}
|
||||
foreignProjects={vercelProjects}
|
||||
supabaseProjects={supabaseProjects}
|
||||
onCreateConnections={onCreateConnections}
|
||||
installedConnections={integration?.connections}
|
||||
isLoading={isCreatingConnection}
|
||||
integrationIcon={VERCEL_ICON}
|
||||
getForeignProjectIcon={getForeignProjectIcon}
|
||||
choosePrompt="Choose Vercel Project"
|
||||
onSkip={() => {
|
||||
if (next) {
|
||||
window.location.href = next
|
||||
}
|
||||
}}
|
||||
loadingForeignProjects={isLoadingVercelProjectsData}
|
||||
loadingSupabaseProjects={isLoadingSupabaseProjectsData}
|
||||
/>
|
||||
<Markdown
|
||||
content={`
|
||||
/>
|
||||
</header>
|
||||
<ProjectLinker
|
||||
organizationIntegrationId={integration?.id}
|
||||
foreignProjects={vercelProjects}
|
||||
supabaseProjects={supabaseProjects}
|
||||
onCreateConnections={onCreateConnections}
|
||||
installedConnections={integration?.connections}
|
||||
isLoading={isCreatingConnection}
|
||||
integrationIcon={VERCEL_ICON}
|
||||
getForeignProjectIcon={getForeignProjectIcon}
|
||||
choosePrompt="Choose Vercel Project"
|
||||
onSkip={() => {
|
||||
if (next) {
|
||||
window.location.href = next
|
||||
}
|
||||
}}
|
||||
loadingForeignProjects={isLoadingVercelProjectsData}
|
||||
loadingSupabaseProjects={isLoadingSupabaseProjectsData}
|
||||
/>
|
||||
<Markdown
|
||||
content={`
|
||||
The following environment variables will be added:
|
||||
|
||||
${ENV_VAR_RAW_KEYS.map((x) => {
|
||||
return `
|
||||
\n
|
||||
- \`${x}\`
|
||||
`
|
||||
return `\n - \`${x}\``
|
||||
})}
|
||||
`}
|
||||
/>
|
||||
</ScaffoldColumn>
|
||||
</ScaffoldContainer>
|
||||
</>
|
||||
|
||||
<ScaffoldDivider />
|
||||
</main>
|
||||
<ScaffoldContainer className="bg-body flex flex-row gap-6 py-6 border-t">
|
||||
<div className="flex items-center gap-2 text-xs text-scale-900">
|
||||
<IconBook size={16} /> Docs
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-scale-900">
|
||||
<IconLifeBuoy size={16} /> Support
|
||||
</div>
|
||||
/>
|
||||
</ScaffoldColumn>
|
||||
</ScaffoldContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
VercelIntegration.getLayout = (page) => <VercelIntegrationLayout>{page}</VercelIntegrationLayout>
|
||||
VercelIntegration.getLayout = (page) => (
|
||||
<VercelIntegrationWindowLayout>{page}</VercelIntegrationWindowLayout>
|
||||
)
|
||||
|
||||
export default VercelIntegration
|
||||
@@ -7,11 +7,12 @@ import OrganizationPicker from 'components/interfaces/Integrations/OrganizationP
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
import { getHasInstalledObject } from 'components/layouts/IntegrationsLayout/Integrations.utils'
|
||||
import VercelIntegrationWindowLayout from 'components/layouts/IntegrationsLayout/VercelIntegrationWindowLayout'
|
||||
import { ScaffoldColumn, ScaffoldContainer, ScaffoldDivider } from 'components/layouts/Scaffold'
|
||||
import { ScaffoldColumn, ScaffoldContainer } from 'components/layouts/Scaffold'
|
||||
import { useIntegrationsQuery } from 'data/integrations/integrations-query'
|
||||
import { useVercelIntegrationCreateMutation } from 'data/integrations/vercel-integration-create-mutation'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useStore } from 'hooks'
|
||||
import Link from 'next/link'
|
||||
import { NextPageWithLayout, Organization } from 'types'
|
||||
import {
|
||||
AlertDescription_Shadcn_,
|
||||
@@ -19,11 +20,9 @@ import {
|
||||
Alert_Shadcn_,
|
||||
Button,
|
||||
IconAlertTriangle,
|
||||
IconBook,
|
||||
IconInfo,
|
||||
IconLifeBuoy,
|
||||
LoadingLine,
|
||||
} from 'ui'
|
||||
import { useIntegrationInstallationSnapshot } from 'state/integration-installation'
|
||||
|
||||
/**
|
||||
* Variations of the Vercel integration flow.
|
||||
@@ -42,6 +41,8 @@ const VercelIntegration: NextPageWithLayout = () => {
|
||||
const [selectedOrg, setSelectedOrg] = useState<Organization | null>(null)
|
||||
const [organizationIntegrationId, setOrganizationIntegrationId] = useState<string | null>(null)
|
||||
|
||||
const snapshot = useIntegrationInstallationSnapshot()
|
||||
|
||||
/**
|
||||
* Fetch the list of organization based integration installations for Vercel.
|
||||
*
|
||||
@@ -49,8 +50,11 @@ const VercelIntegration: NextPageWithLayout = () => {
|
||||
*/
|
||||
const { data: integrationData } = useIntegrationsQuery()
|
||||
|
||||
const { data: organizationsData, isLoading: isLoadingOrganizationsQuery } =
|
||||
useOrganizationsQuery()
|
||||
const {
|
||||
data: organizationsData,
|
||||
isLoading: isLoadingOrganizationsQuery,
|
||||
isSuccess: isOrganizationsDataSuccess,
|
||||
} = useOrganizationsQuery()
|
||||
|
||||
useEffect(() => {
|
||||
if (organizationsData !== undefined && integrationData !== undefined) {
|
||||
@@ -105,10 +109,13 @@ const VercelIntegration: NextPageWithLayout = () => {
|
||||
|
||||
const { mutate, isLoading: isLoadingVercelIntegrationCreateMutation } =
|
||||
useVercelIntegrationCreateMutation({
|
||||
onMutate() {
|
||||
snapshot.setLoading(true)
|
||||
},
|
||||
onSuccess({ id }) {
|
||||
setOrganizationIntegrationId(id)
|
||||
|
||||
handleRouteChange()
|
||||
snapshot.setLoading(false)
|
||||
},
|
||||
onError(error) {
|
||||
toast.error(`Creating Vercel integration failed: ${error.message}`)
|
||||
@@ -158,81 +165,90 @@ const VercelIntegration: NextPageWithLayout = () => {
|
||||
|
||||
const dataLoading = isLoadingVercelIntegrationCreateMutation || isLoadingOrganizationsQuery
|
||||
|
||||
const noOrganizations = useMemo(() => {
|
||||
return isOrganizationsDataSuccess && organizationsData?.length === 0 ? true : false
|
||||
}, [isOrganizationsDataSuccess, organizationsData])
|
||||
|
||||
const alreadyInstalled = useMemo(() => {
|
||||
return selectedOrg && installed[selectedOrg.slug] && source === 'marketplace' && !dataLoading
|
||||
? true
|
||||
: false
|
||||
}, [installed, selectedOrg, source, dataLoading])
|
||||
|
||||
const disableInstallationForm =
|
||||
(isLoadingVercelIntegrationCreateMutation && !dataLoading) ||
|
||||
// disables installation button if integration is already installed and it is Marketplace flow
|
||||
(selectedOrg && installed[selectedOrg.slug] && source === 'marketplace' && !dataLoading)
|
||||
? true
|
||||
: false
|
||||
alreadyInstalled ||
|
||||
noOrganizations
|
||||
|
||||
const isLoading = useMemo(() => {
|
||||
return isLoadingVercelIntegrationCreateMutation || isLoadingOrganizationsQuery
|
||||
}, [isLoadingVercelIntegrationCreateMutation, isLoadingOrganizationsQuery])
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="overflow-auto flex flex-col h-full bg">
|
||||
<LoadingLine loading={isLoadingVercelIntegrationCreateMutation} />
|
||||
{organizationIntegrationId === null && (
|
||||
<ScaffoldContainer className="flex flex-col gap-6 grow py-8">
|
||||
<ScaffoldColumn className="mx-auto w-full max-w-md">
|
||||
<h1 className="text-xl text-scale-1200">Choose organization</h1>
|
||||
<>
|
||||
<ScaffoldContainer className="flex flex-col gap-6 grow py-8">
|
||||
<ScaffoldColumn className="mx-auto w-full max-w-md">
|
||||
<h1 className="text-xl text-scale-1200">Choose organization</h1>
|
||||
<>
|
||||
<Markdown content={`Choose the Supabase organization you wish to install in`} />
|
||||
<OrganizationPicker
|
||||
integrationName="Vercel"
|
||||
selectedOrg={selectedOrg}
|
||||
onSelectedOrgChange={(org) => {
|
||||
setSelectedOrg(org)
|
||||
router.query.organizationSlug = org.slug
|
||||
}}
|
||||
configurationId={configurationId}
|
||||
/>
|
||||
{disableInstallationForm && (
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<IconAlertTriangle className="h-4 w-4" strokeWidth={2} />
|
||||
<AlertTitle_Shadcn_>
|
||||
Vercel Integration is already installed.
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
You will need to choose another organization to install the integration.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
)}
|
||||
<div className="flex flex-row w-full justify-end">
|
||||
<Button
|
||||
size="medium"
|
||||
className="self-end"
|
||||
disabled={disableInstallationForm || isLoadingVercelIntegrationCreateMutation}
|
||||
loading={isLoadingVercelIntegrationCreateMutation}
|
||||
onClick={onInstall}
|
||||
>
|
||||
Install integration
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</ScaffoldColumn>
|
||||
</ScaffoldContainer>
|
||||
<ScaffoldContainer className="flex flex-col gap-6 py-3">
|
||||
<Alert_Shadcn_ variant="default">
|
||||
<IconInfo className="h-4 w-4" strokeWidth={2} />
|
||||
<AlertTitle_Shadcn_>
|
||||
You can uninstall this Integration at any time.
|
||||
</AlertTitle_Shadcn_>
|
||||
<Markdown content={`Choose the Supabase organization you wish to install in`} />
|
||||
<OrganizationPicker
|
||||
integrationName="Vercel"
|
||||
selectedOrg={selectedOrg}
|
||||
disabled={noOrganizations || isLoading}
|
||||
onSelectedOrgChange={(org) => {
|
||||
setSelectedOrg(org)
|
||||
router.query.organizationSlug = org.slug
|
||||
}}
|
||||
configurationId={configurationId}
|
||||
/>
|
||||
{alreadyInstalled && (
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<IconAlertTriangle className="h-4 w-4" strokeWidth={2} />
|
||||
<AlertTitle_Shadcn_>Vercel Integration is already installed.</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
Remove this integration at any time via Vercel or the Supabase dashboard.
|
||||
You will need to choose another organization to install the integration.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
</ScaffoldContainer>
|
||||
)}
|
||||
{noOrganizations && (
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<IconAlertTriangle className="h-4 w-4" strokeWidth={2} />
|
||||
<AlertTitle_Shadcn_>
|
||||
No Supabase Organizations to install Integration.
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_ className="prose">
|
||||
You will need to create a Supabase Organization before you can install the Vercel
|
||||
Integration. You can create a new organization{' '}
|
||||
<Link href="https://supabase.com/dashboard/new" passHref>
|
||||
<a target="_blank">here</a>
|
||||
</Link>
|
||||
.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
)}
|
||||
<div className="flex flex-row w-full justify-end">
|
||||
<Button
|
||||
size="medium"
|
||||
className="self-end"
|
||||
disabled={disableInstallationForm || isLoadingVercelIntegrationCreateMutation}
|
||||
loading={isLoadingVercelIntegrationCreateMutation}
|
||||
onClick={onInstall}
|
||||
>
|
||||
Install integration
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ScaffoldDivider />
|
||||
</main>
|
||||
<ScaffoldContainer className="bg-body flex flex-row gap-6 py-6">
|
||||
<div className="flex items-center gap-2 text-xs text-scale-900">
|
||||
<IconBook size={16} /> Docs
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-scale-900">
|
||||
<IconLifeBuoy size={16} /> Support
|
||||
</div>
|
||||
</ScaffoldColumn>
|
||||
</ScaffoldContainer>
|
||||
<ScaffoldContainer className="flex flex-col gap-6 py-3">
|
||||
<Alert_Shadcn_ variant="default">
|
||||
<IconInfo className="h-4 w-4" strokeWidth={2} />
|
||||
<AlertTitle_Shadcn_>You can uninstall this Integration at any time.</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
Remove this integration at any time via Vercel or the Supabase dashboard.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
</ScaffoldContainer>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { proxy, snapshot, useSnapshot } from 'valtio'
|
||||
|
||||
export const integrationInstallationState = proxy({
|
||||
loading: false as boolean,
|
||||
setLoading: (boolean: boolean) => {
|
||||
integrationInstallationState.loading = boolean
|
||||
},
|
||||
})
|
||||
|
||||
export const getIntegrationInstallation = () => snapshot(integrationInstallationState)
|
||||
|
||||
export const useIntegrationInstallationSnapshot = (options?: Parameters<typeof useSnapshot>[1]) =>
|
||||
useSnapshot(integrationInstallationState, options)
|
||||
Reference in new issue
Block a user