remove integrations feature preview and old layout

This commit is contained in:
Francesco Sansalvadore committed 2026-07-23 19:42:14 +02:00
1 parent 7781ee0d04
commit 60d1856e05
23 files changed
+35 -500

No files matched your search

@@ -134,12 +134,6 @@ export const useIsSqlEditorManualSaveEnabled = () => {
return sqlEditorManualSaveEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE]
}
export const useIsMarketplaceEnabled = () => {
const { flags } = useFeaturePreviewContext()
const isMarketplaceEnabled = useFlag('marketplaceIntegrations')
return isMarketplaceEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE]
}
export const useFeaturePreviewModal = () => {
const featurePreviews = useFeaturePreviews()
const [featurePreviewModal, setFeaturePreviewModal] = useQueryState('featurePreviewModal')
@@ -50,7 +50,6 @@ const FEATURE_PREVIEW_KEY_TO_CONTENT: {
[LOCAL_STORAGE_KEYS.UI_PREVIEW_PLATFORM_WEBHOOKS]: <PlatformWebhooksPreview />,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_JIT_DB_ACCESS]: <JitDbAccessPreview />,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE]: <SqlEditorManualSavePreview />,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE]: <IntegrationsLayoutPreview />,
}
export const FeaturePreviewModal = () => {
@@ -22,7 +22,6 @@ export type FeaturePreview = {
export const useFeaturePreviews = (): FeaturePreview[] => {
const platformWebhooksEnabled = useFlag('platformWebhooks')
const jitDbAccessEnabled = useFlag('jitDbAccess')
const isMarketplaceEnabled = useFlag('marketplaceIntegrations')
const sqlEditorManualSaveEnabled = useFlag('sqlEditorManualSave')
const unifiedLogsDefaultOptIn = useFlag('unifiedLogsDefaultOptIn')
@@ -90,16 +89,6 @@ export const useFeaturePreviews = (): FeaturePreview[] => {
isDefaultOptIn: false,
getRoute: (ref?: string) => `/project/${ref}/database/column-privileges`,
},
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE,
name: 'One-Click Integrations',
discussionsUrl: undefined,
enabled: isMarketplaceEnabled,
isNew: true,
isPlatformOnly: false,
isDefaultOptIn: true,
getRoute: (ref?: string) => `/project/${ref}/integrations`,
},
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE,
name: 'Disable snippet auto-saving',
@@ -114,7 +103,6 @@ export const useFeaturePreviews = (): FeaturePreview[] => {
unifiedLogsDefaultOptIn,
platformWebhooksEnabled,
jitDbAccessEnabled,
isMarketplaceEnabled,
sqlEditorManualSaveEnabled,
]
)
@@ -1,8 +1,6 @@
import { PropsWithChildren } from 'react'
import { cn } from 'ui'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
interface ConstrainedIntegrationTabScaffoldProps extends PropsWithChildren {
className?: string
}
@@ -11,9 +9,8 @@ export const ConstrainedIntegrationTabScaffold = ({
children,
className,
}: ConstrainedIntegrationTabScaffoldProps) => {
const isMarketplace = useIsMarketplaceEnabled()
return (
<div className={cn('w-full py-6 xl:py-10', !isMarketplace && 'px-6 xl:px-10', className)}>
<div className={cn('w-full py-6 xl:py-10', className)}>
{children}
</div>
)
@@ -1,10 +1,5 @@
import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab'
import { RequiredExtensionsSection } from '../Integration/RequiredExtensionsSection'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
export const CronOverviewTab = () => {
const isMarketplaceEnabled = useIsMarketplaceEnabled()
if (isMarketplaceEnabled) return <RequiredExtensionsSection />
return <IntegrationOverviewTab />
}
@@ -1,12 +1,7 @@
import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab'
import { DataApiURLSettings } from './DataApiURLSettings'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
export const DataApiOverviewTab = () => {
const isMarketplaceEnabled = useIsMarketplaceEnabled()
if (isMarketplaceEnabled) return <DataApiURLSettings />
return (
<IntegrationOverviewTab>
<div className="px-4 md:px-10 max-w-4xl space-y-4">
@@ -1,10 +1,5 @@
import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab'
import { RequiredExtensionsSection } from '../Integration/RequiredExtensionsSection'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
export const GraphQLOverviewTab = () => {
const isMarketplaceEnabled = useIsMarketplaceEnabled()
if (isMarketplaceEnabled) return <RequiredExtensionsSection />
return <IntegrationOverviewTab />
return <RequiredExtensionsSection />
}
@@ -1,11 +1,5 @@
import type { PropsWithChildren } from 'react'
import { IntegrationOverviewTab } from './IntegrationOverviewTab'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
export const MarketplaceIntegrationOverviewTab = (props: PropsWithChildren) => {
const isMarketplaceEnabled = useIsMarketplaceEnabled()
if (isMarketplaceEnabled) return null
return <IntegrationOverviewTab>{props.children}</IntegrationOverviewTab>
return null
}
@@ -1,7 +1,5 @@
import { Badge, Card, CardContent, cn, Separator } from 'ui'
import { useIsMarketplaceEnabled } from '../../App/FeaturePreview/FeaturePreviewContext'
import { MissingExtensionAlert } from './MissingExtensionAlert'
import { useIntegrationDetail } from '@/components/interfaces/Integrations/Landing/useIntegrationDetail'
import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
@@ -21,7 +19,6 @@ export const RequiredExtensionsSection = ({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const isMarketplaceEnabled = useIsMarketplaceEnabled()
const requiredExtensions = integration?.requiredExtensions ?? []
@@ -59,10 +56,8 @@ export const RequiredExtensionsSection = ({
{extension ? (
isInstalled ? (
<Badge variant="secondary">Installed</Badge>
) : isMarketplaceEnabled ? (
<Badge variant="warning">Required</Badge>
) : (
<MissingExtensionAlert extension={extension} />
<Badge variant="warning">Required</Badge>
)
) : (
<span className="text-foreground-muted">Unavailable</span>
@@ -13,7 +13,6 @@ import { useContext, useMemo } from 'react'
import { cn } from 'ui'
import { INTEGRATIONS, Loading, type IntegrationDefinition } from './Integrations.constants'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import {
useMarketplaceIntegrationsQuery,
type MarketplaceIntegration,
@@ -55,16 +54,15 @@ const isPreviewEnabled = (featureFlags: FeatureFlagContextType, listingSlug: str
const useMarketplaceListings = () => {
const { hasLoaded } = useContext(FeatureFlagContext)
const isMarketplaceEnabled = useIsMarketplaceEnabled()
const { data: marketplaceData, error } = useMarketplaceIntegrationsQuery({
enabled: isMarketplaceEnabled,
enabled: true,
})
const isPending =
IS_PLATFORM && (!hasLoaded || (isMarketplaceEnabled && !marketplaceData && !error))
IS_PLATFORM && (!hasLoaded || (!marketplaceData && !error))
const isSuccess =
!IS_PLATFORM || (hasLoaded && (!isMarketplaceEnabled || (!!marketplaceData && !error)))
const isError = IS_PLATFORM && isMarketplaceEnabled && !!error
!IS_PLATFORM || (hasLoaded && (!!marketplaceData && !error))
const isError = IS_PLATFORM && !!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')
@@ -11,7 +11,6 @@ import {
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'
@@ -27,7 +26,6 @@ export const useIntegrationDetail = () => {
const { ref, id, pageId, childId } = useParams()
const { integrationsWrappers } = useIsFeatureEnabled(['integrations:wrappers'])
const isMarketplaceEnabled = useIsMarketplaceEnabled()
const { data: project } = useSelectedProjectQuery()
const { data: extensions } = useDatabaseExtensionsQuery({
@@ -65,10 +63,9 @@ export const useIntegrationDetail = () => {
getFilteredNavItems({
integration,
isInstalled,
isMarketplaceEnabled,
areRequiredExtensionsInstalled,
}),
[integration, isInstalled, isMarketplaceEnabled, areRequiredExtensionsInstalled]
[integration, isInstalled, areRequiredExtensionsInstalled]
)
const activeRoute = pageId ?? 'overview'
@@ -105,11 +102,10 @@ export const useIntegrationDetail = () => {
() =>
getInstallActionType({
integration,
isMarketplaceEnabled,
areRequiredExtensionsInstalled,
isInstalled,
}),
[integration, isMarketplaceEnabled, areRequiredExtensionsInstalled, isInstalled]
[integration, areRequiredExtensionsInstalled, isInstalled]
)
const isReady = !!router?.isReady
@@ -27,17 +27,15 @@ export function areRequiredExtensionsInstalledFor(
export function getFilteredNavItems({
integration,
isInstalled,
isMarketplaceEnabled,
areRequiredExtensionsInstalled,
}: {
integration: IntegrationDefinition | undefined
isInstalled: boolean
isMarketplaceEnabled: boolean
areRequiredExtensionsInstalled: boolean
}): Navigation[] {
if (!integration?.navigation) return []
if (isInstalled || integration.type !== 'wrapper') return integration.navigation
if (isMarketplaceEnabled || areRequiredExtensionsInstalled) return integration.navigation
if (areRequiredExtensionsInstalled) return integration.navigation
return integration.navigation.filter((nav) => nav.route !== 'wrappers')
}
@@ -46,18 +44,16 @@ export function getFilteredNavItems({
*/
export function getInstallActionType({
integration,
isMarketplaceEnabled,
areRequiredExtensionsInstalled,
isInstalled,
}: {
integration: IntegrationDefinition | undefined
isMarketplaceEnabled: boolean
areRequiredExtensionsInstalled: boolean
isInstalled: boolean
}): InstallActionType {
if (!integration) return null
if (integration.type === 'oauth') return 'oauth'
if (integration.type === 'wrapper' && (isMarketplaceEnabled || areRequiredExtensionsInstalled)) {
if (integration.type === 'wrapper' && areRequiredExtensionsInstalled) {
return 'add-wrapper'
}
if (isInstalled) return 'installed'
@@ -1,52 +1,5 @@
import { useParams } from 'common'
import Link from 'next/link'
import { Button } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab'
import { RequiredExtensionsSection } from '../Integration/RequiredExtensionsSection'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
import { useQueuesExposePostgrestStatusQuery } from '@/data/database-queues/database-queues-expose-postgrest-status-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
const QueuesContent = () => {
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
const { data: isExposed } = useQueuesExposePostgrestStatusQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data: extensions = [] } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const isQueuesInstalled = !!extensions.find((x) => x.name === 'pgmq')?.installed_version
if (isExposed) return null
return (
<Admonition
type="default"
title="Queues can be managed via any Supabase client library or PostgREST endpoints"
>
<p>
You may choose to toggle the exposure of Queues through Data APIs via the queues settings
</p>
{isQueuesInstalled && (
<Button asChild variant="default" className="mt-2">
<Link href={`/project/${ref}/integrations/queues/settings`}>Manage queues settings</Link>
</Button>
)}
</Admonition>
)
}
export const QueuesOverviewTab = () => {
const isMarketplaceEnabled = useIsMarketplaceEnabled()
if (isMarketplaceEnabled) return <RequiredExtensionsSection />
return <IntegrationOverviewTab actions={<QueuesContent />} />
return <RequiredExtensionsSection />
}
@@ -1,10 +1,5 @@
import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab'
import { RequiredExtensionsSection } from '../Integration/RequiredExtensionsSection'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
export const VaultOverviewTab = () => {
const isMarketplaceEnabled = useIsMarketplaceEnabled()
if (isMarketplaceEnabled) return <RequiredExtensionsSection />
return <IntegrationOverviewTab />
return <RequiredExtensionsSection />
}
@@ -4,9 +4,7 @@ import { toast } from 'sonner'
import { Admonition } from 'ui-patterns/admonition'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab'
import { RequiredExtensionsSection } from '../Integration/RequiredExtensionsSection'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { NoPermission } from '@/components/ui/NoPermission'
import { useHooksEnableMutation } from '@/data/database/hooks-enable-mutation'
@@ -89,11 +87,5 @@ const WebhooksContent = () => {
}
export const WebhooksOverviewTab = () => {
const isMarketplaceEnabled = useIsMarketplaceEnabled()
if (isMarketplaceEnabled) {
return <RequiredExtensionsSection />
}
return <IntegrationOverviewTab hideRequiredExtensionsSection actions={<WebhooksContent />} />
return <RequiredExtensionsSection />
}
@@ -32,7 +32,6 @@ import {
NewTable,
} from './Wrappers.utils'
import WrapperTableEditor from './WrapperTableEditor'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { getExtensionDefaultSchema } from '@/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/IntegrationOverviewTabV2.utils'
import { RequiredExtensionsSection } from '@/components/interfaces/Integrations/Integration/RequiredExtensionsSection'
import { useIntegrationDetail } from '@/components/interfaces/Integrations/Landing/useIntegrationDetail'
@@ -66,7 +65,6 @@ export const CreateWrapperSheet = ({
onCloseWithConfirmation,
}: CreateWrapperSheetProps) => {
const queryClient = useQueryClient()
const isMarketplaceEnabled = useIsMarketplaceEnabled()
const { integration } = useIntegrationDetail()
const { data: project } = useSelectedProjectQuery()
@@ -261,18 +259,16 @@ export const CreateWrapperSheet = ({
<SheetTitle>Create a {wrapperMeta.label} wrapper</SheetTitle>
</SheetHeader>
<div className="grow overflow-y-auto">
{isMarketplaceEnabled && (
<div className="px-5 py-5 flex flex-col gap-y-4 border-b">
{needsExtensions && (
<Admonition
type="warning"
title="Required extensions will be installed"
description="Proceeding will also install the extensions required by this wrapper."
/>
)}
<RequiredExtensionsSection hideSeparator />
</div>
)}
<div className="px-5 py-5 flex flex-col gap-y-4 border-b">
{needsExtensions && (
<Admonition
type="warning"
title="Required extensions will be installed"
description="Proceeding will also install the extensions required by this wrapper."
/>
)}
<RequiredExtensionsSection hideSeparator />
</div>
<FormSection header={<FormSectionLabel>Wrapper Configuration</FormSectionLabel>}>
<FormSectionContent className="flex flex-col space-y-2" loading={false}>
<FormField
@@ -13,7 +13,6 @@ import { CreateIcebergWrapperSheet } from './CreateIcebergWrapperSheet'
import { CreateWrapperSheet } from './CreateWrapperSheet'
import { WRAPPERS } from './Wrappers.constants'
import { WrapperTable } from './WrapperTable'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold'
import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
@@ -192,10 +191,6 @@ export const WrapperContent = () => {
}
export const WrapperOverviewTab = () => {
const isMarketplaceEnabled = useIsMarketplaceEnabled()
if (isMarketplaceEnabled) return <RequiredExtensionsSection />
return (
<IntegrationOverviewTab actions={<AddNewWrapperCTA />}>
<div className="mx-10">
@@ -23,7 +23,6 @@ import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import * as z from 'zod'
import { IntegrationOverviewTab } from '../../Integration/IntegrationOverviewTab'
import { RequiredExtensionsSection } from '../../Integration/RequiredExtensionsSection'
import { InstallationError } from './InstallationError'
import { IntegrationInstalledActions, IntegrationNotInstalledActions } from './IntegrationActions'
@@ -38,7 +37,6 @@ import {
isUninstalling,
} from './stripe-sync-status'
import { StripeSyncChangesCard } from './StripeSyncChangesCard'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useStripeSyncStatus } from '@/components/interfaces/Integrations/templates/StripeSyncEngine/useStripeSyncStatus'
import { useStripeSyncInstallMutation } from '@/data/database-integrations/stripe/stripe-sync-install-mutation'
import { useStripeSyncUninstallMutation } from '@/data/database-integrations/stripe/stripe-sync-uninstall-mutation'
@@ -392,22 +390,10 @@ const StripeSyncContent = ({ hideInstallCTA = false }: { hideInstallCTA?: boolea
}
export const StripeSyncEngineOverviewTab = () => {
const isMarketplaceEnabled = useIsMarketplaceEnabled()
if (isMarketplaceEnabled) {
return (
<>
<RequiredExtensionsSection />
<StripeSyncContent hideInstallCTA />
</>
)
}
return (
<IntegrationOverviewTab>
<div className="px-4 md:px-10 max-w-4xl space-y-4">
<StripeSyncContent />
</div>
</IntegrationOverviewTab>
<>
<RequiredExtensionsSection />
<StripeSyncContent hideInstallCTA />
</>
)
}
@@ -4,7 +4,6 @@ import { PropsWithChildren } from 'react'
import { Menu, Separator } from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useInstalledIntegrations } from '@/components/interfaces/Integrations/Landing/useInstalledIntegrations'
import { ProjectLayout } from '@/components/layouts/ProjectLayout'
import { AlertError } from '@/components/ui/AlertError'
@@ -46,7 +45,6 @@ const IntegrationCategoriesMenu = ({ page }: { page: string }) => {
const router = useRouter()
const { ref } = useParams()
const { hasLoaded: flagsLoaded } = useFeatureFlags()
const isMarketplaceEnabled = useIsMarketplaceEnabled()
const urlParams = new URLSearchParams(router.asPath.split('?')[1] || '')
const categoryParam = urlParams.get('category')
@@ -54,10 +52,10 @@ const IntegrationCategoriesMenu = ({ page }: { page: string }) => {
const { integrationsWrappers: showWrappers } = useIsFeatureEnabled(['integrations:wrappers'])
const { data: categories = [], isPending: isPendingCategories } = useMarketplaceCategoriesQuery({
enabled: isMarketplaceEnabled,
enabled: true,
})
const { data: listings = [], isPending: isPendingListings } = useMarketplaceIntegrationsQuery({
enabled: isMarketplaceEnabled,
enabled: true,
})
const populatedCategoryIds = new Set(
@@ -68,7 +66,7 @@ const IntegrationCategoriesMenu = ({ page }: { page: string }) => {
)
const isLoading = IS_PLATFORM
? !flagsLoaded || (isMarketplaceEnabled && (isPendingCategories || isPendingListings))
? !flagsLoaded || (isPendingCategories || isPendingListings)
: false
const allCategories = [
@@ -1,13 +1,7 @@
import type { PropsWithChildren } from 'react'
import { ProjectIntegrationsLayout } from './ProjectIntegrationsLayout'
import { ProjectMarketplaceLayout } from './ProjectMarketplaceLayout'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
export const ProjectIntegrationsLayoutDispatch = ({ children }: PropsWithChildren) => {
const isMarketplaceEnabled = useIsMarketplaceEnabled()
if (isMarketplaceEnabled) {
return <ProjectMarketplaceLayout>{children}</ProjectMarketplaceLayout>
}
return <ProjectIntegrationsLayout>{children}</ProjectIntegrationsLayout>
return <ProjectMarketplaceLayout>{children}</ProjectMarketplaceLayout>
}
@@ -1,14 +1,10 @@
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import LegacyIntegrationPage from '@/components/interfaces/Integrations/Integration/LegacyIntegrationPage'
import { MarketplaceDetail } from '@/components/interfaces/Integrations/Marketplace/MarketplaceDetail'
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
import { ProjectIntegrationsLayoutDispatch } from '@/components/layouts/ProjectIntegrationsLayoutDispatch'
import type { NextPageWithLayout } from '@/types'
const IntegrationPage: NextPageWithLayout = () => {
const isMarketplaceEnabled = useIsMarketplaceEnabled()
if (isMarketplaceEnabled) return <MarketplaceDetail />
return <LegacyIntegrationPage />
return <MarketplaceDetail />
}
IntegrationPage.getLayout = (page) => (
@@ -1,320 +1,10 @@
import { IS_PLATFORM, useFeatureFlags } from 'common'
import { Database } from 'common/marketplace.types'
import { Search } from 'lucide-react'
import { useRouter } from 'next/router'
import { parseAsString, useQueryState } from 'nuqs'
import { useMemo, type ReactNode } from 'react'
import { Input } from 'ui-patterns/DataInputs/Input'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
PageHeader,
PageHeaderAside,
PageHeaderDescription,
PageHeaderMeta,
PageHeaderSummary,
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import { PageSection, PageSectionContent, PageSectionMeta } from 'ui-patterns/PageSection'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import {
IntegrationCard,
IntegrationLoadingCard,
} from '@/components/interfaces/Integrations/Landing/IntegrationCard'
import { IntegrationDefinition } from '@/components/interfaces/Integrations/Landing/Integrations.constants'
import { useAvailableIntegrations } from '@/components/interfaces/Integrations/Landing/useAvailableIntegrations'
import { useInstalledIntegrations } from '@/components/interfaces/Integrations/Landing/useInstalledIntegrations'
import { useIntegrationFilteringAndSort } from '@/components/interfaces/Integrations/Landing/useIntegrationFilteringAndSort'
import { MarketplaceIndex } from '@/components/interfaces/Integrations/Marketplace/MarketplaceIndex'
import { DefaultLayout } from '@/components/layouts/DefaultLayout'
import { ProjectIntegrationsLayoutDispatch } from '@/components/layouts/ProjectIntegrationsLayoutDispatch'
import { AlertError } from '@/components/ui/AlertError'
import { DocsButton } from '@/components/ui/DocsButton'
import { NoSearchResults } from '@/components/ui/NoSearchResults'
import { useMarketplaceCategoriesQuery } from '@/data/marketplace/integration-categories-query'
import { BASE_PATH, DOCS_URL } from '@/lib/constants'
import type { NextPageWithLayout } from '@/types'
const FEATURED_INTEGRATIONS = ['cron', 'queues', 'stripe_sync_engine']
// Featured integration images
const FEATURED_INTEGRATION_IMAGES: Record<string, string> = {
cron: `${BASE_PATH}/img/integrations/covers/cron-cover.webp`,
queues: `${BASE_PATH}/img/integrations/covers/queues-cover.png`,
stripe_wrapper: `${BASE_PATH}/img/integrations/covers/stripe-cover.png`,
stripe_sync_engine: `${BASE_PATH}/img/integrations/covers/stripe-cover.png`,
}
function getIntegrationImage(integration: IntegrationDefinition) {
let featured_image = FEATURED_INTEGRATION_IMAGES[integration.id]
if (featured_image) {
return featured_image
}
if (integration.files?.length) {
const heroImage = integration?.files?.[0]
return heroImage?.src ?? undefined
}
}
type PageContent = {
title: string
subtitle: string
secondaryActions?: ReactNode
}
const DEFAULT_PAGE_CONTENT: PageContent = {
title: 'Extend your database',
subtitle:
'Extensions and wrappers that add functionality to your database and connect to external services.',
}
const CATEGORY_PAGE_CONTENT = {
wrapper: {
title: 'Wrappers',
subtitle:
'Connect to external data sources and services by querying APIs, databases, and files as if they were Postgres tables.',
secondaryActions: (
<DocsButton href={`${DOCS_URL}/guides/database/extensions/wrappers/overview`} />
),
},
postgres_extension: {
title: 'Postgres Modules',
subtitle: 'Extend your database with powerful Postgres extensions.',
},
} satisfies Record<string, PageContent>
// Converts a category string to title
// Example: some_catory -> Some Category
function formatCategoryTitle(category: string) {
return category
.split(/[-_]/)
.filter(Boolean)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
}
const PageHeaderContentSkeleton = () => (
<div className="flex flex-col gap-y-2">
<ShimmeringLoader className="h-8 w-48" />
<ShimmeringLoader className="h-4 w-full max-w-xl" />
</div>
)
// Returns selected category to filter by
function useFilterCategory() {
const router = useRouter()
const [selectedCategory] = useQueryState(
'category',
parseAsString.withDefault('all').withOptions({ clearOnDefault: true })
)
const categoryFromUrl = useMemo(() => {
const queryString = router.asPath.split('?')[1]
if (!queryString) return null
return new URLSearchParams(queryString).get('category')
}, [router.asPath])
const resolvedSelectedCategory = router.isReady
? (categoryFromUrl ?? selectedCategory)
: undefined
const filterCategory = resolvedSelectedCategory ?? 'all'
return filterCategory
}
// Dynamic page content based on selected category
function usePageContent(
integrationFilterCategory: string,
categories: Database['public']['Views']['categories']['Row'][]
) {
const pageContent = useMemo<PageContent>(() => {
if (integrationFilterCategory === 'all') {
return DEFAULT_PAGE_CONTENT
}
if (integrationFilterCategory in CATEGORY_PAGE_CONTENT) {
return CATEGORY_PAGE_CONTENT[integrationFilterCategory as keyof typeof CATEGORY_PAGE_CONTENT]
}
const selectedMarketplaceCategory = categories.find(
(category) => category.slug === integrationFilterCategory
)
return {
title: selectedMarketplaceCategory?.name ?? formatCategoryTitle(integrationFilterCategory),
subtitle: selectedMarketplaceCategory?.description ?? DEFAULT_PAGE_CONTENT.subtitle,
}
}, [categories, integrationFilterCategory])
return pageContent
}
const IntegrationsPage: NextPageWithLayout = () => {
const isMarketplaceEnabled = useIsMarketplaceEnabled()
if (isMarketplaceEnabled) return <MarketplaceIndex />
return <LegacyIntegrationsPage />
}
const LegacyIntegrationsPage = () => {
const { hasLoaded: flagsLoaded } = useFeatureFlags()
const isMarketplaceEnabled = useIsMarketplaceEnabled()
const [search, setSearch] = useQueryState(
'search',
parseAsString.withDefault('').withOptions({ clearOnDefault: true })
)
const {
data: availableIntegrations,
error,
isPending: isLoadingAvailableIntegrations,
isError,
isSuccess: isSuccessAvailableIntegrations,
} = useAvailableIntegrations()
const {
installedIntegrations,
isLoading: isLoadingInstalledIntegrations,
isSuccess: isSuccessInstalledIntegrations,
} = useInstalledIntegrations()
const installedIds = installedIntegrations.map((i) => i.id)
const isLoading = isLoadingAvailableIntegrations || isLoadingInstalledIntegrations
const isSuccess = isSuccessAvailableIntegrations && isSuccessInstalledIntegrations
const selectedCategory = useFilterCategory()
const { data: categories = [], isPending: isPendingCategories } = useMarketplaceCategoriesQuery({
enabled: isMarketplaceEnabled,
})
const isLoadingSelectedCategory =
selectedCategory !== 'all' &&
!(selectedCategory in CATEGORY_PAGE_CONTENT) &&
(IS_PLATFORM ? !flagsLoaded || (isMarketplaceEnabled && isPendingCategories) : false)
const pageContent = usePageContent(selectedCategory, categories)
const filteredIntegrations = useMemo(() => {
let filtered = availableIntegrations ?? []
if (selectedCategory !== 'all') {
filtered = filtered.filter(
(i) => i.type === selectedCategory || i.categories?.includes(selectedCategory)
)
}
if (search.length > 0) {
filtered = filtered.filter((i) => i.name.toLowerCase().includes(search.toLowerCase()))
}
return filtered
}, [availableIntegrations, selectedCategory, search])
const hasActiveFilter = selectedCategory !== 'all' || search.length > 0
const { sorted: filteredAndSortedIntegrations, featured: featuredIntegrations } =
useIntegrationFilteringAndSort(filteredIntegrations, availableIntegrations, installedIds, {
featuredIds: FEATURED_INTEGRATIONS,
hasActiveFilter,
includeFeaturedFlag: true,
})
return (
<>
<PageHeader size="large">
<PageHeaderMeta>
<PageHeaderSummary>
{isLoadingSelectedCategory ? (
<PageHeaderContentSkeleton />
) : (
<>
<PageHeaderTitle>{pageContent.title}</PageHeaderTitle>
<PageHeaderDescription>{pageContent.subtitle}</PageHeaderDescription>
</>
)}
</PageHeaderSummary>
{pageContent.secondaryActions && (
<PageHeaderAside>{pageContent.secondaryActions}</PageHeaderAside>
)}
</PageHeaderMeta>
</PageHeader>
<PageContainer size="large">
<PageSection>
<PageSectionMeta>
<Input
value={search}
size="tiny"
onChange={(e) => setSearch(e.target.value)}
placeholder="Search integrations..."
icon={<Search size={14} />}
className="w-52"
/>
</PageSectionMeta>
<PageSectionContent>
{isLoading && (
<div
className="grid xl:grid-cols-3 2xl:grid-cols-4 gap-x-4 gap-y-3"
style={{ gridAutoRows: 'minmax(110px, auto)' }}
>
{Array.from({ length: 8 }).map((_, idx) => (
<IntegrationLoadingCard key={`integration-loading-${idx}`} />
))}
</div>
)}
{/* Error State */}
{isError && (
<AlertError subject="Failed to retrieve available integrations" error={error} />
)}
{/* Success State */}
{isSuccess && (
<>
{/* No Search Results */}
{search.length > 0 && filteredAndSortedIntegrations.length === 0 && (
<NoSearchResults searchString={search} onResetFilter={() => setSearch('')} />
)}
{/* Featured Integrations */}
{featuredIntegrations && featuredIntegrations.length > 0 && (
<div
className="grid grid-cols-2 @4xl:grid-cols-3 gap-4 mb-4 items-stretch pb-6 border-b"
style={{ gridAutoRows: 'minmax(110px, auto)' }}
>
{featuredIntegrations.map((integration) => (
<IntegrationCard
key={integration.id}
{...integration}
isInstalled={installedIds.includes(integration.id)}
featured={true}
image={getIntegrationImage(integration)}
/>
))}
</div>
)}
{/* All Filtered and Sorted Integrations */}
{filteredAndSortedIntegrations.length > 0 && (
<div className="grid @xl:grid-cols-3 @6xl:grid-cols-4 gap-4">
{filteredAndSortedIntegrations.map((integration) => (
<IntegrationCard
key={integration.id}
{...integration}
isInstalled={installedIds.includes(integration.id)}
featured={false}
image={FEATURED_INTEGRATION_IMAGES[integration.id]}
/>
))}
</div>
)}
</>
)}
</PageSectionContent>
</PageSection>
</PageContainer>
</>
)
return <MarketplaceIndex />
}
IntegrationsPage.getLayout = (page) => (
@@ -26,7 +26,6 @@ export const LOCAL_STORAGE_KEYS = {
UI_PREVIEW_PLATFORM_WEBHOOKS: 'supabase-ui-platform-webhooks',
UI_PREVIEW_JIT_DB_ACCESS: 'supabase-ui-jit-db-access',
UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE: 'supabase-ui-sql-editor-manual-save',
UI_PREVIEW_MARKETPLACE: 'supabase-ui-marketplace',
AI_ASSISTANT_MCP_OPT_IN: 'ai-assistant-mcp-opt-in',
SIGN_IN_CHATGPT_ENABLED: 'siwc-enabled',
@@ -155,7 +154,6 @@ const LOCAL_STORAGE_KEYS_ALLOWLIST = [
LOCAL_STORAGE_KEYS.UI_PREVIEW_PLATFORM_WEBHOOKS,
LOCAL_STORAGE_KEYS.UI_PREVIEW_JIT_DB_ACCESS,
LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE,
LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE,
LOCAL_STORAGE_KEYS.LAST_SIGN_IN_METHOD,
LOCAL_STORAGE_KEYS.HIDE_PROMO_TOAST,
LOCAL_STORAGE_KEYS.BLOG_VIEW,