mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
1 parent
f2ff4ec0e9
commit
c856de8fda
7 files changed
+82
-77
No files matched your search
+31
-21
@@ -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'}
|
||||
|
||||
+5
-1
@@ -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 () => {
|
||||
|
||||
+11
-6
@@ -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>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -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 />}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user