diff --git a/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts b/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts index 3c55db6f9e6..a6802341ab7 100644 --- a/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts +++ b/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts @@ -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 + ) +} diff --git a/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegrationInterstitial.tsx b/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegrationInterstitial.tsx new file mode 100644 index 00000000000..f6c8960261f --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegrationInterstitial.tsx @@ -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 = Vercel + +export function VercelIntegrationLogo() { + return ( + + } + right={} + /> + ) +} + +export function VercelIntegrationFooter() { + return ( +

+ You can remove this integration at any time from Vercel or the Supabase dashboard. +

+ ) +} + +export function VercelEnvVarsSyncDescription() { + return ( + <> + + + + + +
+

Environment variables

+
    + {ENV_VAR_RAW_KEYS.map((key) => ( +
  • + {key} +
  • + ))} +
+
+
+
{' '} + will be synced to your Vercel project. + + ) +} + +interface VercelIntegrationInterstitialErrorStateProps { + title: string + errorMessage?: string | null +} + +export function VercelIntegrationInterstitialErrorState({ + title, + errorMessage, +}: VercelIntegrationInterstitialErrorStateProps) { + return ( +
+ + Retry the installation request from Vercel. + {errorMessage && ( + Error: {errorMessage} + )} + + } + /> + +
+ ) +} diff --git a/apps/studio/components/interfaces/Integrations/VercelGithub/IntegrationPanels.tsx b/apps/studio/components/interfaces/Integrations/VercelGithub/IntegrationPanels.tsx index 7d4ef739ef9..63c447fd0dd 100644 --- a/apps/studio/components/interfaces/Integrations/VercelGithub/IntegrationPanels.tsx +++ b/apps/studio/components/interfaces/Integrations/VercelGithub/IntegrationPanels.tsx @@ -18,7 +18,7 @@ import { getIntegrationConfigurationUrl } from '@/lib/integration-utils' const ICON_STROKE_WIDTH = 2 const ICON_SIZE = 14 -export interface IntegrationInstallationProps extends RefAttributes { +interface IntegrationInstallationProps extends RefAttributes { title: string integration: Integration disabled?: boolean @@ -72,16 +72,16 @@ const Avatar = ({ src }: { src: string | undefined }) => { ) } +const IntegrationIconBlock = ({ integration }: { integration: Integration }) => { + return ( +
+ +
+ ) +} + export const IntegrationInstallation = forwardRef( ({ integration, disabled, ...props }, ref) => { - const IntegrationIconBlock = () => { - return ( -
- -
- ) - } - return (
  • - +
    @@ -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 {props.title &&
    {props.title}
    } - + ) } diff --git a/apps/studio/components/interfaces/Integrations/VercelGithub/ProjectLinker.tsx b/apps/studio/components/interfaces/Integrations/VercelGithub/ProjectLinker.tsx index 238e5f1c7e0..a256f0dd418 100644 --- a/apps/studio/components/interfaces/Integrations/VercelGithub/ProjectLinker.tsx +++ b/apps/studio/components/interfaces/Integrations/VercelGithub/ProjectLinker.tsx @@ -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) => { + return ( +
    + {children} +
    + ) +} + +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( defaultForeignProjectId ) - const [supabaseProjectRef, setSupabaseProjectRef] = useState( - defaultSupabaseProjectRef - ) + const [selectedSupabaseProject, setSelectedSupabaseProject] = useState() 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) => { - return ( -
    - {children} -
    - ) - } - 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 (
    @@ -199,94 +431,7 @@ const ProjectLinker = ({ Supabase
    - { - setSupabaseProjectRef(project.ref) - setOpenProjectsDropdown(false) - }} - renderRow={(project) => { - return ( -
    -
    -
    - Supabase -
    -

    {project.name}

    - {project.status === 'INACTIVE' && Paused} - {project.status === 'GOING_DOWN' && Pausing} -
    - {project.ref === supabaseProjectRef && } -
    - ) - }} - 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

    - -
    -
    - ) - ) - }} - /> + {supabaseProjectSelector}
    @@ -296,116 +441,13 @@ const ProjectLinker = ({ {integrationIcon}
    - - -
    - } - iconRight={ - - - - } - > - {(selectedForeignProject && selectedForeignProject.name) ?? choosePrompt} - - - - - - - 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 - - - - )} -
    -
    -
    - + {foreignProjectSelector} )} -
    - {onSkip !== undefined && ( - - )} - -
    +
    {actionButtons}
    ) } - -export default ProjectLinker diff --git a/apps/studio/components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx b/apps/studio/components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx index dfc6aae960a..9a5d547a858 100644 --- a/apps/studio/components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx +++ b/apps/studio/components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx @@ -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 ) : (
    -
    +
      {connections?.map((connection) => ( {vercelIntegration ? (
      - - {connections.length > 0 ? ( - <> - -
        - {connections.map((connection) => ( -
        li]:pb-0' - )} - > - - {isProjectScoped ? ( -
        -
        - -
        + + + {connections.length > 0 && ( +
          + {connections.map((connection) => ( +
          li]:pb-0' + )} + > + + {isProjectScoped ? ( +
          +
          +
          - ) : null} -
          - ))} -
        - - ) : ( - +
        + ) : null} +
        + ))} +
      )} ( {displayName || Loading account}

      + {detail &&

      {detail}

      }
      {action} diff --git a/apps/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx b/apps/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx index aa6de0620fe..239ec8e56e9 100644 --- a/apps/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx +++ b/apps/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx @@ -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 = ( - Vercel Icon -) +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[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 ( <> - - -
      -

      Create your first Project Connection

      - -
      - { - if (next && isVercelUrl(next)) { - window.location.href = next - } - }} - loadingForeignProjects={isLoadingVercelProjectsData} - mode="Vercel" - /> - + {PAGE_TITLE} + -${ENV_VAR_RAW_KEYS.map((x) => { - return `\n - \`${x}\`` -})} -`} - /> -
      -
      + } + title="Connect Vercel project" + description={} + footer={} + > +
      + {showLoadingState ? ( + + ) : isError ? ( + + ) : organization === undefined ? ( + + ) : integrationNotFound ? ( + + ) : ( +
      + + + { + if (next && isVercelUrl(next)) { + window.location.href = next + } + }} + loadingForeignProjects={isLoadingVercelProjectsData} + mode="Vercel" + /> +
      + )} +
      +
      ) } -VercelIntegration.getLayout = (page) => ( - {page} +const ChooseProjectLoadingState = () => ( +
      + + + +
      + + + +
      +
      +
      +
      + + +
      +
      + + +
      + +
      ) -export default VercelIntegration +export default withAuth(VercelChooseProjectPage) diff --git a/apps/studio/pages/integrations/vercel/install.tsx b/apps/studio/pages/integrations/vercel/install.tsx index bc01578a213..ea4d72d9bd3 100644 --- a/apps/studio/pages/integrations/vercel/install.tsx +++ b/apps/studio/pages/integrations/vercel/install.tsx @@ -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(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 ( <> @@ -251,27 +255,19 @@ const VercelIntegration: NextPageWithLayout = () => { - } - right={} - /> - } - title="Install Vercel integration" - description="Choose an organization to connect to Vercel" + logo={} + title="Install Vercel Integration" + description="Choose the Supabase organization Vercel can connect to" + footer={} >
      {showLoadingState ? ( ) : isError ? ( - + ) : (
      @@ -357,26 +353,6 @@ const InstallationLoadingState = () => (
      ) -const InstallationErrorState = ({ errorMessage }: { errorMessage?: string }) => ( -
      - - Retry the installation request from Vercel. - {errorMessage && ( - Error: {errorMessage} - )} - - } - /> - -
      -) - interface OrganizationSelectProps { organizations: Organization[] selectedOrg: Organization | null