Files
supabase/apps/studio/components/interfaces/ConnectSheet/ConnectStepsSection.tsx
Danny White 5dc054ae8f feat(studio): warn in Connect sheet when Data API is disabled (#47537)
## What kind of change does this PR introduce?

Feature. Resolves DEPR-599.

## What is the current behavior?

When the Data API is disabled (PostgREST has no exposed schemas), the
Connect sheet still shows client-library setup steps for Framework and
MCP modes without indicating that database queries will fail.

## What is the new behavior?

When database access via the Connect instructions requires PostgREST, an
inline warning appears above the steps (setup instructions remain
visible):

- **Framework**: warns when Data API is off; install, env vars, and
auth/SSR setup still work
- **MCP**: warns only when Database tools apply (selected explicitly, or
by default when no feature filter is set)

The warning fails open if PostgREST config cannot be loaded, and links
to Data API settings via an "Enable Data API" CTA.

| After |
| --- |
| <img width="1664" height="718" alt="CleanShot 2026-07-02 at 21 29
16@2x"
src="https://github.com/user-attachments/assets/80d21927-c4dd-4158-8946-bf648b95e451"
/>|

## Additional context

- Gating logic lives in `ConnectStepsSection.utils.ts` with unit tests
- Out of scope: warning when Data API is on but zero tables/schemas are
exposed
- Coexists with the upcoming warehouse branch's catalog warning — that
lives in a separate `WarehouseCatalogPanel` for `catalog` mode only

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Connection setup now checks Data API enablement and conditionally
shows a “Data API disabled” warning, including an action to open Data
API settings.
* **Bug Fixes**
* Warning logic now more accurately reflects the selected connection
mode and chosen feature/tool selections.
* **Tests**
* Added a focused test suite covering the Data API configuration
decision rules and when the warning should appear.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-07 18:05:19 +00:00

331 lines
12 KiB
TypeScript

import { useParams } from 'common'
import dynamic from 'next/dynamic'
import Link from 'next/link'
import { useMemo, useRef, type ComponentType } from 'react'
import { Button } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import type {
ConnectionStringPooler,
ConnectState,
DeploymentMode,
ProjectKeys,
ResolvedStep,
StepContentProps,
} from './Connect.types'
import { ConnectSheetStep } from './ConnectSheetStep'
import {
shouldFetchDataApiConfig,
shouldShowDataApiDisabledWarning,
} from './ConnectStepsSection.utils'
import { CopyPromptAdmonition } from './CopyPromptAdmonition'
import { buildConnectionStringPooler, getConnectionStrings } from './DatabaseSettings.utils'
import { getAddons } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
import { DocsButton } from '@/components/ui/DocsButton'
import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
import { usePgbouncerConfigQuery } from '@/data/database/pgbouncer-config-query'
import { useSupavisorConfigurationQuery } from '@/data/database/supavisor-configuration-query'
import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
import { useDeploymentMode } from '@/hooks/misc/useDeploymentMode'
import { useIsDataApiEnabled } from '@/hooks/misc/useIsDataApiEnabled'
import { DOCS_URL } from '@/lib/constants'
import { pluckObjectFields } from '@/lib/helpers'
interface ConnectStepsSectionProps {
steps: ResolvedStep[]
state: ConnectState
projectKeys: ProjectKeys
}
/**
* Resolves a content path template by replacing {{key}} placeholders with state values.
* Empty segments are filtered out to handle optional state values like frameworkVariant.
*
* Examples:
* - '{{framework}}/{{frameworkVariant}}/{{library}}' with state {framework: 'nextjs', frameworkVariant: 'app', library: 'supabasejs'}
* → 'nextjs/app/supabasejs'
* - '{{orm}}' with state {orm: 'prisma'}
* → 'prisma'
* - 'steps/install' (no templates)
* → 'steps/install'
*/
function resolveContentPath(template: string, state: ConnectState): string {
return template
.replace(/\{\{(\w+)\}\}/g, (_, key) => String(state[key] ?? ''))
.split('/')
.filter(Boolean)
.join('/')
}
/**
* Hook to fetch and prepare connection strings for step content.
*/
function useConnectionStringPooler(deploymentMode: DeploymentMode): ConnectionStringPooler {
const { ref: projectRef } = useParams()
const { hasAccess: allowPgBouncerSelection } = useCheckEntitlements('dedicated_pooler')
const { data: settings } = useProjectSettingsV2Query({ projectRef })
const { data: pgbouncerConfig } = usePgbouncerConfigQuery({ projectRef })
const { data: supavisorConfig } = useSupavisorConfigurationQuery({ projectRef })
const { data: addons } = useProjectAddonsQuery({ projectRef })
const { ipv4: ipv4Addon } = getAddons(addons?.selected_addons ?? [])
// Each intermediate derived value is memoized so its reference stays stable
// across renders while the upstream query data is unchanged. Without this,
// pluckObjectFields / getConnectionStrings would mint fresh objects every
// render and invalidate the final useMemo on each tick (and ripple through
// every consumer that lists ConnectionStringPooler in their own deps).
const connectionInfo = useMemo(() => {
const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at']
const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' }
return pluckObjectFields(settings || emptyState, DB_FIELDS)
}, [settings])
const poolingConfigurationShared = supavisorConfig?.find((x) => x.database_type === 'PRIMARY')
const poolingConfigurationDedicated = allowPgBouncerSelection ? pgbouncerConfig : undefined
const connectionStringsShared = useMemo(
() =>
getConnectionStrings({
connectionInfo,
poolingInfo: {
connectionString: poolingConfigurationShared?.connection_string ?? '',
db_host: poolingConfigurationShared?.db_host ?? '',
db_name: poolingConfigurationShared?.db_name ?? '',
db_port: poolingConfigurationShared?.db_port ?? 0,
db_user: poolingConfigurationShared?.db_user ?? '',
},
metadata: { projectRef },
}),
[connectionInfo, poolingConfigurationShared, projectRef]
)
const connectionStringsDedicated = useMemo(
() =>
poolingConfigurationDedicated !== undefined
? getConnectionStrings({
connectionInfo,
poolingInfo: {
connectionString: poolingConfigurationDedicated.connection_string,
db_host: poolingConfigurationDedicated.db_host,
db_name: poolingConfigurationDedicated.db_name,
db_port: poolingConfigurationDedicated.db_port,
db_user: poolingConfigurationDedicated.db_user,
},
metadata: { projectRef },
})
: undefined,
[connectionInfo, poolingConfigurationDedicated, projectRef]
)
return useMemo(
() =>
buildConnectionStringPooler({
deploymentMode,
connectionInfo,
connectionStringsShared,
connectionStringsDedicated,
ipv4Addon: !!ipv4Addon,
}),
[deploymentMode, connectionInfo, connectionStringsShared, connectionStringsDedicated, ipv4Addon]
)
}
// Vite needs `import.meta.glob` to statically discover the step content
// modules because the `${filePath}` template can span multiple directory
// segments (`flask/supabasepy`, `steps/shadcn/explore`, ...) which Vite's
// dynamic-import-vars plugin can't analyze. Skip the glob on the SSR bundle
// — Vite replaces `import.meta.env.SSR` at build time and tree-shakes the
// call so the 37 content modules stay out of the server graph (pulling them
// in reshuffles chunks enough to surface latent circular-dep bugs in
// unrelated modules). Next/webpack doesn't know about `import.meta.glob`
// either; the try/catch lets that branch fall through to the webpack-friendly
// `import()` below.
let contentModules: Record<string, () => Promise<unknown>> = {}
if (!import.meta.env?.SSR) {
try {
contentModules = import.meta.glob('./content/**/content.{tsx,ts}')
} catch {
// webpack build: import.meta.glob is undefined, keep empty map
}
}
type StepContentModule = { default: ComponentType<StepContentProps> }
/**
* Dynamically loads and renders a content component from the content directory.
* All step content uses this unified loader - no built-in component registry needed.
*/
function StepContent({
contentId,
state,
projectKeys,
connectionStringPooler,
deploymentMode,
}: {
contentId: string
state: ConnectState
projectKeys: ProjectKeys
connectionStringPooler: ConnectionStringPooler
deploymentMode: DeploymentMode
}) {
// Resolve any template placeholders in the content path
const filePath = useMemo(() => resolveContentPath(contentId, state), [contentId, state])
// Dynamically import the content component
const ContentComponent = useMemo(() => {
const viteLoader =
contentModules[`./content/${filePath}/content.tsx`] ??
contentModules[`./content/${filePath}/content.ts`]
const loader = viteLoader
? (viteLoader as () => Promise<StepContentModule>)
: () =>
import(/* @vite-ignore */ `./content/${filePath}/content`) as Promise<StepContentModule>
return dynamic<StepContentProps>(loader, {
loading: () => (
<div className="p-4 min-h-[200px]">
<GenericSkeletonLoader />
</div>
),
})
}, [filePath])
return (
<ContentComponent
state={state}
projectKeys={projectKeys}
connectionStringPooler={connectionStringPooler}
deploymentMode={deploymentMode}
/>
)
}
export function ConnectStepsSection({ steps, state, projectKeys }: ConnectStepsSectionProps) {
const { ref } = useParams()
const stepsContainerRef = useRef<HTMLDivElement | null>(null)
const deploymentMode = useDeploymentMode()
const connectionStringPooler = useConnectionStringPooler(deploymentMode)
const { data: ipv4Addon } = useProjectAddonsQuery(
{ projectRef: ref },
{
select: (data) => {
const selectedAddons = data?.selected_addons ?? []
return selectedAddons.find((addon) => addon.type === 'ipv4')
},
}
)
const showIpv4AddonNotice =
deploymentMode.isPlatform &&
state.mode === 'direct' &&
!ipv4Addon &&
(state.connectionMethod === 'direct' ||
(state.connectionMethod === 'transaction' && !state.useSharedPooler))
const showSessionPoolerNotice =
deploymentMode.isPlatform && state.mode === 'direct' && state.connectionMethod === 'session'
const showSelfHostedMcpNotice = deploymentMode.isSelfHosted && state.mode === 'mcp'
const shouldFetchDataApiStatus = shouldFetchDataApiConfig({
mode: state.mode,
})
const {
isEnabled: isDataApiEnabled,
isPending: isDataApiConfigPending,
isError: isDataApiConfigError,
} = useIsDataApiEnabled({
projectRef: ref,
enabled: shouldFetchDataApiStatus,
})
const showDataApiDisabledWarning = shouldShowDataApiDisabledWarning({
mode: state.mode,
isDataApiEnabled,
isPending: isDataApiConfigPending,
isError: isDataApiConfigError,
})
if (steps.length === 0) return null
return (
<div className="bg-muted/50 flex-1">
<div className="p-8 flex flex-col gap-y-6">
<h3>Connect your app</h3>
{showDataApiDisabledWarning && (
<Admonition
type="warning"
layout="responsive"
title="Database access requires the Data API"
description="Client library database queries will not work until the Data API is enabled."
actions={[
<Button asChild key="enable" variant="default">
<Link href={`/project/${ref}/integrations/data_api`}>Enable Data API</Link>
</Button>,
]}
/>
)}
{showIpv4AddonNotice && (
<Admonition
type="default"
title={`${state.connectionMethod === 'direct' ? 'Direct connections use' : 'Transaction pooler uses'} IPv6 by default`}
description="Enable the dedicated IPv4 address add-on to connect from IPv4-only networks"
actions={[
<Button asChild key="addon" variant="default">
<Link href={`/project/${ref}/settings/addons?panel=ipv4`}>Enable IPv4 add-on</Link>
</Button>,
<DocsButton key="docs" href={`${DOCS_URL}/guides/platform/ipv4-address`} />,
]}
/>
)}
{showSessionPoolerNotice && (
<Admonition
type="default"
title="Only use Session Pooler on an IPv4 network"
description="Session pooler connections are IPv4 proxied for free. Use Direct Connection if connecting via an IPv6 network."
/>
)}
{showSelfHostedMcpNotice && (
<Admonition
type="default"
title="MCP for self-hosted Supabase requires extra setup"
description="The configuration below points at the hosted Supabase MCP server. To use MCP against your self-hosted instance, follow the self-hosted MCP guide."
actions={[
<DocsButton
key="docs"
href="https://supabase.com/docs/guides/self-hosting/enable-mcp"
/>,
]}
/>
)}
<CopyPromptAdmonition stepsContainerRef={stepsContainerRef} />
<div className="mt-6" ref={stepsContainerRef}>
{steps.map((step, index) => (
<ConnectSheetStep
key={step.id}
number={index + 1}
title={step.title}
description={step.description}
>
<StepContent
contentId={step.content}
state={state}
projectKeys={projectKeys}
connectionStringPooler={connectionStringPooler}
deploymentMode={deploymentMode}
/>
</ConnectSheetStep>
))}
</div>
</div>
</div>
)
}