fix(studio): polish pipeline creation copy and actions (#50956)

## Problem

The current pipeline creation sheet describes destination type with two
disconnected sentences. The enablement callout repeats itself, while
three Pipelines action menus have undersized triggers.

## Solution

Use a complete destination-specific description for each release stage.
Present the Enable Pipelines callout as a note with an action title, a
short explanation, and one button. Clarify the enablement dialog copy
and align the three action-menu triggers with a consistent button width
and hit area.

| Before | After |
| --- | --- |
| <img width="1024" height="759" alt="Pipelines Database Sandals Field
Lab Supabase"
src="https://github.com/user-attachments/assets/fac744dd-2701-40ae-a65d-0801a1a86e6f"
/> | <img width="1024" height="759" alt="Pipelines Database Sandals
Field Lab Supabase"
src="https://github.com/user-attachments/assets/075bf609-5b65-40ca-8aaf-d27335d48838"
/> |
| <img width="1024" height="759" alt="18436"
src="https://github.com/user-attachments/assets/92f8c63b-f9b9-4dd6-b9b6-26ede932c172"
/> | <img width="1076" height="759" alt="93893"
src="https://github.com/user-attachments/assets/3c6f26b0-130c-4508-bf6b-d2c641805426"
/> |
| <img width="1024" height="759" alt="16935"
src="https://github.com/user-attachments/assets/63e75045-412a-4bcc-9cf9-20245ac60871"
/> | <img width="1024" height="759" alt="75021"
src="https://github.com/user-attachments/assets/09771e1e-2e89-466d-8f60-22c5bacc9956"
/> |

## Review instructions

1. Open [Database > Pipelines in the Studio
preview](https://studio-staging-git-dnywh-fixpipelines-creation-polish-supabase.vercel.app/dashboard/project/_/database/pipelines),
click **Add pipeline**, and select BigQuery or Snowflake. Confirm the
Type description reads as a complete sentence.
2. On a project where Pipelines is not enabled, open the creation sheet.
Confirm the note has no extra padding or Docs link, then click
**Enable** and check the dialog copy. The list menu's **Enable
Pipelines** action opens the same dialog.
3. Check the action-menu triggers on the Pipelines list, pipeline detail
page, and table row for consistent sizing and click targets.

## Checklist

- [x] I have read
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)


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

## Summary by CodeRabbit

* **User Experience**
* Replication setup now displays a loading state while access and
organization details are checked, then shows the appropriate access
request, enablement notice, or destination form.
* Access notices use clearer, responsive messaging with a primary
“Request access” link.
* Enablement messaging now reflects whether your plan includes access,
and the success notification is shorter.
* Destination-type notices now identify the selected type and clarify
that it cannot be changed after creation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Danny White authored and GitHub committed 2026-09-28 17:34:10 +10:00
1 parent f2ff4ec0e9
commit c856de8fda
7 files changed
+82 -77

No files matched your search

@@ -1,4 +1,4 @@
import { useParams } from 'common'
import { useFeatureFlags, useParams } from 'common'
import { ArrowUpRight } from 'lucide-react'
import Link from 'next/link'
import { parseAsInteger, parseAsStringEnum, useQueryState } from 'nuqs'
@@ -6,7 +6,6 @@ import { useEffect, useRef } from 'react'
import { toast } from 'sonner'
import {
Button,
cn,
DialogSectionSeparator,
Sheet,
SheetContent,
@@ -16,6 +15,7 @@ import {
SheetTitle,
} from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { EnablePipelinesCallout } from '../EnablePipelinesCallout'
import { PipelineStatusName } from '../Replication.constants'
@@ -28,8 +28,9 @@ import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialo
import { DocsButton } from '@/components/ui/DocsButton'
import { useReplicationDestinationsQuery } from '@/data/replication/destinations-query'
import { checkLocalETLNotSetUp } from '@/data/replication/utils'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose'
import { DOCS_URL } from '@/lib/constants'
import { DOCS_URL, IS_PLATFORM } from '@/lib/constants'
const DESTINATION_DOCS_PATHS: Partial<Record<DestinationType, string>> = {
BigQuery: '/guides/database/replication/pipelines/bigquery#configure-bigquery-as-a-destination',
@@ -41,7 +42,10 @@ const DESTINATION_DOCS_PATHS: Partial<Record<DestinationType, string>> = {
export const DestinationPanel = () => {
const { ref: projectRef } = useParams()
const { hasLoaded: flagsLoaded } = useFeatureFlags()
const { isPending: isOrganizationPending } = useSelectedOrganizationQuery()
const enablePgReplicate = useIsETLPrivateAlpha()
const isAccessLoading = IS_PLATFORM && (!flagsLoaded || isOrganizationPending)
const { error: destinationsError } = useReplicationDestinationsQuery({ projectRef })
const isLocalETLNotSetUp = checkLocalETLNotSetUp(destinationsError)
@@ -80,6 +84,9 @@ export const DestinationPanel = () => {
} = useDestinationInformation({ id: edit })
const destinationType = existingDestinationType ?? urlDestinationType
const invalidExistingDestination = destinationFetcher.error?.code === 404
const showAccessRequest = !isAccessLoading && !enablePgReplicate
const showEnablement = !isAccessLoading && enablePgReplicate && replicationNotEnabled
const showDestinationForm = !isAccessLoading && enablePgReplicate && !replicationNotEnabled
const existingDestination = editMode
? {
@@ -159,22 +166,24 @@ export const DestinationPanel = () => {
/>
</SheetHeader>
{!enablePgReplicate ? (
{isAccessLoading && (
<SheetSection>
<GenericSkeletonLoader />
</SheetSection>
)}
{showAccessRequest && (
<div className="grow overflow-auto min-h-0">
{pipelinesTypeSelection}
<SheetSection>
<div className={cn('border rounded-md p-6 flex flex-col gap-y-4')}>
<div className="flex flex-col gap-y-1">
<h4>Request Pipelines access</h4>
<p className="text-sm text-foreground-light">
Pipelines is in <span className="text-foreground">public alpha</span> and
being rolled out gradually. Request access below to join the waitlist.
</p>
</div>
<div className="flex gap-x-2">
<Admonition
type="note"
layout="responsive"
title="Request Pipelines access"
description="Pipelines is in public alpha and available to approved organizations."
actions={
<Button
asChild
variant="secondary"
variant="primary"
iconRight={<ArrowUpRight size={16} strokeWidth={1.5} />}
>
<Link
@@ -182,22 +191,23 @@ export const DestinationPanel = () => {
rel="noreferrer"
href="https://forms.supabase.com/pg_replicate"
>
Request Pipelines access
Request access
</Link>
</Button>
<DocsButton href={`${DOCS_URL}/guides/database/replication#pipelines`} />
</div>
</div>
}
/>
</SheetSection>
</div>
) : replicationNotEnabled ? (
)}
{showEnablement && (
<div className="grow overflow-auto min-h-0">
{pipelinesTypeSelection}
<SheetSection>
<EnablePipelinesCallout className="p-6!" type={destinationType} />
<EnablePipelinesCallout type={destinationType} />
</SheetSection>
</div>
) : (
)}
{showDestinationForm && (
<DestinationForm
visible={visible}
selectedType={destinationType ?? 'BigQuery'}
@@ -120,7 +120,11 @@ describe('DestinationTypeSelection', () => {
fireEvent.click(await screen.findByRole('combobox'))
fireEvent.click(await screen.findByText('BigQuery'))
expect(await screen.findByText(/In public alpha and may change/)).toBeInTheDocument()
expect(
await screen.findByText(
'Destination type cannot be changed after creation. BigQuery support is in public alpha.'
)
).toBeInTheDocument()
})
test('disables the selector in edit mode so the destination type cannot be changed', async () => {
@@ -127,18 +127,23 @@ export const DestinationTypeSelection = () => {
const selectedOption = options.find((option) => option.value === destinationType)
const STAGE_DESCRIPTIONS: Record<NonNullable<DestinationTypeOption['stage']>, string> = {
'Public Alpha': 'In public alpha and may change.',
'Early Access': 'In early access and may change.',
Deprecated: 'This destination type is deprecated.',
const STAGE_DESCRIPTIONS: Record<
NonNullable<DestinationTypeOption['stage']>,
(type: DestinationType) => string
> = {
'Public Alpha': (type) => `${type} support is in public alpha.`,
'Early Access': (type) => `${type} support is in early access.`,
Deprecated: (type) => `${type} is deprecated.`,
}
const stageDescription = selectedOption?.stage ? STAGE_DESCRIPTIONS[selectedOption.stage] : null
const stageDescription = selectedOption?.stage
? STAGE_DESCRIPTIONS[selectedOption.stage](selectedOption.value)
: null
const typeDescription =
!editMode || stageDescription ? (
<span>
{!editMode && 'Cannot be changed after creation.'}
{!editMode && 'Destination type cannot be changed after creation.'}
{!editMode && stageDescription ? ' ' : null}
{stageDescription}
</span>
@@ -300,7 +300,7 @@ export const Destinations = () => {
aria-label="More actions"
variant="default"
icon={<MoreVertical />}
className="px-1.25"
className="w-6.5 hit-area-1"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-64">
@@ -3,10 +3,8 @@ import { useState } from 'react'
import { toast } from 'sonner'
import {
Button,
cn,
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogSection,
@@ -14,9 +12,9 @@ import {
DialogTitle,
DialogTrigger,
} from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import { DestinationType } from './DestinationPanel/DestinationPanel.types'
import { DocsButton } from '@/components/ui/DocsButton'
import { InlineLink } from '@/components/ui/InlineLink'
import { UpgradePlanButton } from '@/components/ui/UpgradePlanButton'
import { useCreateTenantSourceMutation } from '@/data/replication/create-tenant-source-mutation'
@@ -43,7 +41,7 @@ export const EnablePipelinesModal = ({
const { mutate: createTenantSource, isPending: creatingTenantSource } =
useCreateTenantSourceMutation({
onSuccess: () => {
toast.success('Pipelines has been successfully enabled!')
toast.success('Pipelines enabled')
setOpen(false)
},
onError: (error) => {
@@ -60,38 +58,33 @@ export const EnablePipelinesModal = ({
<Dialog open={open} onOpenChange={setOpen}>
{!hideTrigger && (
<DialogTrigger asChild>
<Button variant="primary" className="w-min">
Enable Pipelines
</Button>
<Button variant="primary">Enable</Button>
</DialogTrigger>
)}
<DialogContent size="small">
<DialogHeader>
<DialogTitle>Enable Pipelines</DialogTitle>
<DialogDescription>
{hasAccess
? 'Enabling creates Pipelines resources for this project.'
: 'Upgrade to the Pro plan to replicate database changes to data warehouses and analytics platforms.'}
</DialogDescription>
</DialogHeader>
{hasAccess && (
<>
<DialogSectionSeparator />
<DialogSection className="flex flex-col gap-y-3">
<DialogSectionSeparator />
<DialogSection className="flex flex-col gap-y-3">
{hasAccess ? (
<>
<p className="text-sm text-foreground-light">
Pipelines is in public alpha and may change as we refine it.
</p>
<p className="text-sm text-foreground-light">
You’ll be billed for configured pipeline hours and for Postgres row data processed
during initial sync and ongoing replication. Review{' '}
Pipelines bills for configured pipeline hours and Postgres row data processed during
initial sync and ongoing replication. Review{' '}
<InlineLink href={`${DOCS_URL}/guides/platform/manage-your-usage/pipelines`}>
Pipelines pricing
</InlineLink>{' '}
before enabling.
</p>
</DialogSection>
</>
)}
<p className="text-sm text-foreground-light">
Pipelines is in public alpha and may change.
</p>
</>
) : (
<p className="text-sm text-foreground-light">Pipelines requires the Pro plan.</p>
)}
</DialogSection>
<DialogFooter>
<Button disabled={creatingTenantSource} onClick={() => setOpen(false)}>
Cancel
@@ -109,33 +102,26 @@ export const EnablePipelinesModal = ({
)
}
export const EnablePipelinesCallout = ({
type,
className,
}: {
type?: DestinationType | null
className?: string
}) => {
export const EnablePipelinesCallout = ({ type }: { type?: DestinationType | null }) => {
const { hasAccess } = useCheckEntitlements('replication.etl')
return (
<div className={cn('border rounded-md p-4 md:p-12 flex flex-col gap-y-4', className)}>
<div className="flex flex-col gap-y-1">
<h4>Enable Pipelines</h4>
<p className="text-sm text-foreground-light">
Supabase Pipelines replicates database changes to supported destination systems.{' '}
{hasAccess ? 'Enable Pipelines for your project' : 'Upgrade to the Pro plan'} to replicate
database changes to {type ?? 'data warehouses and analytics platforms'}.
</p>
</div>
<div className="flex gap-x-2">
{hasAccess ? (
<Admonition
type="note"
layout="responsive"
title={hasAccess ? 'Enable Pipelines' : 'Upgrade to Pro for Pipelines'}
description={
hasAccess
? `Pipelines must be enabled before this project can replicate database changes to ${type ?? 'external destinations'}.`
: `The Pro plan is required to replicate database changes to ${type ?? 'external destinations'} with Pipelines.`
}
actions={
hasAccess ? (
<EnablePipelinesModal />
) : (
<UpgradePlanButton source="replication" featureProposition="use replication" />
)}
<DocsButton href={`${DOCS_URL}/guides/database/replication#pipelines`} />
</div>
</div>
)
}
/>
)
}
@@ -335,7 +335,7 @@ export const ReplicationPipelineLayout = ({ children }: PropsWithChildren) => {
<DropdownMenuTrigger asChild>
<Button
variant="default"
className="px-1.25 hit-area-2"
className="w-6.5 hit-area-1"
aria-label="Pipeline options"
icon={<MoreVertical />}
disabled={isPipelineRequestPending || isTransitioning}
@@ -160,7 +160,7 @@ export const RowMenu = ({
<div className="relative">
<Button
variant="default"
className="px-1.25 hit-area-2"
className="w-6.5 hit-area-1"
aria-label={hasUpdate ? 'Pipeline options, update available' : 'Pipeline options'}
icon={<MoreVertical />}
/>