feat(studio): marketplace preview listings (#47004)

Updates the Studio integrations marketplace to support the new
marketplace-specific database view and `preview` status
This commit is contained in:
Alex Hall authored and GitHub committed 2026-06-23 11:16:58 -04:00
1 parent d1f71464f1
commit c7ab0c7370
4 files changed
+129 -38

No files matched your search

@@ -1,4 +1,10 @@
import { FeatureFlagContext, IS_PLATFORM, useFlag } from 'common'
import {
FeatureFlagContext,
FeatureFlagContextType,
IS_PLATFORM,
useFeatureFlags,
useFlag,
} from 'common'
import { fullImageUrl } from 'common/marketplace-client'
import { Boxes } from 'lucide-react'
import dynamic from 'next/dynamic'
@@ -38,28 +44,65 @@ function isForeignDataWrapper(integration: MarketplaceIntegration) {
return integration.categories.some((c) => c?.slug === 'foreign-data-wrapper')
}
/**
* Use per-listing feature flags with a templated naming convention in order to independently
* enable/disable previews for users where the global `previewMarketplaceListingsEnabled` flag is not set.
*/
const isPreviewEnabled = (featureFlags: FeatureFlagContextType, listingSlug: string) => {
const flagName = `${listingSlug}DashboardIntegrationEnabled`
return (featureFlags.configcat[flagName] ?? false) as boolean
}
const useMarketplaceListings = () => {
const { hasLoaded } = useContext(FeatureFlagContext)
const isMarketplaceEnabled = useIsMarketplaceEnabled()
const { data: marketplaceData, error } = useMarketplaceIntegrationsQuery({
enabled: isMarketplaceEnabled,
})
const isPending =
IS_PLATFORM && (!hasLoaded || (isMarketplaceEnabled && !marketplaceData && !error))
const isSuccess =
!IS_PLATFORM || (hasLoaded && (!isMarketplaceEnabled || (!!marketplaceData && !error)))
const isError = IS_PLATFORM && isMarketplaceEnabled && !!error
// This flag can globally enable preview listings for all partners for a given user (i.e. for Supabase users)
const previewAllListingsEnabled = useFlag<boolean>('previewMarketplaceListingsEnabled')
const featureFlags = useFeatureFlags()
const enabledListings = useMemo(
() =>
(marketplaceData ?? []).filter(
(integration) =>
integration.review_status === 'approved' ||
(integration.review_status === 'preview' &&
(previewAllListingsEnabled || isPreviewEnabled(featureFlags, integration.slug)))
),
[marketplaceData, previewAllListingsEnabled, featureFlags]
)
return { isPending, isSuccess, isError, data: enabledListings, error }
}
/**
* [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 = useIsMarketplaceEnabled()
const { integrationsWrappers } = useIsFeatureEnabled(['integrations:wrappers'])
const grafanaEnabled = useFlag('grafanaDashboardIntegrationEnabled')
const resendEnabled = useFlag('resendDashboardIntegrationEnabled')
const aikidoEnabled = useFlag('aikidoDashboardIntegrationEnabled')
const dopplerEnabled = useFlag('dopplerDashboardIntegrationEnabled')
const { data: cliData } = useCLIReleaseVersionQuery()
const isCLI = !!cliData?.current
const { data, error } = useMarketplaceIntegrationsQuery({ enabled: isMarketplaceEnabled })
const isPending = IS_PLATFORM && (!hasLoaded || (isMarketplaceEnabled && !data && !error))
const isSuccess = !IS_PLATFORM || (hasLoaded && (!isMarketplaceEnabled || (!!data && !error)))
const isError = IS_PLATFORM && isMarketplaceEnabled && !!error
const {
isPending,
isSuccess,
isError,
data: marketplaceListings,
error,
} = useMarketplaceListings()
// [Joshen] Format marketplace integrations into existing ones for now
// Likely that we might need to change, but can look into separately
@@ -67,8 +110,8 @@ export const useAvailableIntegrations = () => {
// hardcoded studio wrappers below as content overrides.
const marketplaceIntegrations: IntegrationDefinition[] = useMemo(
() =>
(data ?? [])
?.filter((integration) => !isForeignDataWrapper(integration))
marketplaceListings
.filter((integration) => !isForeignDataWrapper(integration))
.map((integration) => {
const {
id: listingId,
@@ -158,19 +201,19 @@ export const useAvailableIntegrations = () => {
},
}
}),
[data]
[marketplaceListings]
)
// Marketplace wrapper listings keyed by their studio-equivalent id
// (marketplace uses dash-separated slugs, studio uses underscore-separated ids).
const marketplaceWrappers = useMemo(() => {
const map: Record<string, MarketplaceIntegration> = {}
;(data ?? []).forEach((integration) => {
marketplaceListings.forEach((integration) => {
if (!isForeignDataWrapper(integration)) return
map[integration.slug.replaceAll('-', '_')] = integration
})
return map
}, [data])
}, [marketplaceListings])
// [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
@@ -226,24 +269,10 @@ export const useAvailableIntegrations = () => {
}, [integrationsWrappers, isCLI, marketplaceWrappers])
const dataWithMarketplace = useMemo(() => {
const flagGatedIds: Record<string, boolean> = {
grafana: grafanaEnabled,
resend: resendEnabled,
aikido: aikidoEnabled,
doppler: dopplerEnabled,
}
return [...marketplaceIntegrations, ...allIntegrations]
.filter((integration) => flagGatedIds[integration.id] !== false)
.sort((a, b) => a.name.localeCompare(b.name))
}, [
marketplaceIntegrations,
allIntegrations,
grafanaEnabled,
resendEnabled,
aikidoEnabled,
dopplerEnabled,
])
return [...marketplaceIntegrations, ...allIntegrations].sort((a, b) =>
a.name.localeCompare(b.name)
)
}, [marketplaceIntegrations, allIntegrations])
return {
data: dataWithMarketplace,
@@ -1,16 +1,16 @@
import { useQuery } from '@tanstack/react-query'
import { createMarketplaceClient, type Listing } from 'common/marketplace-client'
import { createMarketplaceClient, type MarketplaceListing } from 'common/marketplace-client'
import { marketplaceIntegrationsKeys } from './keys'
import { handleError } from '@/data/fetchers'
import type { ResponseError, UseCustomQueryOptions } from '@/types'
export type MarketplaceIntegration = Listing
export type MarketplaceIntegration = MarketplaceListing
export async function getMarketplaceIntegrations(signal?: AbortSignal) {
const marketplaceClient = createMarketplaceClient()
let query = marketplaceClient
.from('listings')
.from('marketplace_listings')
.select('*')
.not('published_in_marketplace_at', 'is', null)
if (signal) query = query.abortSignal(signal)
+19
View File
@@ -33,12 +33,31 @@ export type Database = MergeDeep<
listing_logo: string
}
}
marketplace_listings: {
Row: {
// add a type for the JSON structure
categories: Category[]
// These all come from non-nullable columns but all view columns are inferred as nullable.
// See https://github.com/orgs/supabase/discussions/14151
featured: boolean
partner_name: string
built_by: string
slug: string
title: string
description: string
content: string
website_url: string
documentation_url: string
listing_logo: string
}
}
}
}
}
>
export type Listing = Database['public']['Views']['listings']['Row']
export type MarketplaceListing = Database['public']['Views']['marketplace_listings']['Row']
export const createMarketplaceClient = () => {
const API_URL = process.env.NEXT_PUBLIC_MARKETPLACE_API_URL || ''
+43
View File
@@ -73,6 +73,49 @@ export type Database = {
},
]
}
marketplace_listings: {
Row: {
built_by: string | null
categories: Json | null
content: string | null
description: string | null
documentation_url: string | null
edge_function_secret_name: string | null
featured: boolean | null
id: string | null
images: string[] | null
installation_identification_method:
| 'secret_key_prefix'
| 'edge_function_secret_name'
| 'integration_status'
| 'oauth_authorization'
| null
installation_url: string | null
installation_url_type: 'get' | 'post' | null
listing_logo: string | null
oauth_app_id: string | null
partner_id: string | null
partner_logo: string | null
partner_name: string | null
partner_slug: string | null
published_in_marketplace_at: string | null
review_status: 'draft' | 'pending' | 'approved' | 'rejected' | 'preview' | null
secret_key_prefix: string | null
slug: string | null
title: string | null
website_url: string | null
youtube_id: string | null
}
Relationships: [
{
foreignKeyName: 'listings_partner_id_fkey'
columns: ['partner_id']
isOneToOne: false
referencedRelation: 'partners'
referencedColumns: ['id']
},
]
}
partners: {
Row: {
country: string | null