From b02ad91319cce0921168fcbd714896525dcffd22 Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Wed, 15 Jul 2026 12:24:07 +0200 Subject: [PATCH 01/23] fix(studio): stop unified logs bar chart animation replay on click FE-3912 (#47910) ## Problem In the unified logs dashboard, clicking an activity bar in the chart restarted the fly-by entrance animation instead of selecting that bar. There was also no way to select a single bar by clicking it (only drag-to-select worked), and the selection menu was centered over the selection rather than anchored to its start. ## Fix - Disabled Recharts' `isAnimationActive` on the stacked `Bar` elements so re-renders from selection state no longer replay the entrance animation. - Clicking a single bar now selects that bar's full time bucket and opens the "Filter logs to selected range" menu, the same as dragging across it. - The menu is anchored to the start (leftmost pixel) of the selection instead of the mouse release position. ## How to test - Open a project's unified logs page - Wait for the activity chart to finish its initial load - Click on a single bar - Expected result: the bar is highlighted, the chart does not replay its entrance animation, and the range-filter menu appears anchored at the start of that bar - Drag across multiple bars - Expected result: the range-filter menu appears anchored at the start of the dragged selection, not centered over it ## Summary by CodeRabbit - **Bug Fixes** - Fixed chart highlight popovers to anchor consistently to the start of the selected range. - Improved popover behavior so it updates correctly when the position changes. - Corrected zoom-in filtering when the selected range contains a single timestamp. - **UI Improvements** - Reduced conflicts between tooltips and selection popovers by rendering tooltip content only when appropriate. - Disabled stacked-bar animations for error/warning/success to make chart interactions feel steadier. --- .../ui/Charts/ChartHighlightActions.tsx | 10 ++- .../ui/Charts/useChartHighlight.tsx | 15 +++- .../components/ui/DataTable/TimelineChart.tsx | 79 +++++++++++++------ 3 files changed, 74 insertions(+), 30 deletions(-) diff --git a/apps/studio/components/ui/Charts/ChartHighlightActions.tsx b/apps/studio/components/ui/Charts/ChartHighlightActions.tsx index ddc0460555e09..2184b6ff766e5 100644 --- a/apps/studio/components/ui/Charts/ChartHighlightActions.tsx +++ b/apps/studio/components/ui/Charts/ChartHighlightActions.tsx @@ -48,7 +48,7 @@ export const ChartHighlightActions = ({ const formatChartDate = useFormatDateTime() useEffect(() => { - setIsOpen(!!chartHighlight?.popoverPosition && selectedRangeStart !== selectedRangeEnd) + setIsOpen(!!chartHighlight?.popoverPosition) }, [chartHighlight?.popoverPosition]) const ctx: ChartHighlightActionContext | undefined = @@ -80,8 +80,12 @@ export const ChartHighlightActions = ({ return [...defaultActions, ...provided] }, [defaultActions, actions]) + const positionKey = chartHighlight?.popoverPosition + ? `${chartHighlight.popoverPosition.x}-${chartHighlight.popoverPosition.y}` + : 'closed' + return ( - + clearHighlight?.()} onInteractOutside={(e) => { const target = e.target as Element | null diff --git a/apps/studio/components/ui/Charts/useChartHighlight.tsx b/apps/studio/components/ui/Charts/useChartHighlight.tsx index e4f8593e9afd4..1fa248b8b808b 100644 --- a/apps/studio/components/ui/Charts/useChartHighlight.tsx +++ b/apps/studio/components/ui/Charts/useChartHighlight.tsx @@ -4,8 +4,12 @@ import { useState } from 'react' type ChartHighlightMouseEvent = { activeLabel?: string coordinates?: string + chartX?: number + chartY?: number } +type Pixel = { x: number; y: number } + export interface ChartHighlight { left: string | undefined right: string | undefined @@ -28,6 +32,7 @@ export function useChartHighlight(): ChartHighlight { const [isSelecting, setIsSelecting] = useState(false) const [popoverPosition, setPopoverPosition] = useState<{ x: number; y: number } | null>(null) const [initialPoint, setInitialPoint] = useState(undefined) + const [anchorPixel, setAnchorPixel] = useState(undefined) const handleMouseDown = (e: ChartHighlightMouseEvent) => { clearHighlight() @@ -37,6 +42,9 @@ export function useChartHighlight(): ChartHighlight { setRight(e.activeLabel) setInitialPoint(e.activeLabel) setCoordinates({ left: e.coordinates, right: e.coordinates }) + if (typeof e.chartX === 'number' && typeof e.chartY === 'number') { + setAnchorPixel({ x: e.chartX, y: e.chartY }) + } } const handleMouseMove = (e: ChartHighlightMouseEvent) => { @@ -69,7 +77,11 @@ export function useChartHighlight(): ChartHighlight { setIsSelecting(false) setInitialPoint(undefined) - if ( + // Anchor the popover to where the selection started rather than wherever + // the mouse happened to be released. + if (anchorPixel) { + setPopoverPosition(anchorPixel) + } else if ( typeof e === 'object' && e !== null && 'chartX' in e && @@ -87,6 +99,7 @@ export function useChartHighlight(): ChartHighlight { setCoordinates({ left: undefined, right: undefined }) setPopoverPosition(null) setInitialPoint(undefined) + setAnchorPixel(undefined) } return { diff --git a/apps/studio/components/ui/DataTable/TimelineChart.tsx b/apps/studio/components/ui/DataTable/TimelineChart.tsx index 64524e9cc1d56..3532853b4e387 100644 --- a/apps/studio/components/ui/DataTable/TimelineChart.tsx +++ b/apps/studio/components/ui/DataTable/TimelineChart.tsx @@ -48,8 +48,7 @@ export function TimelineChart({ const { table } = useDataTable() const chartHighlight = useChartHighlight() - const showHighlight = - chartHighlight?.left && chartHighlight?.right && chartHighlight?.left !== chartHighlight?.right + const showHighlight = !!chartHighlight?.left && !!chartHighlight?.right // REMINDER: date has to be a string for tooltip label to work - don't ask me why const chart = useMemo( @@ -69,6 +68,11 @@ export function TimelineChart({ return { interval, period: calculatePeriod(interval) } }, [data]) + const bucketWidthMs = useMemo( + () => (data.length > 1 ? Math.abs(data[1].timestamp - data[0].timestamp) : 0), + [data] + ) + const highlightActions: ChartHighlightAction[] = useMemo( () => [ { @@ -76,7 +80,9 @@ export function TimelineChart({ label: 'Filter logs to selected range', icon: , onSelect: ({ start, end, clear }) => { - const [left, right] = [start, end].sort( + const resolvedEnd = + start === end ? new Date(new Date(start).getTime() + bucketWidthMs).toString() : end + const [left, right] = [start, resolvedEnd].sort( (a, b) => new Date(a).getTime() - new Date(b).getTime() ) table.getColumn(resolvedFilterColumnId)?.setFilterValue([new Date(left), new Date(right)]) @@ -101,13 +107,23 @@ export function TimelineChart({ { + onMouseDown={({ activeLabel, activeTooltipIndex, chartX, chartY }) => { if (activeTooltipIndex === undefined || activeTooltipIndex === null) return - chartHighlight.handleMouseDown({ activeLabel, coordinates: activeLabel }) + chartHighlight.handleMouseDown({ + activeLabel, + coordinates: activeLabel, + chartX, + chartY, + }) }} - onMouseMove={({ activeLabel, activeTooltipIndex }) => { + onMouseMove={({ activeLabel, activeTooltipIndex, chartX, chartY }) => { if (activeTooltipIndex === undefined || activeTooltipIndex === null) return - chartHighlight.handleMouseMove({ activeLabel, coordinates: activeLabel }) + chartHighlight.handleMouseMove({ + activeLabel, + coordinates: activeLabel, + chartX, + chartY, + }) }} onMouseUp={chartHighlight.handleMouseUp} style={{ cursor: 'crosshair' }} @@ -131,26 +147,37 @@ export function TimelineChart({ return format(date, 'LLL dd, y') }} /> - {!chartHighlight.popoverPosition && ( - { - const date = new Date(value) - if (isNaN(date.getTime())) return 'N/A' - if (timerange.period === '10m') { - return format(date, 'LLL dd, HH:mm:ss') - } - return format(date, 'LLL dd, y HH:mm') - }} - /> - } - /> - )} + { + const date = new Date(value) + if (isNaN(date.getTime())) return 'N/A' + if (timerange.period === '10m') { + return format(date, 'LLL dd, HH:mm:ss') + } + return format(date, 'LLL dd, y HH:mm') + }} + /> + } + /> {/* TODO: we could use the `{timestamp, ...rest} = data[0]` to dynamically create the bars but that would mean the order can be very much random */} - - - + + + {showHighlight && ( Date: Wed, 15 Jul 2026 19:01:29 +0800 Subject: [PATCH 02/23] chore: remove dead telemetry code (#47950) ## Summary Removes two pieces of dead telemetry code found while root-causing the docs pageview re-fire investigation (GROWTH-997, closed with no fix needed). Pure deletion, 30 lines, no behavior change. ## Changes - Delete `apps/www/app/ConsentWrapper.tsx`: an unwired third `PageTelemetry` mount. www already mounts `PageTelemetry` in `pages/_app.tsx` (Pages Router) and `app/providers.tsx` (App Router); nothing imports this wrapper. - Remove the exported `POSTHOG_URL` constant from `apps/studio/lib/constants/index.ts`: zero consumers. The CSP allowlist in `apps/studio/csp.ts` defines and uses its own local `POSTHOG_URL`, which stays. ## Testing Deadness verified before deletion, on current master: - [x] Repo-wide grep for `ConsentWrapper`: only self-references inside the deleted file - [x] Repo-wide grep for `POSTHOG_URL`: remaining references are the `csp.ts` local const only ## Linear - fixes GROWTH-1002 ## Summary by CodeRabbit * **Bug Fixes** * Removed an obsolete consent-related page wrapper to streamline page behavior. * Updated application configuration handling without changing existing payment or usage settings. * **Refactor** * Simplified internal configuration and page composition while preserving the existing user experience. --- apps/studio/lib/constants/index.ts | 6 ------ apps/www/app/ConsentWrapper.tsx | 24 ------------------------ 2 files changed, 30 deletions(-) delete mode 100644 apps/www/app/ConsentWrapper.tsx diff --git a/apps/studio/lib/constants/index.ts b/apps/studio/lib/constants/index.ts index 2dd90f2652da5..b177951c71619 100644 --- a/apps/studio/lib/constants/index.ts +++ b/apps/studio/lib/constants/index.ts @@ -59,12 +59,6 @@ export const GOTRUE_ERRORS = { export const STRIPE_PUBLIC_KEY = process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY || 'pk_test_XVwg5IZH3I9Gti98hZw6KRzd00v5858heG' -export const POSTHOG_URL = - process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging' || - process.env.NEXT_PUBLIC_ENVIRONMENT === 'local' - ? 'https://ph.supabase.green' - : 'https://ph.supabase.com' - export const USAGE_APPROACHING_THRESHOLD = 0.75 export const DOCS_URL = process.env.NEXT_PUBLIC_DOCS_URL || 'https://supabase.com/docs' diff --git a/apps/www/app/ConsentWrapper.tsx b/apps/www/app/ConsentWrapper.tsx deleted file mode 100644 index 595ad4a0bad49..0000000000000 --- a/apps/www/app/ConsentWrapper.tsx +++ /dev/null @@ -1,24 +0,0 @@ -'use client' - -import { IS_PLATFORM, PageTelemetry } from 'common' -import { useConsentToast } from 'ui-patterns/consent' -import { API_URL } from '~/lib/constants' - -interface ConsentWrapperProps { - children: React.ReactNode -} - -export function ConsentWrapper({ children }: ConsentWrapperProps) { - const { hasAcceptedConsent } = useConsentToast() - - return ( - <> - {children} - - - ) -} From d23f86021a899575b764c1e57dbc55d85fd6d4f4 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Wed, 15 Jul 2026 13:10:51 +0200 Subject: [PATCH 03/23] feat(www): Partner Catalog update (#46757) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Info architecture change around "Partners" The www "integrations" now become more partner-driven. `/partners/integrations` -> now Partner Catalog under `/partners/catalog` (old links redirect to new paths) Moved them close together in the nav dropdown and in the footer Screenshot 2026-07-09 at 11 06 41 Screenshot 2026-07-09 at 11 09 48 ## /partners This page remains untouched in this PR, updates to layout, content and intake form are delegated to #47874 ## /partners/catalog Listed in the [catalog](https://zone-www-dot-com-git-feat-www-partners-pages-supabase.vercel.app/partners/catalog) are now partners. Some partners match with a listing. Screenshot 2026-07-09 at 11 14 17 ## /partners/catalog/[partner] Each partner can have one or more "listings" which are either - simple guides - foreign data wrappers - dashboard integrations Integrations available in the dashboard now all have a prominent "Install integration" cta to open it in the dashboard [integrations page](https://supabase.com/dashboard/project/_/integrations). Screenshot 2026-07-09 at 11 16 51 ## Docs Update docs → [Preview](https://docs-git-feat-www-partners-pages-supabase.vercel.app/docs/guides/integrations) - remove "Supabase marketplace" - use "Dashboard Integrations and Partner Catalog - update integrations in sidenav to link to updated /partners/catalog/** listings Screenshot 2026-07-15 at 12 54 47 ## Summary by CodeRabbit * **New Features** * Added a Partner Catalog experience with search, category filters, official-partner toggle, responsive filtering (sidebar + bottom sheet), grid/list views, and featured partners. * Added Partner Catalog detail pages with tabbed listings, MDX-rendered content, image gallery with zoom overlay, and “add/install” actions. * **Improvements** * Updated “Become a Partner” layout and form support for prefilled values and checkbox-group fields (including validation). * Updated navigation/footer/docs and partner tile links to use Partner Catalog routes; expanded redirects from legacy integrations paths. * Added public agent-skills discovery manifest. --------- Co-authored-by: Alan Daniel Co-authored-by: Alex Hall Co-authored-by: Miranda Limonczenko --- apps/docs/.env.development | 12 +- apps/docs/app/guides/integrations/layout.tsx | 93 +++- .../Navigation/Navigation.commands.tsx | 4 +- .../NavigationMenu.constants.ts | 12 +- apps/docs/content/guides/integrations.mdx | 12 +- ...se-marketplace.mdx => partner-catalog.mdx} | 11 +- apps/docs/lib/supabaseMisc.ts | 14 - apps/docs/turbo.jsonc | 2 + .../Marketplace/Marketplace.constants.tsx | 54 +- .../Marketplace/MarketplaceIndex.tsx | 2 +- apps/www/app/partners/[slug]/page.tsx | 21 + .../partners/catalog/IntegrationsContent.tsx | 458 ++++++++++++++++ .../catalog/[slug]/PartnerCatalogDetail.tsx | 409 ++++++++++++++ apps/www/app/partners/catalog/[slug]/page.tsx | 87 +++ apps/www/app/partners/catalog/page.tsx | 36 ++ apps/www/app/providers.tsx | 47 +- .../www/components/Layouts/SectionHeading.tsx | 36 ++ apps/www/components/Panel/Panel.tsx | 2 +- .../components/Partners/BecomeAPartner.tsx | 17 - apps/www/components/Partners/TileGrid.tsx | 6 +- .../Sections/ProductHeaderCentered.tsx | 57 +- apps/www/data/Developers.tsx | 20 +- apps/www/data/Footer.ts | 4 +- apps/www/data/partners/index.tsx | 4 + apps/www/internals/generate-sitemap.mjs | 2 + apps/www/lib/marketplaceDb.ts | 518 +++++++++++++++--- apps/www/lib/redirects.js | 118 ++-- apps/www/pages/partners/[slug].tsx | 39 -- apps/www/pages/partners/index.tsx | 4 +- .../pages/partners/integrations/[slug].tsx | 356 ------------ .../www/pages/partners/integrations/index.tsx | 193 ------- .../.well-known/agent-skills/index.json | 10 +- apps/www/types/partners.ts | 17 + packages/common/marketplace-categories.ts | 53 ++ packages/common/marketplace-client.ts | 22 +- packages/common/marketplace.types.ts | 33 ++ packages/common/package.json | 2 + .../marketing/src/forms/MarketingForm.tsx | 1 - packages/marketing/src/go/schemas.ts | 11 + pnpm-lock.yaml | 3 + supa-mdx-lint/Rule001HeadingCase.toml | 2 + 41 files changed, 1887 insertions(+), 917 deletions(-) rename apps/docs/content/guides/integrations/{supabase-marketplace.mdx => partner-catalog.mdx} (71%) delete mode 100644 apps/docs/lib/supabaseMisc.ts create mode 100644 apps/www/app/partners/[slug]/page.tsx create mode 100644 apps/www/app/partners/catalog/IntegrationsContent.tsx create mode 100644 apps/www/app/partners/catalog/[slug]/PartnerCatalogDetail.tsx create mode 100644 apps/www/app/partners/catalog/[slug]/page.tsx create mode 100644 apps/www/app/partners/catalog/page.tsx create mode 100644 apps/www/components/Layouts/SectionHeading.tsx delete mode 100644 apps/www/components/Partners/BecomeAPartner.tsx delete mode 100644 apps/www/pages/partners/[slug].tsx delete mode 100644 apps/www/pages/partners/integrations/[slug].tsx delete mode 100644 apps/www/pages/partners/integrations/index.tsx create mode 100644 packages/common/marketplace-categories.ts diff --git a/apps/docs/.env.development b/apps/docs/.env.development index 56120419134e7..d10a8699d76fa 100644 --- a/apps/docs/.env.development +++ b/apps/docs/.env.development @@ -7,16 +7,20 @@ NEXT_PUBLIC_BASE_PATH="/docs" # Whether to enable features available only on Supabase's own hosted services # Setting this to true requires certain secret keys -NEXT_PUBLIC_IS_PLATFORM="false" +NEXT_PUBLIC_IS_PLATFORM="true" # Base URL for the hosted MCP server. Overrides the DEFAULT_MCP_URL_PLATFORM # fallback in @ui-patterns/McpUrlBuilder. NEXT_PUBLIC_MCP_URL="http://localhost:8080/mcp" -# Supabase project containing integration information +# Supabase project containing integration information (legacy) NEXT_PUBLIC_MISC_URL="https://obuldanrptloktxcffvn.supabase.co" NEXT_PUBLIC_MISC_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTg2MTQ2ODUsImV4cCI6MjAzNDE5MDY4NX0.NFt49g6DFkc1X5khCzN5p01iAVo2TMxlx88cY1V0E2M" +# Supabase project backing the Partner Catalog / Dashboard Integrations Marketplace +NEXT_PUBLIC_MARKETPLACE_API_URL="https://fgxbxpvumhvzrhqngsyu.supabase.co" +NEXT_PUBLIC_MARKETPLACE_PUBLISHABLE_KEY="sb_publishable_VuF5ZvGqj6ODhZgN1J_vMw_YbiEs1R6" + # Supabase project containing docs content information -NEXT_PUBLIC_SUPABASE_URL="https://xguihxuzqibwxjnimxev.supabase.co" -NEXT_PUBLIC_SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhndWloeHV6cWlid3hqbmlteGV2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTgzNzc1MTgsImV4cCI6MjAzMzk1MzUxOH0.aIqjQ9V7djMxYit-DT1fYNV_VWMHSqldh_18XfX2_BE" +NEXT_PUBLIC_MARKETPLACE_API_URL="https://otqhrpbxhxkrhrnjqbba.supabase.co/" +NEXT_PUBLIC_MARKETPLACE_PUBLISHABLE_KEY="sb_publishable_ZVVKKu1s88KsSBWVYlou-g_phb2OJVQ" diff --git a/apps/docs/app/guides/integrations/layout.tsx b/apps/docs/app/guides/integrations/layout.tsx index c319779007f32..efd9c36710be8 100644 --- a/apps/docs/app/guides/integrations/layout.tsx +++ b/apps/docs/app/guides/integrations/layout.tsx @@ -1,9 +1,14 @@ -import { IS_PLATFORM } from 'common' -import { unstable_cache } from 'next/cache' import { type NavMenuSection } from '~/components/Navigation/Navigation.types' import { REVALIDATION_TAGS } from '~/features/helpers.fetch' import Layout from '~/layouts/guides' -import { supabaseMisc } from '~/lib/supabaseMisc' +import { IS_PLATFORM } from 'common' +import { + createMarketplaceClient, + SUPABASE_LISTING_OVERRIDES, + SUPABASE_PARTNER_SLUG, + type CatalogListing, +} from 'common/marketplace-client' +import { unstable_cache } from 'next/cache' export default async function IntegrationsLayout({ children }: { children: React.ReactNode }) { const additionalNavItems = { integrations: await getPartners() } @@ -16,26 +21,82 @@ export default async function IntegrationsLayout({ children }: { children: React const getPartners = unstable_cache(getPartnersImpl, [], { tags: [REVALIDATION_TAGS.PARTNERS], }) + async function getPartnersImpl() { if (!IS_PLATFORM) return [] - const { data, error } = await supabaseMisc() - .from('partners') - .select('slug, title') - .eq('approved', true) - .eq('type', 'technology') - .order('title') + const marketplaceClient = createMarketplaceClient() + + const [{ data: partners, error }, { data: listings }] = await Promise.all([ + marketplaceClient.from('partners').select('slug, name, type'), + marketplaceClient.from('catalog_listings').select('*'), + ]) if (error) { console.error(new Error('Error fetching partners', { cause: error })) } - const partnerNavItems = (data ?? []).map( - (partner) => - ({ - name: partner.title, - url: `https://supabase.com/partners/integrations/${partner.slug}` as `https://${string}`, - }) as Partial - ) + // Only "technology" partners belong in this list, not agencies/experts. + const technologyPartnerSlugs = new Set() + const partnerNameBySlug = new Map() + for (const partner of partners ?? []) { + if (!partner.slug) continue + partnerNameBySlug.set(partner.slug, partner.name ?? partner.slug) + if (partner.type === 'technology') technologyPartnerSlugs.add(partner.slug) + } + + // Supabase-owned listings still get their own nav item, at their clean catalog page slug. + const supabaseOwnedNavItems: Partial[] = [] + const listingsByPartnerSlug = new Map() + + for (const listing of listings ?? []) { + if (!listing.slug) continue + + const override = SUPABASE_LISTING_OVERRIDES[listing.slug] + if (override) { + supabaseOwnedNavItems.push({ + name: listing.title || override.name, + url: `https://supabase.com/partners/catalog/${override.slug}` as `https://${string}`, + }) + continue + } + + const partnerSlug = listing.partner_slug + if ( + !partnerSlug || + partnerSlug === SUPABASE_PARTNER_SLUG || + !technologyPartnerSlugs.has(partnerSlug) + ) { + continue + } + + const partnerListings = listingsByPartnerSlug.get(partnerSlug) ?? [] + partnerListings.push(listing) + listingsByPartnerSlug.set(partnerSlug, partnerListings) + } + + // One nav item per listing (a partner with multiple listings — e.g. an FDW plus a + // Dashboard Integration — gets one entry per listing), linking straight to the tab + // that listing occupies on its Partner Catalog page. + const partnerNavItems: Partial[] = [ + ...Array.from(listingsByPartnerSlug.entries()).flatMap(([partnerSlug, partnerListings]) => { + const partnerName = partnerNameBySlug.get(partnerSlug) ?? partnerSlug + const catalogUrl = `https://supabase.com/partners/catalog/${partnerSlug}` + + // Show only listing name if partner has one listing + if (partnerListings.length === 1) { + const listingName = partnerListings[0].title || partnerName + return [{ name: listingName, url: catalogUrl as `https://${string}` }] + } + + // otherwise show listing name (or slug if no title) plus partner name. + return partnerListings.map((listing) => ({ + name: `${listing.title || listing.slug} (${partnerName})`, + url: `${catalogUrl}?tab=${listing.slug}` as `https://${string}`, + })) + }), + ...supabaseOwnedNavItems, + ] + partnerNavItems.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? '')) return partnerNavItems } diff --git a/apps/docs/components/Navigation/Navigation.commands.tsx b/apps/docs/components/Navigation/Navigation.commands.tsx index 2a97ee8fc8cd2..0a00c936c9945 100644 --- a/apps/docs/components/Navigation/Navigation.commands.tsx +++ b/apps/docs/components/Navigation/Navigation.commands.tsx @@ -146,8 +146,8 @@ const navCommands = [ }, { id: 'nav-integrations', - name: 'Go to Integrations', - route: 'https://supabase.com/partners/integrations', + name: 'Go to Partner Catalog', + route: 'https://supabase.com/partners/catalog', icon: () => , enabled: isFeatureEnabled('integrations:partners'), }, diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index d341727d4de69..2c1ee5514fde2 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -3233,12 +3233,16 @@ export const integrations: NavMenuConstant = { url: '/guides/integrations', }, { - name: 'Vercel Marketplace', - url: '/guides/integrations/vercel-marketplace', + name: 'Dashboard Integrations', + url: '/guides/integrations#dashboard-integrations', + }, + { + name: 'Partner Catalog', + url: '/guides/integrations/partner-catalog', }, { - name: 'Supabase Marketplace', - url: '/guides/integrations/supabase-marketplace', + name: 'Vercel Marketplace', + url: '/guides/integrations/vercel-marketplace', }, { name: 'Build Your Own', diff --git a/apps/docs/content/guides/integrations.mdx b/apps/docs/content/guides/integrations.mdx index 579b0fdcd36da..8b0144d761001 100644 --- a/apps/docs/content/guides/integrations.mdx +++ b/apps/docs/content/guides/integrations.mdx @@ -4,10 +4,14 @@ title: Integrations Supabase integrates with many of your favorite third-party services. -## Vercel Marketplace +## Dashboard Integrations -Create and manage your Supabase projects directly through Vercel. [Get started with Vercel](/docs/guides/integrations/vercel-marketplace). +Install and manage extensions, wrappers, and Postgres Modules directly into your project in a couple of clicks. [Browse Dashboard Integrations](/dashboard/project/_/integrations) + +## Partner Catalog -## Supabase Marketplace +Browse a curated list of Partners who offer one-click integrations, guides or other ways to extend your Supabase project. [Browse the Partner Catalog](/partners/catalog) -Browse tools for extending your Supabase project. [Browse the Supabase Marketplace](/partners/integrations). +## Vercel Marketplace + +Create and manage your Supabase projects directly through Vercel. [Get started with Vercel](/docs/guides/integrations/vercel-marketplace). diff --git a/apps/docs/content/guides/integrations/supabase-marketplace.mdx b/apps/docs/content/guides/integrations/partner-catalog.mdx similarity index 71% rename from apps/docs/content/guides/integrations/supabase-marketplace.mdx rename to apps/docs/content/guides/integrations/partner-catalog.mdx index b196fca240f38..e6e59da9cfb6d 100644 --- a/apps/docs/content/guides/integrations/supabase-marketplace.mdx +++ b/apps/docs/content/guides/integrations/partner-catalog.mdx @@ -1,13 +1,12 @@ --- id: 'integrations' -title: 'Supabase Marketplace' +title: 'Partner Catalog' description: 'Integrations and Partners' --- -The Supabase Marketplace brings together all the tools you need to extend your Supabase project. This includes: +The [Partner Catalog](/partners/catalog) is Supabase's public directory of third-party integrations. It lists tools that extend your Supabase project. These tools cover Auth, Caching, Hosting, and Low-code categories. -- [Experts](/partners/experts) - partners to help you build and support your Supabase project. -- [Integrations](/partners/integrations) - extend your projects with external Auth, Caching, Hosting, and Low-code tools. +The Partner Catalog is different from [Dashboard Integrations](/guides/integrations#dashboard-integrations). You install Dashboard Integrations directly from a project in the Supabase Dashboard. ## Build an integration @@ -20,12 +19,12 @@ Supabase provides several integration points: ## List your integration -[Apply to the Partners program](/partners/integrations#become-a-partner) to list your integration in the Partners marketplace and in the Supabase docs. +[Apply to the Partners program](/partners/catalog#become-a-partner) to list your integration in the Partner Catalog and in the Supabase docs. Integrations are assessed on the following criteria: - **Business viability** - While we welcome everyone to built an integration, we only list companies that are deemed to be long-term viable. This includes an official business registration and bank account, meaningful revenue, or Venture Capital backing. We require this criteria to ensure the health of the marketplace. + While we welcome everyone to built an integration, we only list companies that are deemed to be long-term viable. This includes an official business registration and bank account, meaningful revenue, or Venture Capital backing. We require this criteria to ensure the health of the catalog. - **Compliance** Integrations should not infringe on the Supabase brand/trademark. In short, you cannot use "Supabase" in the name. As the listing appears on the Supabase domain, we don't want to mislead developers into thinking that an integration is an official product. - **Service Level Agreements** diff --git a/apps/docs/lib/supabaseMisc.ts b/apps/docs/lib/supabaseMisc.ts deleted file mode 100644 index 2c70c480b9dd2..0000000000000 --- a/apps/docs/lib/supabaseMisc.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { createClient, type SupabaseClient } from '@supabase/supabase-js' - -let _supabaseMisc: SupabaseClient - -export function supabaseMisc() { - if (!_supabaseMisc) { - _supabaseMisc = createClient( - process.env.NEXT_PUBLIC_MISC_URL!, - process.env.NEXT_PUBLIC_MISC_ANON_KEY! - ) - } - - return _supabaseMisc -} diff --git a/apps/docs/turbo.jsonc b/apps/docs/turbo.jsonc index 6ffd9e5257a6c..f46ce1cc5df78 100644 --- a/apps/docs/turbo.jsonc +++ b/apps/docs/turbo.jsonc @@ -24,6 +24,8 @@ "NEXT_PUBLIC_SUPABASE_ANON_KEY", "NEXT_PUBLIC_MISC_URL", "NEXT_PUBLIC_MISC_ANON_KEY", + "NEXT_PUBLIC_MARKETPLACE_API_URL", + "NEXT_PUBLIC_MARKETPLACE_PUBLISHABLE_KEY", "NODE_ENV", "NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_BASE_PATH", diff --git a/apps/studio/components/interfaces/Integrations/Marketplace/Marketplace.constants.tsx b/apps/studio/components/interfaces/Integrations/Marketplace/Marketplace.constants.tsx index e80fd564f0f4d..2ae515958f8f5 100644 --- a/apps/studio/components/interfaces/Integrations/Marketplace/Marketplace.constants.tsx +++ b/apps/studio/components/interfaces/Integrations/Marketplace/Marketplace.constants.tsx @@ -1,26 +1,4 @@ -import { - BadgeCheck, - BarChart3, - Boxes, - Cable, - Cpu, - CreditCard, - Database, - Fingerprint, - KeyRound, - Mail, - MessageSquare, - MousePointerClick, - Package2, - Plug, - Server, - ShieldCheck, - Users, - Webhook, - Wrench, - Zap, - type LucideIcon, -} from 'lucide-react' +import { BadgeCheck, Cable, Database, Package2, Plug, Users, type LucideIcon } from 'lucide-react' import type { ReactNode } from 'react' import { Badge, cn, IconPartners } from 'ui' @@ -29,6 +7,8 @@ import type { MarketplaceSource, } from '@/components/interfaces/Integrations/Landing/Integrations.constants' +export { getCategoryIcon, CATEGORY_ICONS } from 'common/marketplace-categories' + export type { MarketplaceSource } from '@/components/interfaces/Integrations/Landing/Integrations.constants' // Defines featured integrations and their order in the featured hero @@ -47,29 +27,6 @@ export const INTEGRATION_TYPES: Array<{ { key: 'wrapper', label: 'Wrapper', icon: Cable }, ] -// Lucide icon per marketplace category slug. New categories fall back to a -// neutral icon (Boxes) so nothing breaks when the marketplaceDB is updated. -export const CATEGORY_ICONS: Record = { - observability: BarChart3, - security: ShieldCheck, - billing: CreditCard, - secrets: KeyRound, - email: Mail, - wrappers: Database, - ai: Cpu, - ai_vectors: Cpu, - storage: Package2, - postgres_extension: Database, - wrapper: Cable, - devtools: Wrench, - auth: Fingerprint, - 'low-code': MousePointerClick, - 'data-platform': Server, - api: Webhook, - 'caching-offline-first': Zap, - messaging: MessageSquare, -} - export const FEATURED_CATEGORIES: Array<{ slug: string; name: string }> = [ { slug: 'observability', name: 'Observability' }, { slug: 'security', name: 'Security' }, @@ -87,11 +44,6 @@ export const EXCLUDED_CATEGORY_SLUGS = new Set([ 'app-templates', ]) -export const getCategoryIcon = (slug: string | null | undefined): LucideIcon => { - if (!slug) return Boxes - return CATEGORY_ICONS[slug] ?? Boxes -} - export const formatCategoryLabel = ( slug: string | null | undefined, categoryOptions?: Array<{ slug: string; name: string }> diff --git a/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceIndex.tsx b/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceIndex.tsx index db457a626566d..2314874e1dca8 100644 --- a/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceIndex.tsx +++ b/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceIndex.tsx @@ -229,7 +229,7 @@ export const MarketplaceIndex = () => {
- +
diff --git a/apps/www/app/partners/[slug]/page.tsx b/apps/www/app/partners/[slug]/page.tsx new file mode 100644 index 0000000000000..4aec577e86dcd --- /dev/null +++ b/apps/www/app/partners/[slug]/page.tsx @@ -0,0 +1,21 @@ +import supabase from '~/lib/supabaseMisc' +import { notFound, redirect } from 'next/navigation' + +type Params = { slug: string } + +export const dynamic = 'force-dynamic' + +export default async function PartnerLegacyPage({ params }: { params: Promise }) { + const { slug } = await params + + const { data: partner } = await supabase + .from('partners') + .select('slug, type') + .eq('approved', true) + .eq('slug', slug) + .single() + + if (!partner || partner.type === 'expert') notFound() + + redirect(`/partners/catalog/${partner.slug}`) +} diff --git a/apps/www/app/partners/catalog/IntegrationsContent.tsx b/apps/www/app/partners/catalog/IntegrationsContent.tsx new file mode 100644 index 0000000000000..744f69d326475 --- /dev/null +++ b/apps/www/app/partners/catalog/IntegrationsContent.tsx @@ -0,0 +1,458 @@ +'use client' + +import DefaultLayout from '~/components/Layouts/Default' +import SectionContainer from '~/components/Layouts/SectionContainer' +import Panel from '~/components/Panel' +import { searchCatalogPartners } from '~/lib/marketplaceDb' +import type { Partner } from '~/types/partners' +import { getCategoryIcon } from 'common/marketplace-categories' +import { useInfiniteScrollWithFetch } from 'hooks/useInfiniteScroll' +import { ArrowRight, ArrowUpRight, Filter, LayoutGrid, List, Loader, Search } from 'lucide-react' +import Image from 'next/image' +import Link from 'next/link' +import { + parseAsArrayOf, + parseAsBoolean, + parseAsString, + parseAsStringEnum, + useQueryStates, +} from 'nuqs' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { + Badge, + Button, + Checkbox, + cn, + InputGroup, + InputGroupAddon, + InputGroupInput, + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from 'ui' +import { useDebounce } from 'use-debounce' + +interface Props { + initialPartners: Partner[] + metaTitle: string + metaDescription: string +} + +type ViewMode = 'grid' | 'list' + +const OFFICIAL_PARTNER_SLUGS = new Set(['grafana', 'stripe', 'aikido', 'doppler', 'resend']) +const PARTNERS_PAGE_SIZE = 24 + +export default function IntegrationsContent({ + initialPartners, + metaTitle, + metaDescription, +}: Props) { + const [partners, setPartners] = useState(initialPartners) + // Deduplicate by slug, keep name for display, sort alphabetically. + const allCategories = Array.from( + new Map(initialPartners?.flatMap((p) => p.categories).map((c) => [c.slug, c]) ?? []).values() + ).sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())) + + const [{ q: search, cat: selectedCategories, partner: partnerOnly, view: viewMode }, setFilters] = + useQueryStates( + { + q: parseAsString.withDefault(''), + cat: parseAsArrayOf(parseAsString).withDefault([]), + partner: parseAsBoolean.withDefault(false), + view: parseAsStringEnum(['grid', 'list']).withDefault('grid'), + }, + { history: 'replace' } + ) + + const [debouncedSearchTerm] = useDebounce(search, 300) + const [isSearching, setIsSearching] = useState(false) + const searchIdRef = useRef(0) + + useEffect(() => { + if (debouncedSearchTerm.trim() === '') { + setIsSearching(false) + setPartners(initialPartners) + return + } + setIsSearching(true) + const currentSearchId = ++searchIdRef.current + searchCatalogPartners(debouncedSearchTerm) + .then((results) => { + if (currentSearchId === searchIdRef.current) setPartners(results ?? []) + }) + .catch(() => { + if (currentSearchId === searchIdRef.current) setPartners([]) + }) + .finally(() => { + if (currentSearchId === searchIdRef.current) setIsSearching(false) + }) + }, [debouncedSearchTerm, initialPartners]) + + // selectedCategories holds slugs; toggle by slug + const handleCategoryChange = (slug: string) => + setFilters({ + cat: selectedCategories.includes(slug) + ? selectedCategories.filter((s) => s !== slug) + : [...selectedCategories, slug], + }) + + const HAS_ACTIVE_FILTERS = search.trim() !== '' || selectedCategories.length > 0 || partnerOnly + const activeFilterCount = selectedCategories.length + (partnerOnly ? 1 : 0) + + const filtered = useMemo(() => { + const categoryFiltered = + selectedCategories.length > 0 + ? partners.filter((p) => p.categories.some((c) => selectedCategories.includes(c.slug))) + : partners + + return partnerOnly + ? categoryFiltered.filter((p) => OFFICIAL_PARTNER_SLUGS.has(p.slug)) + : categoryFiltered + }, [partners, selectedCategories, partnerOnly]) + + const featuredPartners = useMemo( + () => filtered.filter((p) => p.featured).sort((a, b) => a.title.localeCompare(b.title)), + [filtered] + ) + const listPartners = useMemo( + () => + HAS_ACTIVE_FILTERS + ? [...filtered.filter((p) => p.featured), ...filtered.filter((p) => !p.featured)] + : filtered.filter((p) => !p.featured), + [filtered, HAS_ACTIVE_FILTERS] + ) + const showFeatured = !HAS_ACTIVE_FILTERS && featuredPartners.length > 0 + + // Only render the first page of the (already-fetched, in-memory) partner list up front, + // then reveal more as the sentinel below the grid/list scrolls into view — avoids mounting + // every partner card (and its logo ) on initial load. + const initialVisiblePartners = useMemo( + () => listPartners.slice(0, PARTNERS_PAGE_SIZE), + [listPartners] + ) + const fetchMoreVisiblePartners = useCallback( + async (offset: number, limit: number) => listPartners.slice(offset, offset + limit), + [listPartners] + ) + const { + items: visiblePartners, + hasMore: hasMorePartners, + loadMoreRef: partnersLoadMoreRef, + } = useInfiniteScrollWithFetch({ + initialItems: initialVisiblePartners, + totalItems: listPartners.length, + pageSize: PARTNERS_PAGE_SIZE, + fetchMore: fetchMoreVisiblePartners, + }) + + // Shared filter UI — rendered in both the desktop sidebar and the mobile bottom sheet. + // Uses wrapping