Files
supabase/apps/studio/components/interfaces/Integrations/Landing/useIntegrationDetail.ts
Alex Hall 568a26899a fix: catch permission errors when querying integrations data (#47272)
Previously, uncaught permission errors were bubbling up and preventing
the marketplace UI from rendering at all. This is a problem because
users might have access to the integrations tab, but not permissions to
view all the connected resources on a particular integration. In that
scenario, we want to degrade gracefully and show them only those
resources they have access to.
2026-07-06 15:26:23 -04:00

164 lines
5.0 KiB
TypeScript

import { useParams } from 'common'
import { useRouter } from 'next/router'
import { useEffect, useMemo } from 'react'
import { useProjectOAuthIntegrationData } from './Landing.utils'
import { useAvailableIntegrations } from './useAvailableIntegrations'
import { useInstalledIntegrations } from './useInstalledIntegrations'
import {
areRequiredExtensionsInstalledFor,
getFilteredNavItems,
getInstallActionType,
type InstallActionType,
} from './useIntegrationDetail.utils'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
export type IntegrationTab = {
label: string
href: string
active: boolean
}
export const useIntegrationDetail = () => {
const router = useRouter()
const { ref, id, pageId, childId } = useParams()
const { integrationsWrappers } = useIsFeatureEnabled(['integrations:wrappers'])
const isMarketplaceEnabled = useIsMarketplaceEnabled()
const { data: project } = useSelectedProjectQuery()
const { data: extensions } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data: allIntegrations, isPending: isAvailableLoading } = useAvailableIntegrations()
const { installedIntegrations, isLoading: isInstalledLoading } = useInstalledIntegrations()
// wrapped in useMemo to avoid UI resets when installing additional extensions like pg_net
const integration = useMemo(() => allIntegrations.find((i) => i.id === id), [allIntegrations, id])
const installation = useMemo(
() => installedIntegrations.find((i) => i.id === id),
[installedIntegrations, id]
)
// There does exist a mgmt-api endpoint to get a single status by integration-id, but the bulk results are likely already cached so go ahead and use them.
const { data: projectData, isLoading: isIntegrationStatusLoading } =
useProjectOAuthIntegrationData(project?.ref, { enabled: integration?.type === 'oauth' })
const integrationStatus = useMemo(
() => projectData.partnerIntegrations.find((i) => i.listing_slug === id),
[projectData, id]
)
const isInstalled = !!integration && !!installation
const areRequiredExtensionsInstalled = useMemo(
() => areRequiredExtensionsInstalledFor(integration, extensions),
[integration, extensions]
)
const navItems = useMemo(
() =>
getFilteredNavItems({
integration,
isInstalled,
isMarketplaceEnabled,
areRequiredExtensionsInstalled,
}),
[integration, isInstalled, isMarketplaceEnabled, areRequiredExtensionsInstalled]
)
const activeRoute = pageId ?? 'overview'
const activeNav = useMemo(
() => navItems.find((nav) => nav.route === activeRoute),
[navItems, activeRoute]
)
const isKnownRoute = !!activeNav
const layout = (activeNav?.layout ?? 'full') as 'full' | 'constrained'
const tabs: IntegrationTab[] = useMemo(
() =>
navItems.map((nav) => ({
label: nav.label,
href: `/project/${ref}/integrations/${id}/${nav.route}`,
active: nav.route === activeRoute,
})),
[navItems, ref, id, activeRoute]
)
const Component = useMemo(
() => integration?.navigate({ id, pageId, childId }),
[integration, id, pageId, childId]
)
const wrappersTabHref = useMemo(
() => tabs.find((tab) => tab.href.endsWith('/wrappers'))?.href,
[tabs]
)
const installActionType: InstallActionType = useMemo(
() =>
getInstallActionType({
integration,
isMarketplaceEnabled,
areRequiredExtensionsInstalled,
isInstalled,
}),
[integration, isMarketplaceEnabled, areRequiredExtensionsInstalled, isInstalled]
)
const isReady = !!router?.isReady
const isWrapperBlocked = !integrationsWrappers && !!id?.endsWith('_wrapper')
const pageTitle = integration?.name ?? 'Integration not found'
const pageSubTitle =
integration?.description ?? 'If you think this is an error, please contact support'
useEffect(() => {
if (
router?.isReady &&
!isAvailableLoading &&
!isInstalledLoading &&
!!integration &&
pageId &&
!isKnownRoute
) {
router.replace(`/project/${ref}/integrations/${id}/overview`)
}
}, [router, pageId, ref, id, integration, isKnownRoute, isAvailableLoading, isInstalledLoading])
return {
ref,
id,
pageId,
childId,
activeRoute,
activeNav,
isKnownRoute,
layout,
tabs,
isReady,
isWrapperBlocked,
pageTitle,
pageSubTitle,
integrationsWrappers,
integration,
installation,
integrationStatus,
isInstalled,
areRequiredExtensionsInstalled,
installActionType,
wrappersTabHref,
isAvailableLoading,
isInstalledLoading,
isIntegrationStatusLoading,
oauthIntegrationData: projectData,
Component,
}
}