Merge pull request #16398 from supabase/chore/vercel-integration-empty-states

chore: fix empty states for Vercel Integration
This commit is contained in:
Jonathan Summers-Muir authored and GitHub committed 2023-08-10 13:21:09 +08:00
commit 2be678a8a3
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 && (
@@ -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
+87 -71
View File
@@ -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>
</>
)
+13
View File
@@ -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)