mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
1 parent
d1f71464f1
commit
c7ab0c7370
4 files changed
+129
-38
No files matched your search
+64
-35
@@ -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)
|
||||
|
||||
@@ -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 || ''
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user