feat(studio): dashboard integrations update (#46671)

- remove "Marketplace" branding and naming from integrations section
- remove "featured partners" hero with multiple partners
- keep only _one_ featured partner to start with (Grafana)
- gate each partner integration with separate flags to enable
testing/releasing them separately

## Before
<img width="1487" height="834" alt="Screenshot 2026-06-08 at 11 00 19"
src="https://github.com/user-attachments/assets/9359a98f-18f8-4b1e-be23-63646cf66106"
/>

## After
<img width="1494" height="849" alt="Screenshot 2026-06-08 at 11 03 57"
src="https://github.com/user-attachments/assets/2c28cf36-97f0-4bbd-8c97-7998768e6ce6"
/>
This commit is contained in:
Francesco Sansalvadore authored and GitHub committed 2026-06-09 15:30:42 +02:00
1 parent ff34a6753c
commit f622bc6ea0
12 files changed
+252 -46

No files matched your search

@@ -25,8 +25,8 @@ import {
import { AdvisorRulesPreview } from './AdvisorRulesPreview'
import { CLSPreview } from './CLSPreview'
import { useFeaturePreviewContext, useFeaturePreviewModal } from './FeaturePreviewContext'
import { IntegrationsLayoutPreview } from './IntegrationsLayoutPreview'
import { JitDbAccessPreview } from './JitDbAccessPreview'
import { MarketplacePreview } from './MarketplacePreview'
import { PgDeltaDiffPreview } from './PgDeltaDiffPreview'
import { PlatformWebhooksPreview } from './PlatformWebhooksPreview'
import { RLSTesterPreview } from './RLSTesterPreview'
@@ -47,7 +47,7 @@ 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_RLS_TESTER]: <RLSTesterPreview />,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE]: <MarketplacePreview />,
[LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE]: <IntegrationsLayoutPreview />,
}
export const FeaturePreviewModal = () => {
@@ -152,7 +152,7 @@ export const FeaturePreviewModal = () => {
</SelectContent>
</Select>
</div>
<div className="h-auto min-h-0 max-h-auto md:max-h-[550px] p-4 pb-0 flex flex-col">
<div className="w-full h-auto min-h-0 max-h-auto md:max-h-[550px] p-4 pb-0 flex flex-col">
<div className="flex items-center justify-between border-b gap-2 pb-3">
<p>{selectedFeature?.name}</p>
<div className="flex items-center gap-x-2">
@@ -0,0 +1,7 @@
export const IntegrationsLayoutPreview = () => (
<div>
<p className="text-sm text-foreground-light">
Browse and install integrations from a new layout with improved filtering and search.
</p>
</div>
)
@@ -1,12 +0,0 @@
export const MarketplacePreview = () => (
<div>
<p className="text-sm text-foreground-light mb-4">
Browse and install integrations from a new Marketplace layout with improved filtering and
search.
</p>
<p className="text-sm text-foreground-light">
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.
</p>
</div>
)
@@ -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,
@@ -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"
/>
) : (
@@ -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<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 {
data: dataWithMarketplace,
@@ -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'
@@ -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<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`,
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 (
<section className="@container">
<div className="mb-2">
<h2 className="text-sm">Featured integrations</h2>
</div>
<div className="grid grid-cols-1 @lg:grid-cols-2 @3xl:grid-cols-4 gap-3 items-stretch">
<div className="col-span-1 @lg:col-span-2">
<Link
href={`/project/${ref}/integrations/${primaryIntegration.id}/overview`}
className="block h-full rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground-lighter focus-visible:ring-offset-1 focus-visible:ring-offset-background"
>
<Card className="relative flex flex-row overflow-hidden h-full min-h-[168px] hover:border-stronger">
<div className="relative z-10 flex flex-col gap-2.5 p-4 flex-1 min-w-0">
<div className="flex items-start justify-between">
<IntegrationLogo integration={primaryIntegration} />
{primaryInstalled && <Badge variant="success">Installed</Badge>}
</div>
<div className="@lg:max-w-2/3">
<div className="mb-1 text-sm font-medium text-pretty">
{primaryIntegration.name}
</div>
{primaryIntegration.description && (
<p className="line-clamp-3 text-xs leading-snug text-foreground-light text-balance">
{primaryIntegration.description}
</p>
)}
<div className="text-xs flex items-center gap-1 text-foreground-lighter shrink-0 mt-4">
<span>Built by</span>
<span>{primaryIntegration.author?.name}</span>
</div>
</div>
<div className="flex-1" />
<div className="flex items-center justify-between gap-2 pt-2.5">
<div className="flex flex-wrap items-center gap-1">
<MarketplaceSourceBadge source={primarySource} />
{primaryIntegration.status && (
<Badge variant="warning">{primaryIntegration.status}</Badge>
)}
</div>
</div>
</div>
<div className="absolute inset-0 left-auto w-2/5 @lg:w-3/5 shrink-0 bg-surface-400">
{primaryImage ? (
<Image
fill
src={primaryImage}
alt={`${primaryIntegration.name} integration`}
className="object-cover"
/>
) : (
<div className="flex h-full items-center justify-center">
{primaryIntegration.icon({ className: 'w-10 h-10 text-foreground' })}
</div>
)}
<div className="absolute inset-0 bg-linear-to-r from-surface-100 to-transparent" />
</div>
</Card>
</Link>
</div>
{secondaryIntegrations.map((integration) => {
const image = getIntegrationImage(integration)
const source = getMarketplaceSource(integration)
const isInstalled = installedIds.includes(integration.id)
return (
<div key={integration.id} className="col-span-1">
<Link
href={`/project/${ref}/integrations/${integration.id}/overview`}
className="block h-full rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground-lighter focus-visible:ring-offset-1 focus-visible:ring-offset-background"
>
<Card className="flex flex-col overflow-hidden h-full hover:border-stronger">
<div className="hidden @lg:block relative w-full h-28 bg-alternative shrink-0">
{image ? (
<Image
fill
src={image}
alt={`${integration.name} integration`}
className="object-cover"
/>
) : (
<div className="flex h-full items-center justify-center">
{integration.icon({ className: 'w-8 h-8 text-foreground' })}
</div>
)}
</div>
<div className="flex flex-col gap-2.5 p-4 flex-1">
<div className="flex items-start justify-between">
<IntegrationLogo integration={integration} size="h-9 w-9" />
{isInstalled && <Badge variant="success">Installed</Badge>}
</div>
<div>
<div className="mb-1 text-sm font-medium">{integration.name}</div>
{integration.description && (
<p className="line-clamp-2 text-xs leading-snug text-foreground-light">
{integration.description}
</p>
)}
</div>
<div className="flex-1" />
<div className="flex items-center justify-between gap-2 pt-2.5">
<div className="flex flex-wrap items-center gap-1">
<MarketplaceSourceBadge source={source} />
{integration.status && (
<Badge variant="warning">{integration.status}</Badge>
)}
</div>
<div className="text-xs flex items-center gap-1 text-foreground-lighter">
<span>Built by</span>
<span>{integration.author?.name}</span>
</div>
</div>
</div>
</Card>
</Link>
</div>
)
})}
</div>
</section>
)
}
@@ -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 && (
<div className="grid gap-4 xl:grid-cols-3 2xl:grid-cols-4">
{Array.from({ length: 8 }).map((_, idx) => (
<IntegrationLoadingCard key={`marketplace-loading-${idx}`} />
<IntegrationLoadingCard key={`integrations-loading-${idx}`} />
))}
</div>
)}
@@ -251,10 +250,11 @@ export const MarketplaceIndex = () => {
{isSuccess && (
<>
{featured.length > 0 && (
<MarketplaceFeaturedHero
<MarketplaceFeaturedHeroGrid
integrations={featured}
installedIds={installedIds}
categoryOptions={categoryOptions}
primaryIntegrationId={featured[0].id}
secondaryIntegrationIds={featured.slice(1, 3).map((i) => i.id)}
/>
)}
@@ -70,7 +70,7 @@ export const MarketplaceSidebar = () => {
href={baseHref}
active={isDiscoverActive}
icon={<LayoutGrid size={13} />}
label="Marketplace"
label="Explore all"
/>
{isLoading ? (
@@ -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<string, string> = {
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<string, boolean> = {
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 (
<PageContainer size="full">
Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB