Files
supabase/apps/studio/components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx
T
Raminder SinghandJoshen Lim 897043952a feat: add edge function secret installation method (#45826)
Adds a new method of installation detection for partners like Doppler.
Doppler creates edge function secrets with specific names
(`DOPPLER_CONFIG`, `DOPPLER_ENVIRONMENT`, and `DOPPLER_PROJECT`). This
method allows the dashboard to check for the presence of such a secret
to show the installation status.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Support for edge-function-secret-based OAuth identification and
verification.
* Installation checks adapt to the selected installation method and only
fetch required credentials when needed.
* Integration detection can validate installations via API key prefixes
or matching edge-function secret names.
* **Chores**
* Shared helper utilities and types exported to streamline installation
checks and mappings.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45826)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-05-14 13:52:29 +05:30

164 lines
5.5 KiB
TypeScript

import { useQuery } from '@tanstack/react-query'
import { FeatureFlagContext, IS_PLATFORM, useFlag } from 'common'
import { fullImageUrl } from 'common/marketplace-client'
import { Boxes } from 'lucide-react'
import dynamic from 'next/dynamic'
import Image from 'next/image'
import { useContext, useMemo } from 'react'
import { cn } from 'ui'
import { INTEGRATIONS, Loading, type IntegrationDefinition } from './Integrations.constants'
import { marketplaceIntegrationsQueryOptions } from '@/data/marketplace/integrations-query'
import { useCLIReleaseVersionQuery } from '@/data/misc/cli-release-version-query'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
/**
* [Joshen] Returns a combination of
* - Marketplace integrations retrieved remotely (Only if feature flag enabled)
* - Existing integrations that are defined within studio
*/
export const useAvailableIntegrations = () => {
const { hasLoaded } = useContext(FeatureFlagContext)
const isMarketplaceEnabled = useFlag('marketplaceIntegrations')
const { integrationsWrappers } = useIsFeatureEnabled(['integrations:wrappers'])
const { data: cliData } = useCLIReleaseVersionQuery()
const isCLI = !!cliData?.current
const { data, error } = useQuery({
...marketplaceIntegrationsQueryOptions(),
enabled: isMarketplaceEnabled,
})
const isPending = IS_PLATFORM && (!hasLoaded || (isMarketplaceEnabled && !data && !error))
const isSuccess = !IS_PLATFORM || (hasLoaded && (!isMarketplaceEnabled || (!!data && !error)))
const isError = IS_PLATFORM && isMarketplaceEnabled && !!error
// [Joshen] Format marketplace integrations into existing ones for now
// Likely that we might need to change, but can look into separately
const marketplaceIntegrations: IntegrationDefinition[] = useMemo(
() =>
(data ?? [])?.map((integration) => {
const {
id: listingId,
slug,
categories,
featured,
title,
description,
documentation_url: docsUrl,
website_url: siteUrl,
installation_url: installUrl,
installation_url_type: installUrlType,
installation_identification_method: installMethod,
secret_key_prefix: secretKeyPrefix,
edge_function_secret_name: edgeFunctionSecretName,
images,
content,
partner_name: authorName,
listing_logo: listingLogo,
} = integration
const status = undefined
const author = { name: authorName ?? '', websiteUrl: '' }
return {
id: slug ?? '',
name: title ?? '',
status,
featured: !!featured,
type: 'oauth' as const, // Currently marketplace only supports oauth apps
categories: Array.isArray(categories)
? (categories as Array<{ slug: string }>).map((x) => x.slug)
: [],
content,
files: images?.map((image) => fullImageUrl(image)),
description,
docsUrl,
siteUrl,
installUrl,
installUrlType: installUrlType ?? undefined,
installIdentificationMethod: installMethod ?? undefined,
secretKeyPrefix: secretKeyPrefix ?? undefined,
edgeFunctionSecretName: edgeFunctionSecretName ?? undefined,
listingId: listingId ?? undefined,
author,
requiredExtensions: [],
icon: ({ className, ...props } = {}) => (
<div className="relative w-full h-full">
{listingLogo ? (
<Image
fill
src={fullImageUrl(listingLogo)}
alt=""
className={cn('p-2', className)}
{...props}
/>
) : (
<Boxes
className={cn('inset-0 p-2 text-black w-full h-full', className)}
{...props}
/>
)}
</div>
),
navigation: [
{
route: 'overview',
label: 'Overview',
},
],
navigate: ({ pageId = 'overview' }) => {
switch (pageId) {
case 'overview':
return dynamic(
() =>
import('@/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/index').then(
(mod) => mod.IntegrationOverviewTabV2
),
{
loading: Loading,
}
)
}
return null
},
}
}),
[data]
)
// [Joshen] Existing integrations that are defined within studio
// Available integrations are all integrations that can be installed. If an integration can't be installed (needed
// extensions are not available on this DB image), the UI will provide a tooltip explaining why.
const allIntegrations = useMemo(() => {
return INTEGRATIONS.filter((integration) => {
if (
!integrationsWrappers &&
(integration.type === 'wrapper' || integration.id.endsWith('_wrapper'))
) {
return false
}
if (integration.id === 'stripe_sync_engine' && isCLI) {
return false
}
return true
})
}, [integrationsWrappers, isCLI])
const dataWithMarketplace = useMemo(() => {
return [...marketplaceIntegrations, ...allIntegrations].sort((a, b) =>
a.name.localeCompare(b.name)
)
}, [marketplaceIntegrations, allIntegrations])
return {
data: dataWithMarketplace,
error,
isPending,
isSuccess,
isError,
}
}