From d691073f02ade045bba1db0f76a35c7ab7639345 Mon Sep 17 00:00:00 2001 From: Raminder Singh Date: Mon, 1 Jun 2026 13:06:37 +0530 Subject: [PATCH 1/3] feat: override wrappers in dashboard with those from marketplace db (#46472) This PR overrides title, description, content, logo, images, docs url, and site url from marketplace db for wrappers. If marketplace doesn't yet publish a wrapper listing, the page falls back to the hardcoded content we show today. It also improves the marketplace listings and categories queries by returning typed results, making the code more type safe. ## Summary by CodeRabbit * **New Features** * Studio integrations now surface updated marketplace metadata (name, description, icon, docs, site, author, files) when available. * Marketplace wrapper integrations are consolidated and shown alongside studio integrations. * **Refactor** * Marketplace category and integration fetching rewritten for more reliable loading, cancellation support, and improved menu/category population. [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46472?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) --- .../Landing/useAvailableIntegrations.tsx | 244 +++++++++++------- .../Marketplace/MarketplaceIndex.tsx | 5 +- .../layouts/ProjectIntegrationsLayout.tsx | 23 +- .../integration-categories-query.ts | 27 +- .../data/marketplace/integrations-query.ts | 30 ++- .../project/[ref]/integrations/index.tsx | 9 +- 6 files changed, 197 insertions(+), 141 deletions(-) diff --git a/apps/studio/components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx b/apps/studio/components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx index 0d13f17615d40..82d1a407f7f4c 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx @@ -1,4 +1,3 @@ -import { useQuery } from '@tanstack/react-query' import { FeatureFlagContext, IS_PLATFORM } from 'common' import { fullImageUrl } from 'common/marketplace-client' import { Boxes } from 'lucide-react' @@ -9,10 +8,36 @@ import { cn } from 'ui' import { INTEGRATIONS, Loading, type IntegrationDefinition } from './Integrations.constants' import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' -import { marketplaceIntegrationsQueryOptions } from '@/data/marketplace/integrations-query' +import { + useMarketplaceIntegrationsQuery, + type MarketplaceIntegration, +} from '@/data/marketplace/integrations-query' import { useCLIReleaseVersionQuery } from '@/data/misc/cli-release-version-query' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' +const renderMarketplaceLogo = (listingLogo?: string | null) => { + const MarketplaceLogo = ({ className, ...props }: { className?: string } = {}) => ( +
+ {listingLogo ? ( + + ) : ( + + )} +
+ ) + return MarketplaceLogo +} + +function isForeignDataWrapper(integration: MarketplaceIntegration) { + return integration.categories.some((c) => c?.slug === 'foreign-data-wrapper') +} + /** * [Joshen] Returns a combination of * - Marketplace integrations retrieved remotely (Only if feature flag enabled) @@ -26,118 +51,108 @@ export const useAvailableIntegrations = () => { const { data: cliData } = useCLIReleaseVersionQuery() const isCLI = !!cliData?.current - const { data, error } = useQuery({ - ...marketplaceIntegrationsQueryOptions(), - enabled: isMarketplaceEnabled, - }) + const { data, error } = useMarketplaceIntegrationsQuery({ enabled: isMarketplaceEnabled }) const isPending = IS_PLATFORM && (!hasLoaded || (isMarketplaceEnabled && !data && !error)) const isSuccess = !IS_PLATFORM || (hasLoaded && (!isMarketplaceEnabled || (!!data && !error))) const isError = IS_PLATFORM && isMarketplaceEnabled && !!error // [Joshen] Format marketplace integrations into existing ones for now // Likely that we might need to change, but can look into separately + // Wrappers from marketplace are excluded here — they are merged into the + // hardcoded studio wrappers below as content overrides. const marketplaceIntegrations: IntegrationDefinition[] = useMemo( () => - (data ?? [])?.map((integration) => { - const { - id: listingId, - slug, - categories, - featured, - title, - description, - documentation_url: docsUrl, - website_url: siteUrl, - installation_url: installUrl, - installation_url_type: installUrlType, - installation_identification_method: installMethod, - secret_key_prefix: secretKeyPrefix, - edge_function_secret_name: edgeFunctionSecretName, - images, - content, - partner_name: authorName, - listing_logo: listingLogo, - } = integration - - const status = undefined - const author = { name: authorName ?? '', websiteUrl: '' } - - return { - id: slug ?? '', - name: title ?? '', - status, - featured: !!featured, - type: 'oauth' as const, // Currently marketplace only supports oauth apps - source: 'Partner' as const, - categories: Array.isArray(categories) - ? (categories as Array<{ slug: string }>).map((x) => x.slug) - : [], - content, - files: images?.map((image) => fullImageUrl(image)), - description, - docsUrl, - siteUrl, - installUrl, - installUrlType: installUrlType ?? undefined, - installIdentificationMethod: installMethod ?? undefined, - secretKeyPrefix: secretKeyPrefix ?? undefined, - edgeFunctionSecretName: edgeFunctionSecretName ?? undefined, - listingId: listingId ?? undefined, - author, - requiredExtensions: [], - icon: ({ className, ...props } = {}) => ( -
- {listingLogo ? ( - - ) : ( - - )} -
- ), - navigation: [ - { - route: 'overview', - label: 'Overview', + (data ?? []) + ?.filter((integration) => !isForeignDataWrapper(integration)) + .map((integration) => { + const { + id: listingId, + slug, + categories, + featured, + title, + description, + documentation_url: docsUrl, + website_url: siteUrl, + installation_url: installUrl, + installation_url_type: installUrlType, + installation_identification_method: installMethod, + secret_key_prefix: secretKeyPrefix, + edge_function_secret_name: edgeFunctionSecretName, + images, + content, + partner_name: authorName, + listing_logo: listingLogo, + } = integration + + const status = undefined + const author = { name: authorName ?? '', websiteUrl: '' } + + return { + id: slug ?? '', + name: title ?? '', + status, + featured: !!featured, + type: 'oauth' as const, // Currently marketplace only supports oauth apps + source: 'Partner' as const, + categories: categories.map((x) => x.slug), + content, + files: images?.map((image) => fullImageUrl(image)), + description, + docsUrl, + siteUrl, + installUrl, + installUrlType: installUrlType ?? undefined, + installIdentificationMethod: installMethod ?? undefined, + secretKeyPrefix: secretKeyPrefix ?? undefined, + edgeFunctionSecretName: edgeFunctionSecretName ?? undefined, + listingId: listingId ?? undefined, + author, + requiredExtensions: [], + icon: renderMarketplaceLogo(listingLogo), + navigation: [ + { + route: 'overview', + label: 'Overview', + }, + ], + navigate: ({ pageId = 'overview' }) => { + switch (pageId) { + case 'overview': + return dynamic( + () => + import('@/components/interfaces/Integrations/Integration/MarketplaceIntegrationOverviewTab').then( + (mod) => mod.MarketplaceIntegrationOverviewTab + ), + { + loading: Loading, + } + ) + } + return null }, - ], - navigate: ({ pageId = 'overview' }) => { - switch (pageId) { - case 'overview': - return dynamic( - () => - import('@/components/interfaces/Integrations/Integration/MarketplaceIntegrationOverviewTab').then( - (mod) => mod.MarketplaceIntegrationOverviewTab - ), - { - loading: Loading, - } - ) - } - return null - }, - } - }), + } + }), [data] ) + // Marketplace wrapper listings keyed by their studio-equivalent id + // (marketplace uses dash-separated slugs, studio uses underscore-separated ids). + const marketplaceWrappers = useMemo(() => { + const map: Record = {} + ;(data ?? []).forEach((integration) => { + if (!isForeignDataWrapper(integration)) return + map[integration.slug.replaceAll('-', '_')] = integration + }) + return map + }, [data]) + // [Joshen] Existing integrations that are defined within studio // Available integrations are all integrations that can be installed. If an integration can't be installed (needed // extensions are not available on this DB image), the UI will provide a tooltip explaining why. const allIntegrations = useMemo(() => { return INTEGRATIONS.filter((integration) => { - if ( - !integrationsWrappers && - (integration.type === 'wrapper' || integration.id.endsWith('_wrapper')) - ) { + if (!integrationsWrappers && integration.type === 'wrapper') { return false } @@ -146,8 +161,41 @@ export const useAvailableIntegrations = () => { } return true + }).map((integration) => { + const isWrapper = integration.type === 'wrapper' + if (!isWrapper) return integration + + const marketplaceWrapper = marketplaceWrappers[integration.id] + if (!marketplaceWrapper) return integration + + const { + title, + description, + content, + documentation_url: docsUrl, + website_url: siteUrl, + images, + partner_name: authorName, + listing_logo: listingLogo, + } = marketplaceWrapper + + const overrides = { + name: title, + description, + content, + docsUrl, + siteUrl, + author: authorName ? { name: authorName, websiteUrl: '' } : undefined, + files: images?.map((image) => fullImageUrl(image)), + icon: listingLogo ? renderMarketplaceLogo(listingLogo) : undefined, + } + + return { + ...integration, + ...Object.fromEntries(Object.entries(overrides).filter(([, v]) => v != null)), + } }) - }, [integrationsWrappers, isCLI]) + }, [integrationsWrappers, isCLI, marketplaceWrappers]) const dataWithMarketplace = useMemo(() => { return [...marketplaceIntegrations, ...allIntegrations].sort((a, b) => diff --git a/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceIndex.tsx b/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceIndex.tsx index d129289772e90..dbfb503b907dc 100644 --- a/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceIndex.tsx +++ b/apps/studio/components/interfaces/Integrations/Marketplace/MarketplaceIndex.tsx @@ -1,4 +1,3 @@ -import { useQuery } from '@tanstack/react-query' import { parseAsString, parseAsStringEnum, useQueryState } from 'nuqs' import { useMemo, useRef } from 'react' import { Button, Card, ShadowScrollArea, Table, TableBody, TableHeader } from 'ui' @@ -33,7 +32,7 @@ import { useInstalledIntegrations } from '@/components/interfaces/Integrations/L import { useIntegrationFilteringAndSort } from '@/components/interfaces/Integrations/Landing/useIntegrationFilteringAndSort' import { AlertError } from '@/components/ui/AlertError' import { DocsButton } from '@/components/ui/DocsButton' -import { marketplaceCategoriesQueryOptions } from '@/data/marketplace/integration-categories-query' +import { useMarketplaceCategoriesQuery } from '@/data/marketplace/integration-categories-query' import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' import { DOCS_URL } from '@/lib/constants' import { SHORTCUT_IDS } from '@/state/shortcuts/registry' @@ -89,7 +88,7 @@ export const MarketplaceIndex = () => { const isLoading = isLoadingAvailable || isLoadingInstalled const isSuccess = isSuccessAvailable && isSuccessInstalled - const { data: marketplaceCategories = [] } = useQuery(marketplaceCategoriesQueryOptions()) + const { data: marketplaceCategories = [] } = useMarketplaceCategoriesQuery() const categoryOptions = useMemo(() => { // Start with marketplace DB categories diff --git a/apps/studio/components/layouts/ProjectIntegrationsLayout.tsx b/apps/studio/components/layouts/ProjectIntegrationsLayout.tsx index 5a4a798f0f6c9..244b489bdba26 100644 --- a/apps/studio/components/layouts/ProjectIntegrationsLayout.tsx +++ b/apps/studio/components/layouts/ProjectIntegrationsLayout.tsx @@ -1,4 +1,3 @@ -import { useQuery } from '@tanstack/react-query' import { IS_PLATFORM, useFeatureFlags, useParams } from 'common' import { useRouter } from 'next/router' import { PropsWithChildren } from 'react' @@ -10,8 +9,8 @@ import { useInstalledIntegrations } from '@/components/interfaces/Integrations/L import { ProjectLayout } from '@/components/layouts/ProjectLayout' import AlertError from '@/components/ui/AlertError' import { ProductMenu } from '@/components/ui/ProductMenu' -import { marketplaceCategoriesQueryOptions } from '@/data/marketplace/integration-categories-query' -import { marketplaceIntegrationsQueryOptions } from '@/data/marketplace/integrations-query' +import { useMarketplaceCategoriesQuery } from '@/data/marketplace/integration-categories-query' +import { useMarketplaceIntegrationsQuery } from '@/data/marketplace/integrations-query' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' import { withAuth } from '@/hooks/misc/withAuth' @@ -54,19 +53,15 @@ const IntegrationCategoriesMenu = ({ page }: { page: string }) => { const pageKey = categoryParam || page const { integrationsWrappers: showWrappers } = useIsFeatureEnabled(['integrations:wrappers']) - const { data: categories = [], isPending: isPendingCategories } = useQuery( - marketplaceCategoriesQueryOptions({ enabled: isMarketplaceEnabled }) - ) - const { data: listings = [], isPending: isPendingListings } = useQuery( - marketplaceIntegrationsQueryOptions({ enabled: isMarketplaceEnabled }) - ) + const { data: categories = [], isPending: isPendingCategories } = useMarketplaceCategoriesQuery({ + enabled: isMarketplaceEnabled, + }) + const { data: listings = [], isPending: isPendingListings } = useMarketplaceIntegrationsQuery({ + enabled: isMarketplaceEnabled, + }) const populatedCategoryIds = new Set( - listings.flatMap((listing) => - Array.isArray(listing.categories) - ? (listing.categories as Array<{ id: string }>).map((c) => c.id) - : [] - ) + listings.flatMap((listing) => listing.categories.map((c) => c.id)) ) const nonEmptyCategories = categories.filter( (category) => category.id && populatedCategoryIds.has(category.id) diff --git a/apps/studio/data/marketplace/integration-categories-query.ts b/apps/studio/data/marketplace/integration-categories-query.ts index 06a452874548e..f9f5ca83a0f22 100644 --- a/apps/studio/data/marketplace/integration-categories-query.ts +++ b/apps/studio/data/marketplace/integration-categories-query.ts @@ -1,23 +1,32 @@ -import { queryOptions } from '@tanstack/react-query' -import { createMarketplaceClient } from 'common/marketplace-client' +import { useQuery } from '@tanstack/react-query' +import { createMarketplaceClient, type Category } from 'common/marketplace-client' import { marketplaceIntegrationsKeys } from './keys' import { handleError } from '@/data/fetchers' +import type { ResponseError, UseCustomQueryOptions } from '@/types' -async function getMarketplaceCategories() { +export type MarketplaceCategory = Category + +export async function getMarketplaceCategories(signal?: AbortSignal) { const marketplaceClient = createMarketplaceClient() - const { data, error } = await marketplaceClient.from('categories').select('*') + let query = marketplaceClient.from('categories').select('*') + if (signal) query = query.abortSignal(signal) + const { data, error } = await query if (error) handleError(error) return data ?? [] } -export const marketplaceCategoriesQueryOptions = ({ +export type MarketplaceCategoriesData = Awaited> +export type MarketplaceCategoriesError = ResponseError + +export const useMarketplaceCategoriesQuery = ({ enabled = true, -}: { enabled?: boolean } = {}) => { - return queryOptions({ + ...options +}: UseCustomQueryOptions = {}) => + useQuery({ queryKey: marketplaceIntegrationsKeys.categories(), - queryFn: () => getMarketplaceCategories(), + queryFn: ({ signal }) => getMarketplaceCategories(signal), enabled, + ...options, }) -} diff --git a/apps/studio/data/marketplace/integrations-query.ts b/apps/studio/data/marketplace/integrations-query.ts index 12b28f5ebfe59..199adae42bec2 100644 --- a/apps/studio/data/marketplace/integrations-query.ts +++ b/apps/studio/data/marketplace/integrations-query.ts @@ -1,26 +1,32 @@ -import { queryOptions } from '@tanstack/react-query' -import { createMarketplaceClient } from 'common/marketplace-client' +import { useQuery } from '@tanstack/react-query' +import { createMarketplaceClient, type Listing } from 'common/marketplace-client' import { marketplaceIntegrationsKeys } from './keys' import { handleError } from '@/data/fetchers' +import type { ResponseError, UseCustomQueryOptions } from '@/types' -async function getMarketplaceIntegrations() { +export type MarketplaceIntegration = Listing + +export async function getMarketplaceIntegrations(signal?: AbortSignal) { const marketplaceClient = createMarketplaceClient() - const { data, error } = await marketplaceClient - .from('listings') - .select('*') - .is('publish_dashboard', true) + let query = marketplaceClient.from('listings').select('*').is('publish_dashboard', true) + if (signal) query = query.abortSignal(signal) + const { data, error } = await query if (error) handleError(error) return data ?? [] } -export const marketplaceIntegrationsQueryOptions = ({ +export type MarketplaceIntegrationsData = Awaited> +export type MarketplaceIntegrationsError = ResponseError + +export const useMarketplaceIntegrationsQuery = ({ enabled = true, -}: { enabled?: boolean } = {}) => { - return queryOptions({ + ...options +}: UseCustomQueryOptions = {}) => + useQuery({ queryKey: marketplaceIntegrationsKeys.list(), - queryFn: () => getMarketplaceIntegrations(), + queryFn: ({ signal }) => getMarketplaceIntegrations(signal), enabled, + ...options, }) -} diff --git a/apps/studio/pages/project/[ref]/integrations/index.tsx b/apps/studio/pages/project/[ref]/integrations/index.tsx index ee48c9d7a3d70..61ad4429f6999 100644 --- a/apps/studio/pages/project/[ref]/integrations/index.tsx +++ b/apps/studio/pages/project/[ref]/integrations/index.tsx @@ -1,4 +1,3 @@ -import { useQuery } from '@tanstack/react-query' import { IS_PLATFORM, useFeatureFlags } from 'common' import { Database } from 'common/marketplace.types' import { Search } from 'lucide-react' @@ -33,7 +32,7 @@ import { ProjectIntegrationsLayoutDispatch } from '@/components/layouts/ProjectI import { AlertError } from '@/components/ui/AlertError' import { DocsButton } from '@/components/ui/DocsButton' import { NoSearchResults } from '@/components/ui/NoSearchResults' -import { marketplaceCategoriesQueryOptions } from '@/data/marketplace/integration-categories-query' +import { useMarketplaceCategoriesQuery } from '@/data/marketplace/integration-categories-query' import { BASE_PATH, DOCS_URL } from '@/lib/constants' import type { NextPageWithLayout } from '@/types' @@ -183,9 +182,9 @@ const LegacyIntegrationsPage = () => { const selectedCategory = useFilterCategory() - const { data: categories = [], isPending: isPendingCategories } = useQuery( - marketplaceCategoriesQueryOptions({ enabled: isMarketplaceEnabled }) - ) + const { data: categories = [], isPending: isPendingCategories } = useMarketplaceCategoriesQuery({ + enabled: isMarketplaceEnabled, + }) const isLoadingSelectedCategory = selectedCategory !== 'all' && From ac59de1f9ed786e476a77071165c84be536cc896 Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:18:07 +0200 Subject: [PATCH 2/3] chore: e2e tests reliability (#46496) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES/NO ## What kind of change does this PR introduce? Bug fix, feature, docs update, ... ## What is the current behavior? Please link any relevant issues here. ## What is the new behavior? Feel free to include screenshots if it includes visual changes. ## Additional context Add any other context or screenshots. ## Summary by CodeRabbit * **Accessibility** * Improved screen-reader label for table row action menus so table controls are clearer for assistive‑technology users. * **Tests** * Enhanced end-to-end test reliability: tightened selectors, added dialog/toast visibility and API-wait synchronization, scoped lookup fixes, removed redundant cleanup helper, and updated test setup to mark a terms-of-service dismissal to reduce flakiness. --- .../interfaces/Database/Tables/TableList.tsx | 7 ++- e2e/studio/features/cron-jobs.spec.ts | 12 +++-- e2e/studio/features/database.spec.ts | 50 +++++++++++++++---- .../features/realtime-inspector.spec.ts | 1 + e2e/studio/features/rls-policies.spec.ts | 35 +------------ e2e/studio/utils/test.ts | 1 + 6 files changed, 56 insertions(+), 50 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Tables/TableList.tsx b/apps/studio/components/interfaces/Database/Tables/TableList.tsx index 360fd1676bb3b..45b38e49c1008 100644 --- a/apps/studio/components/interfaces/Database/Tables/TableList.tsx +++ b/apps/studio/components/interfaces/Database/Tables/TableList.tsx @@ -493,7 +493,12 @@ export const TableList = ({ {!isSchemaLocked && ( -