diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx index 857a10a90a4..cf38d45a091 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx @@ -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]: , [LOCAL_STORAGE_KEYS.UI_PREVIEW_JIT_DB_ACCESS]: , [LOCAL_STORAGE_KEYS.UI_PREVIEW_RLS_TESTER]: , - [LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE]: , + [LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE]: , } export const FeaturePreviewModal = () => { @@ -152,7 +152,7 @@ export const FeaturePreviewModal = () => { -
+

{selectedFeature?.name}

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.name} + ) : ( +
+ {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.name} + ) : ( +
+ {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