mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
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:
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"
|
||||
/>
|
||||
) : (
|
||||
|
||||
+24
-5
@@ -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,
|
||||
|
||||
+1
-8
@@ -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'
|
||||
|
||||
|
||||
+175
@@ -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 |
Reference in new issue
Block a user