diff --git a/apps/studio/components/interfaces/App/FeaturePreview/IntegrationsLayoutPreview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/IntegrationsLayoutPreview.tsx
new file mode 100644
index 00000000000..55c42690c66
--- /dev/null
+++ b/apps/studio/components/interfaces/App/FeaturePreview/IntegrationsLayoutPreview.tsx
@@ -0,0 +1,7 @@
+export const IntegrationsLayoutPreview = () => (
+
+
+ Browse and install integrations from a new layout with improved filtering and search.
+
+
+)
diff --git a/apps/studio/components/interfaces/App/FeaturePreview/MarketplacePreview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/MarketplacePreview.tsx
deleted file mode 100644
index a701e82504f..00000000000
--- a/apps/studio/components/interfaces/App/FeaturePreview/MarketplacePreview.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-export const MarketplacePreview = () => (
-
-
- Browse and install integrations from a new Marketplace layout with improved filtering and
- search.
-
-
- This preview is on by default for internal teammates and select integration partners. Turn it
- off to fall back to the previous Integrations layout at any time.
-
-
-)
diff --git a/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts b/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts
index f3d40b8a6cf..060b22c01fd 100644
--- a/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts
+++ b/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts
@@ -86,7 +86,7 @@ export const useFeaturePreviews = (): FeaturePreview[] => {
},
{
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE,
- name: 'Marketplace',
+ name: 'Integrations layout',
discussionsUrl: undefined,
enabled: true,
isNew: true,
diff --git a/apps/studio/components/interfaces/Integrations/Landing/IntegrationCard.tsx b/apps/studio/components/interfaces/Integrations/Landing/IntegrationCard.tsx
index 934cfd17070..064d91a1f87 100644
--- a/apps/studio/components/interfaces/Integrations/Landing/IntegrationCard.tsx
+++ b/apps/studio/components/interfaces/Integrations/Landing/IntegrationCard.tsx
@@ -59,7 +59,7 @@ export const IntegrationCard = ({
fill
src={image}
alt={`${name} integration`}
- className="w-full h-full object-cover invert dark:invert-0"
+ className="w-full h-full object-cover"
objectFit="cover"
/>
) : (
diff --git a/apps/studio/components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx b/apps/studio/components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx
index 2dd5d116ce4..5319d27c7cb 100644
--- a/apps/studio/components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx
+++ b/apps/studio/components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx
@@ -1,4 +1,4 @@
-import { FeatureFlagContext, IS_PLATFORM } from 'common'
+import { FeatureFlagContext, IS_PLATFORM, useFlag } from 'common'
import { fullImageUrl } from 'common/marketplace-client'
import { Boxes } from 'lucide-react'
import dynamic from 'next/dynamic'
@@ -48,6 +48,11 @@ export const useAvailableIntegrations = () => {
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
@@ -206,10 +211,24 @@ export const useAvailableIntegrations = () => {
}, [integrationsWrappers, isCLI, marketplaceWrappers])
const dataWithMarketplace = useMemo(() => {
- return [...marketplaceIntegrations, ...allIntegrations].sort((a, b) =>
- a.name.localeCompare(b.name)
- )
- }, [marketplaceIntegrations, allIntegrations])
+ const flagGatedIds: Record
= {
+ 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 {
data: dataWithMarketplace,
diff --git a/apps/studio/components/interfaces/Integrations/Marketplace/Marketplace.constants.tsx b/apps/studio/components/interfaces/Integrations/Marketplace/Marketplace.constants.tsx
index f0ab5d79339..e80fd564f0f 100644
--- a/apps/studio/components/interfaces/Integrations/Marketplace/Marketplace.constants.tsx
+++ b/apps/studio/components/interfaces/Integrations/Marketplace/Marketplace.constants.tsx
@@ -32,14 +32,7 @@ import type {
export type { MarketplaceSource } from '@/components/interfaces/Integrations/Landing/Integrations.constants'
// Defines featured integrations and their order in the featured hero
-export const FEATURED_INTEGRATION_IDS = [
- 'stripe_sync_engine',
- 'grafana',
- 'aikido',
- 'aikido-security',
- 'doppler',
- 'resend',
-] as const
+export const FEATURED_INTEGRATION_IDS = ['grafana', 'cron', 'queues', 'data_api'] as const
export type MarketplaceIntegrationType = 'oauth' | 'postgres_extension' | 'template' | 'wrapper'
diff --git a/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceFeaturedHeroGrid.tsx b/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceFeaturedHeroGrid.tsx
new file mode 100644
index 00000000000..9722c979052
--- /dev/null
+++ b/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceFeaturedHeroGrid.tsx
@@ -0,0 +1,175 @@
+import { useParams } from 'common'
+import Image from 'next/image'
+import Link from 'next/link'
+import { Badge, Card } from 'ui'
+
+import { IntegrationLogo } from '../Integration/IntegrationLogo'
+import { getMarketplaceSource, MarketplaceSourceBadge } from './Marketplace.constants'
+import type { IntegrationDefinition } from '@/components/interfaces/Integrations/Landing/Integrations.constants'
+import { BASE_PATH } from '@/lib/constants'
+
+const FEATURED_INTEGRATION_IMAGES: Record = {
+ 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`,
+ grafana: `${BASE_PATH}/img/integrations/covers/grafana-cover.png`,
+}
+
+function getIntegrationImage(integration: IntegrationDefinition) {
+ const featuredImage = FEATURED_INTEGRATION_IMAGES[integration.id]
+ if (featuredImage) return featuredImage
+ if (integration.files?.length) {
+ const heroImage = integration.files[0]
+ return typeof heroImage === 'string' ? heroImage : (heroImage?.src ?? undefined)
+ }
+}
+
+interface MarketplaceFeaturedHeroGridProps {
+ integrations: IntegrationDefinition[]
+ installedIds: string[]
+ primaryIntegrationId: string
+ secondaryIntegrationIds: string[]
+}
+
+export const MarketplaceFeaturedHeroGrid = ({
+ integrations,
+ installedIds,
+ primaryIntegrationId,
+ secondaryIntegrationIds,
+}: MarketplaceFeaturedHeroGridProps) => {
+ const { ref } = useParams()
+ const primaryIntegration = integrations.find((i) => i.id === primaryIntegrationId)
+ const secondaryIntegrations = secondaryIntegrationIds
+ .slice(0, 2)
+ .map((id) => integrations.find((i) => i.id === id))
+ .filter((i) => i !== undefined) as IntegrationDefinition[]
+
+ if (!primaryIntegration) return null
+
+ const primaryImage = getIntegrationImage(primaryIntegration)
+ const primarySource = getMarketplaceSource(primaryIntegration)
+ const primaryInstalled = installedIds.includes(primaryIntegration.id)
+
+ return (
+
+
+
Featured integrations
+
+
+
+
+
+
+
+
+ {primaryInstalled && Installed }
+
+
+
+ {primaryIntegration.name}
+
+ {primaryIntegration.description && (
+
+ {primaryIntegration.description}
+
+ )}
+
+ Built by
+ {primaryIntegration.author?.name}
+
+
+
+
+
+
+ {primaryIntegration.status && (
+ {primaryIntegration.status}
+ )}
+
+
+
+
+ {primaryImage ? (
+
+ ) : (
+
+ {primaryIntegration.icon({ className: 'w-10 h-10 text-foreground' })}
+
+ )}
+
+
+
+
+
+
+ {secondaryIntegrations.map((integration) => {
+ const image = getIntegrationImage(integration)
+ const source = getMarketplaceSource(integration)
+ const isInstalled = installedIds.includes(integration.id)
+
+ return (
+
+
+
+
+ {image ? (
+
+ ) : (
+
+ {integration.icon({ className: 'w-8 h-8 text-foreground' })}
+
+ )}
+
+
+
+
+ {isInstalled && Installed }
+
+
+
{integration.name}
+ {integration.description && (
+
+ {integration.description}
+
+ )}
+
+
+
+
+
+ {integration.status && (
+ {integration.status}
+ )}
+
+
+ Built by
+ {integration.author?.name}
+
+
+
+
+
+
+ )
+ })}
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceIndex.tsx b/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceIndex.tsx
index dbfb503b907..eded2dcfa2d 100644
--- a/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceIndex.tsx
+++ b/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceIndex.tsx
@@ -23,7 +23,7 @@ import {
type MarketplaceSource,
} from './Marketplace.constants'
import { MarketplaceCard } from './MarketplaceCard'
-import { MarketplaceFeaturedHero } from './MarketplaceFeaturedHero'
+import { MarketplaceFeaturedHeroGrid } from './MarketplaceFeaturedHeroGrid'
import { MarketplaceFilterBar, type ViewMode } from './MarketplaceFilterBar'
import { MarketplaceListHeader, MarketplaceListRow } from './MarketplaceListRow'
import { IntegrationLoadingCard } from '@/components/interfaces/Integrations/Landing/IntegrationCard'
@@ -208,14 +208,13 @@ export const MarketplaceIndex = () => {
const activeFilters = [category, type, source].filter(Boolean)
const pageTitle = useMemo(() => {
- if (activeFilters.length !== 1) return 'Integrations Marketplace'
- if (category)
- return `Integrations Marketplace: ${formatCategoryLabel(category, categoryOptions)}`
+ if (activeFilters.length !== 1) return 'Extend your database'
+ if (category) return `Integrations: ${formatCategoryLabel(category, categoryOptions)}`
if (type)
- return `Integrations Marketplace: ${INTEGRATION_TYPES.find((t) => t.key === type)?.label ?? type}s`
+ return `Integrations: ${INTEGRATION_TYPES.find((t) => t.key === type)?.label ?? type}s`
if (source)
- return `Integrations Marketplace: ${MARKETPLACE_SOURCES.find((s) => s.key === source)?.label ?? source}`
- return 'Integrations Marketplace'
+ return `Integrations: ${MARKETPLACE_SOURCES.find((s) => s.key === source)?.label ?? source}`
+ return 'Integrations'
}, [activeFilters.length, category, type, source, categoryOptions])
return (
@@ -239,7 +238,7 @@ export const MarketplaceIndex = () => {
{isLoading && (
{Array.from({ length: 8 }).map((_, idx) => (
-
+
))}
)}
@@ -251,10 +250,11 @@ export const MarketplaceIndex = () => {
{isSuccess && (
<>
{featured.length > 0 && (
- i.id)}
/>
)}
diff --git a/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceSidebar.tsx b/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceSidebar.tsx
index bac1a6e98a2..5ffbcf011ba 100644
--- a/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceSidebar.tsx
+++ b/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceSidebar.tsx
@@ -70,7 +70,7 @@ export const MarketplaceSidebar = () => {
href={baseHref}
active={isDiscoverActive}
icon={ }
- label="Marketplace"
+ label="Explore all"
/>
{isLoading ? (
diff --git a/apps/studio/pages/project/[ref]/integrations/[id]/index.tsx b/apps/studio/pages/project/[ref]/integrations/[id]/index.tsx
index c71e4a6e142..7f1980fa16c 100644
--- a/apps/studio/pages/project/[ref]/integrations/[id]/index.tsx
+++ b/apps/studio/pages/project/[ref]/integrations/[id]/index.tsx
@@ -1,4 +1,4 @@
-import { useParams } from 'common'
+import { useFlag, useParams } from 'common'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { PageContainer } from 'ui-patterns/PageContainer'
@@ -9,16 +9,40 @@ import { DefaultLayout } from '@/components/layouts/DefaultLayout'
import { ProjectIntegrationsLayoutDispatch } from '@/components/layouts/ProjectIntegrationsLayoutDispatch'
import type { NextPageWithLayout } from '@/types'
+const INTEGRATION_FLAGS: Record = {
+ grafana: 'grafanaDashboardIntegrationEnabled',
+ resend: 'resendDashboardIntegrationEnabled',
+ aikido: 'aikidoDashboardIntegrationEnabled',
+ doppler: 'dopplerDashboardIntegrationEnabled',
+}
+
const IntegrationPage: NextPageWithLayout = () => {
const router = useRouter()
const { ref, id } = useParams()
+ const grafanaEnabled = useFlag('grafanaDashboardIntegrationEnabled')
+ const resendEnabled = useFlag('resendDashboardIntegrationEnabled')
+ const aikidoEnabled = useFlag('aikidoDashboardIntegrationEnabled')
+ const dopplerEnabled = useFlag('dopplerDashboardIntegrationEnabled')
+
+ const integrationFlagValues: Record = {
+ grafana: grafanaEnabled,
+ resend: resendEnabled,
+ aikido: aikidoEnabled,
+ doppler: dopplerEnabled,
+ }
+
useEffect(() => {
- // Always redirect to the overview page since this route should not render content
- if (router?.isReady) {
- router.replace(`/project/${ref}/integrations/${id}/overview`)
+ if (!router?.isReady) return
+
+ const flagName = id ? INTEGRATION_FLAGS[id] : undefined
+ if (flagName !== undefined && integrationFlagValues[id!] === false) {
+ router.replace(`/project/${ref}/integrations`)
+ return
}
- }, [router, ref, id])
+
+ router.replace(`/project/${ref}/integrations/${id}/overview`)
+ }, [router, ref, id, grafanaEnabled, resendEnabled, aikidoEnabled, dopplerEnabled])
return (
diff --git a/apps/studio/public/img/integrations/covers/grafana-cover.png b/apps/studio/public/img/integrations/covers/grafana-cover.png
new file mode 100644
index 00000000000..43a0c31dda1
Binary files /dev/null and b/apps/studio/public/img/integrations/covers/grafana-cover.png differ