polish create pipeline wizard UI and docs

- Replace sidebar stepper with single-column Card layout using
  CardContent for automatic section dividers
- Add radio card variant to DestinationTypeSelection for the wizard
- Replace Enable Pipelines full-page gate with a simple AlertDialog
- Add 200ms fade transition between steps
- autoFocus the name input on the connection step
- Guard against alpha gate flash by waiting for flag store
- Add beforeunload guard for dirty forms
- Document title case convention for browser tab titles
- Title case the "New Pipeline" browser tab title
This commit is contained in:
Danny White committed 2026-08-19 18:42:51 +10:00
1 parent e5f23b8824
commit d2a8da188b
10 files changed
+309 -231

No files matched your search

@@ -127,7 +127,7 @@ Supabase UI copy is:
### Use title case for page titles and global navigation
Use title case for page names in the main nav and document titles (e.g. "Database Settings", "Project Settings"). This distinguishes the page as a destination from section labels and in-page headings, which use sentence case.
Use title case for page names in the main nav and document titles (e.g. "Database Settings", "Project Settings"). This includes browser tab titles set via the `browserTitle` prop on layout components (e.g. "New Pipeline", not "New pipeline").
### Use declarative page descriptions
@@ -1,6 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { useFeatureFlags, useParams } from 'common'
import { ArrowUpRight } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
@@ -8,7 +8,21 @@ import { parseAsStringEnum, useQueryState } from 'nuqs'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useForm, useWatch } from 'react-hook-form'
import { toast } from 'sonner'
import { Button, cn, Form } from 'ui'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
Button,
CardContent,
cn,
Form,
} from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import * as z from 'zod'
import { AdvancedSettings } from '../DestinationPanel/DestinationForm/AdvancedSettings'
@@ -40,7 +54,6 @@ import { ValidationFailuresSection } from '../DestinationPanel/DestinationForm/V
import { ValidationWarningsDialog } from '../DestinationPanel/DestinationForm/ValidationWarningsDialog'
import type { DestinationType } from '../DestinationPanel/DestinationPanel.types'
import { DestinationTypeSelection } from '../DestinationPanel/DestinationTypeSelection'
import { EnablePipelinesCallout } from '../EnablePipelinesCallout'
import { LocalReplicationUnavailableAdmonition } from '../LocalReplicationUnavailableAdmonition'
import {
useIsETLBigQueryPrivateAlpha,
@@ -66,12 +79,14 @@ import { DocsButton } from '@/components/ui/DocsButton'
import { SteppedFlow } from '@/components/ui/SteppedFlow/SteppedFlow'
import { useAPIKeys } from '@/data/api-keys/api-keys-query'
import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
import { useCreateTenantSourceMutation } from '@/data/replication/create-tenant-source-mutation'
import { useReplicationPublicationsQuery } from '@/data/replication/publications-query'
import {
useReplicationSourceId,
useReplicationSourcesQuery,
} from '@/data/replication/sources-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose'
import { DOCS_URL } from '@/lib/constants'
@@ -80,6 +95,9 @@ const formId = 'create-pipeline'
export const CreatePipelineWizard = () => {
const router = useRouter()
const { ref: projectRef } = useParams()
const { isLoading: isOrgLoading } = useSelectedOrganizationQuery()
const { configcat: flagStore } = useFeatureFlags()
const isFlagStoreLoaded = Object.keys(flagStore).length > 0
const enablePgReplicate = useIsETLPrivateAlpha()
const etlEnableBigQuery = useIsETLBigQueryPrivateAlpha()
const etlEnableIceberg = useIsETLIcebergPrivateAlpha()
@@ -125,6 +143,12 @@ export const CreatePipelineWizard = () => {
)
const replicationNotEnabled = isSourcesSuccess && !externalReplicationSource
const { mutate: createTenantSource, isPending: isEnablingPipelines } =
useCreateTenantSourceMutation({
onSuccess: () => toast.success('Pipelines enabled'),
onError: (error) => toast.error(`Failed to enable Pipelines: ${error.message}`),
})
const availableDestinations = useMemo(() => {
const destinations: DestinationType[] = []
if (etlEnableBigQuery) destinations.push('BigQuery')
@@ -276,6 +300,16 @@ export const CreatePipelineWizard = () => {
})
useRegisterIsolatedStudioFlowClose(confirmOnClose)
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (isDirty || step !== 'destination') {
e.preventDefault()
}
}
window.addEventListener('beforeunload', handleBeforeUnload)
return () => window.removeEventListener('beforeunload', handleBeforeUnload)
}, [isDirty, step])
const canContinueFromDestination = selectedType !== null
const canContinueFromConnection =
selectedType !== null && hasValidConnection({ type: selectedType, data: formValues })
@@ -404,6 +438,14 @@ export const CreatePipelineWizard = () => {
? `${DOCS_URL}/guides/database/replication/bigquery#configure-bigquery-as-a-destination`
: `${DOCS_URL}/guides/database/replication/pipelines#step-3-configure-a-destination`
if (isOrgLoading || !isFlagStoreLoaded) {
return (
<div className="mx-auto w-full max-w-[760px] px-6 py-8">
<GenericSkeletonLoader />
</div>
)
}
if (!enablePgReplicate) {
return (
<CreatePipelineGate
@@ -437,12 +479,13 @@ export const CreatePipelineWizard = () => {
if (replicationNotEnabled) {
return (
<CreatePipelineGate
title="Enable Pipelines"
description="Turn on Pipelines for this project before creating a destination."
>
<EnablePipelinesCallout className="p-6!" type={selectedType} />
</CreatePipelineGate>
<EnablePipelinesAlertDialog
onEnable={() => {
if (projectRef) createTenantSource({ projectRef })
}}
isEnabling={isEnablingPipelines}
onCancel={goToList}
/>
)
}
@@ -475,92 +518,110 @@ export const CreatePipelineWizard = () => {
}}
>
{step === 'destination' && (
<section className="space-y-2">
<h2 className="text-lg text-foreground">Choose a destination</h2>
<p className="text-sm text-foreground-light">
Where should this database be replicated?
</p>
<LocalReplicationUnavailableAdmonition className="mt-4" />
<DestinationTypeSelection hideReadReplica className="p-0 pt-4" />
</section>
<>
<CardContent className="space-y-1">
<h2 className="text-lg text-foreground">Choose a destination</h2>
<p className="text-sm text-foreground-light">
Where should this database be replicated?
</p>
<LocalReplicationUnavailableAdmonition className="pt-2" />
</CardContent>
<CardContent>
<DestinationTypeSelection variant="radio" hideReadReplica />
</CardContent>
</>
)}
{step === 'connection' && selectedType && (
<section className="space-y-6">
<div className="space-y-1">
<>
<CardContent className="space-y-1">
<h2 className="text-lg text-foreground">Authorize the destination</h2>
<p className="text-sm text-foreground-light">
Name this pipeline and enter credentials for {selectedType}.
</p>
</div>
<DestinationNameInput form={form} />
<PipelineRegionField />
{selectedType === 'BigQuery' && etlEnableBigQuery && (
<BigQueryFields form={form} editMode={false} className="p-0" />
)}
{selectedType === 'Analytics Bucket' && etlEnableIceberg && (
<AnalyticsBucketFields
form={form}
editMode={false}
className="p-0"
onSelectNewBucket={() => setNewBucketSheetVisible(true)}
/>
)}
{selectedType === 'DuckLake' && etlEnableDucklake && (
<DuckLakeFields form={form} editMode={false} className="p-0" />
)}
{selectedType === 'Snowflake' && etlEnableSnowflake && (
<SnowflakeFields form={form} editMode={false} className="p-0" />
)}
{selectedType === 'ClickHouse' && etlEnableClickHouse && (
<ClickHouseFields form={form} editMode={false} className="p-0" />
)}
</section>
</CardContent>
<CardContent className="space-y-6">
<DestinationNameInput form={form} />
<PipelineRegionField />
</CardContent>
<CardContent>
{selectedType === 'BigQuery' && etlEnableBigQuery && (
<BigQueryFields form={form} editMode={false} className="p-0" />
)}
{selectedType === 'Analytics Bucket' && etlEnableIceberg && (
<AnalyticsBucketFields
form={form}
editMode={false}
className="p-0"
onSelectNewBucket={() => setNewBucketSheetVisible(true)}
/>
)}
{selectedType === 'DuckLake' && etlEnableDucklake && (
<DuckLakeFields form={form} editMode={false} className="p-0" />
)}
{selectedType === 'Snowflake' && etlEnableSnowflake && (
<SnowflakeFields form={form} editMode={false} className="p-0" />
)}
{selectedType === 'ClickHouse' && etlEnableClickHouse && (
<ClickHouseFields form={form} editMode={false} className="p-0" />
)}
</CardContent>
</>
)}
{step === 'data' && (
<section className="space-y-6">
<div className="space-y-1">
<>
<CardContent className="space-y-1">
<h2 className="text-lg text-foreground">Choose what to replicate</h2>
<p className="text-sm text-foreground-light">
Select a publication and which existing rows to copy during initial sync.
</p>
</div>
<PublicationSelection
form={form}
onSelectNewPublication={() => setPublicationPanelVisible(true)}
/>
<TableCopySelection form={form} editMode={false} />
</section>
</CardContent>
<CardContent>
<PublicationSelection
form={form}
onSelectNewPublication={() => setPublicationPanelVisible(true)}
/>
</CardContent>
<CardContent>
<TableCopySelection form={form} editMode={false} />
</CardContent>
</>
)}
{step === 'review' && selectedType && (
<section className="space-y-6">
<div className="space-y-1">
<>
<CardContent className="space-y-1">
<h2 className="text-lg text-foreground">Review and create</h2>
<p className="text-sm text-foreground-light">
Confirm optional settings, then create and start the pipeline.
</p>
</div>
<p className="text-sm text-foreground-light">
<span className="text-foreground">{name || 'Untitled pipeline'}</span>
{' · '}
{selectedType}
{publicationName ? ` · ${publicationName}` : ''}
</p>
<div className="-mx-5">
</CardContent>
<CardContent>
<p className="text-sm text-foreground-light">
<span className="text-foreground">{name || 'Untitled pipeline'}</span>
{' · '}
{selectedType}
{publicationName ? ` · ${publicationName}` : ''}
</p>
</CardContent>
<CardContent>
<AdvancedSettings type={selectedType} form={form} />
</div>
</CardContent>
{hasRunValidation && !isValidating && (
<div ref={validationSectionRef}>
<ValidationFailuresSection
destinationFailures={destinationValidationFailures}
pipelineFailures={pipelineValidationFailures}
/>
</div>
<CardContent>
<div ref={validationSectionRef}>
<ValidationFailuresSection
destinationFailures={destinationValidationFailures}
pipelineFailures={pipelineValidationFailures}
/>
</div>
</CardContent>
)}
<DocsButton href={docsUrl} topic={`${selectedType} pipeline settings`} />
</section>
<CardContent>
<DocsButton href={docsUrl} topic={`${selectedType} pipeline settings`} />
</CardContent>
</>
)}
</SteppedFlow>
</form>
@@ -610,3 +671,35 @@ export const CreatePipelineWizard = () => {
</>
)
}
function EnablePipelinesAlertDialog({
onEnable,
isEnabling,
onCancel,
}: {
onEnable: () => void
isEnabling: boolean
onCancel: () => void
}) {
return (
<AlertDialog open>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Enable Pipelines</AlertDialogTitle>
<AlertDialogDescription>
Pipelines replicates your database to external destinations. It is billed for configured
pipeline hours and data processed.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isEnabling} onClick={onCancel}>
Cancel
</AlertDialogCancel>
<AlertDialogAction onClick={onEnable} disabled={isEnabling}>
Enable
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
@@ -16,7 +16,7 @@ export const DestinationNameInput = ({ form }: DestinationNameInputProps) => {
render={({ field }) => (
<FormItemLayout label="Name" layout="horizontal">
<FormControl>
<Input {...field} placeholder="My destination" />
<Input {...field} autoFocus placeholder="My destination" />
</FormControl>
</FormItemLayout>
)}
@@ -1,3 +1,4 @@
import { Check } from 'lucide-react'
import { parseAsInteger, parseAsStringEnum, useQueryState } from 'nuqs'
import {
Badge,
@@ -38,12 +39,27 @@ interface DestinationTypeGroup {
options: DestinationTypeOption[]
}
function StageBadge({ stage }: { stage: DestinationTypeOption['stage'] }) {
if (!stage) return null
return (
<Badge
variant={
stage === 'Early Access' ? 'warning' : stage === 'Deprecated' ? 'destructive' : 'default'
}
>
{stage}
</Badge>
)
}
interface DestinationTypeSelectionProps {
variant?: 'select' | 'radio'
hideReadReplica?: boolean
className?: string
}
export const DestinationTypeSelection = ({
variant = 'select',
hideReadReplica = false,
className,
}: DestinationTypeSelectionProps) => {
@@ -144,9 +160,9 @@ export const DestinationTypeSelection = ({
.map((group) => ({ ...group, options: group.options.filter((option) => option.enabled) }))
.filter((group) => group.options.length > 0)
const selectedOption = visibleGroups
.flatMap((group) => group.options)
.find((option) => option.value === destinationType)
const allVisibleOptions = visibleGroups.flatMap((group) => group.options)
const selectedOption = allVisibleOptions.find((option) => option.value === destinationType)
const stageDescription =
selectedOption?.stage === 'Public Alpha' ? (
@@ -167,6 +183,53 @@ export const DestinationTypeSelection = ({
'This destination type is deprecated.'
) : null
if (variant === 'radio') {
return (
<div className={className}>
<div role="radiogroup" aria-label="Destination type" className="flex flex-col gap-3">
{allVisibleOptions.map((option) => {
const selected = destinationType === option.value
return (
<button
key={option.value}
type="button"
role="radio"
aria-checked={selected}
onClick={() => setDestinationType(option.value)}
className={cn(
'relative flex items-start gap-x-4 rounded-md border p-4 text-left transition',
'hover:border-control-hover',
selected ? 'border-control-hover bg-surface-300' : 'border-default bg-surface-100'
)}
>
<DestinationIcon
type={option.value}
size={20}
className="mt-0.5 shrink-0 text-foreground-light"
/>
<div className="flex min-w-0 flex-1 flex-col gap-y-0.5">
<div className="flex items-center gap-x-2">
<span className="text-sm text-foreground">{option.label}</span>
<StageBadge stage={option.stage} />
</div>
<span className="text-xs text-foreground-lighter">{option.description}</span>
</div>
{selected ? (
<Check size={16} className="mt-0.5 shrink-0 text-brand" />
) : (
<span className="mt-0.5 h-4 w-4 shrink-0 rounded-full border border-strong" />
)}
</button>
)
})}
</div>
{stageDescription && (
<p className="mt-3 text-xs text-foreground-lighter">{stageDescription}</p>
)}
</div>
)
}
const typeDescription =
!editMode || stageDescription ? (
<span>
@@ -199,19 +262,7 @@ export const DestinationTypeSelection = ({
/>
<div className="flex items-center gap-x-2">
<span className="text-sm text-foreground">{selectedOption.label}</span>
{selectedOption.stage && (
<Badge
variant={
selectedOption.stage === 'Early Access'
? 'warning'
: selectedOption.stage === 'Deprecated'
? 'destructive'
: 'default'
}
>
{selectedOption.stage}
</Badge>
)}
<StageBadge stage={selectedOption.stage} />
</div>
</div>
) : (
@@ -234,19 +285,7 @@ export const DestinationTypeSelection = ({
<div className="flex flex-col gap-y-0.5">
<div className="flex items-center gap-x-2">
<span className="text-foreground">{option.label}</span>
{option.stage && (
<Badge
variant={
option.stage === 'Early Access'
? 'warning'
: option.stage === 'Deprecated'
? 'destructive'
: 'default'
}
>
{option.stage}
</Badge>
)}
<StageBadge stage={option.stage} />
</div>
<span className="text-xs text-foreground-lighter">{option.description}</span>
</div>
@@ -24,12 +24,13 @@ import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
import { DOCS_URL } from '@/lib/constants'
type EnablePipelinesModalProps =
| { open: boolean; onOpenChange: (open: boolean) => void }
| { open?: never; onOpenChange?: never }
| { open: boolean; onOpenChange: (open: boolean) => void; onCancel?: () => void }
| { open?: never; onOpenChange?: never; onCancel?: never }
export const EnablePipelinesModal = ({
open: extOpen,
onOpenChange,
onCancel,
}: EnablePipelinesModalProps) => {
const { ref: projectRef } = useParams()
const [_open, _setOpen] = useState(false)
@@ -101,7 +102,14 @@ export const EnablePipelinesModal = ({
</Admonition>
</DialogSection>
<DialogFooter>
<Button variant="default" disabled={creatingTenantSource} onClick={() => setOpen(false)}>
<Button
variant="default"
disabled={creatingTenantSource}
onClick={() => {
setOpen(false)
onCancel?.()
}}
>
Cancel
</Button>
{hasAccess ? (
@@ -5,23 +5,19 @@ import { SteppedFlow } from './SteppedFlow'
import { customRender } from '@/tests/lib/custom-render'
const steps = [
{ id: 'destination', label: 'Destination', description: 'Choose where data should go' },
{ id: 'connection', label: 'Connection', description: 'Authorize the destination' },
{ id: 'review', label: 'Review', description: 'Confirm and create' },
{ id: 'destination', label: 'Destination' },
{ id: 'connection', label: 'Connection' },
{ id: 'review', label: 'Review' },
]
describe('SteppedFlow', () => {
test('does not let the user jump ahead of the current step', () => {
const onStepChange = vi.fn()
test('does not show Back on the first step', () => {
customRender(
<SteppedFlow steps={steps} currentStep="destination" onStepChange={onStepChange}>
<SteppedFlow steps={steps} currentStep="destination" onStepChange={vi.fn()}>
Step body
</SteppedFlow>
)
fireEvent.click(screen.getByRole('button', { name: 'Connection, not yet available' }))
expect(onStepChange).not.toHaveBeenCalled()
expect(screen.queryByRole('button', { name: 'Back' })).not.toBeInTheDocument()
})
@@ -38,19 +34,6 @@ describe('SteppedFlow', () => {
expect(onStepChange).toHaveBeenCalledWith('destination')
})
test('lets the user return to a completed step', () => {
const onStepChange = vi.fn()
customRender(
<SteppedFlow steps={steps} currentStep="connection" onStepChange={onStepChange}>
Step body
</SteppedFlow>
)
fireEvent.click(screen.getByRole('button', { name: 'Go to completed step: Destination' }))
expect(onStepChange).toHaveBeenCalledWith('destination')
})
test('shows the final action on the last step instead of Continue', () => {
const onFinal = vi.fn()
@@ -1,10 +1,9 @@
import { type ReactNode } from 'react'
import { Button, cn } from 'ui'
import { Button, Card, cn } from 'ui'
export type SteppedFlowStep = {
id: string
label: string
description?: string
}
export type SteppedFlowFinalAction = {
@@ -48,107 +47,52 @@ export const SteppedFlow = ({
return (
<div className="flex min-h-full flex-col">
<div className="mx-auto grid w-full max-w-[924px] flex-1 grid-cols-1 gap-6 px-6 pt-8 pb-10 lg:grid-cols-[minmax(0,760px)_140px]">
<div className="min-w-0">
<p className="mb-4 text-xs text-foreground-lighter lg:hidden">
Step {currentIndex + 1} of {steps.length}
{currentStepLabel ? ` · ${currentStepLabel}` : ''}
</p>
<main className="min-w-0">{children}</main>
<footer
className={cn(
'mt-6 flex items-center border-t pt-4',
currentIndex > 0 ? 'justify-between' : 'justify-end'
)}
>
{currentIndex > 0 && (
<Button
type="button"
variant="default"
onClick={() => onStepChange(steps[currentIndex - 1].id)}
>
Back
</Button>
)}
{isLastStep && finalAction ? (
<Button
type={finalAction.type ?? (finalAction.form ? 'submit' : 'button')}
form={finalAction.form}
variant="primary"
loading={finalAction.loading}
disabled={finalAction.disabled}
onClick={finalAction.onClick}
>
{finalAction.label}
</Button>
) : (
<Button
type="button"
variant="primary"
disabled={nextDisabled}
loading={nextLoading}
onClick={onNext}
>
{nextLabel}
</Button>
)}
</footer>
</div>
<nav
aria-label="Setup progress"
className="sticky top-6 hidden h-fit flex-col gap-0.5 lg:flex"
<div className="mx-auto w-full max-w-[760px] flex-1 px-6 pt-8 pb-10">
<p className="mb-4 text-xs text-foreground-lighter">
Step {currentIndex + 1} of {steps.length}
{currentStepLabel ? ` · ${currentStepLabel}` : ''}
</p>
<Card key={currentStep} className="animate-in fade-in-0 duration-200">
{children}
</Card>
<footer
className={cn(
'mt-6 flex items-center',
currentIndex > 0 ? 'justify-between' : 'justify-end'
)}
>
{steps.map((step, index) => {
const isCurrent = index === currentIndex
const isComplete = index < currentIndex
const isReachable = index <= currentIndex
return (
<button
key={step.id}
type="button"
tabIndex={isReachable ? 0 : -1}
aria-current={isCurrent ? 'step' : undefined}
aria-label={
isCurrent
? `Current step: ${step.label}`
: isComplete
? `Go to completed step: ${step.label}`
: `${step.label}, not yet available`
}
disabled={!isReachable}
onClick={() => {
if (!isReachable) return
onStepChange(step.id)
}}
className={cn(
'grid grid-cols-[12px_minmax(0,1fr)] items-start gap-2 rounded-md px-2 py-1.5 text-left',
isCurrent ? 'text-foreground' : 'text-foreground-light',
isReachable && 'hover:bg-surface-200 hover:text-foreground',
!isReachable && 'cursor-not-allowed opacity-60'
)}
>
<span
className={cn(
'mt-1.5 h-2 w-2 rounded-full border',
isComplete && 'border-brand bg-brand',
isCurrent && !isComplete && 'border-foreground bg-foreground',
!isCurrent && !isComplete && 'border-foreground-muted bg-transparent'
)}
/>
<span className="min-w-0">
<strong className="block text-sm font-medium">{step.label}</strong>
{step.description && (
<small className="block text-xs font-normal text-foreground-lighter">
{step.description}
</small>
)}
</span>
</button>
)
})}
</nav>
{currentIndex > 0 && (
<Button
type="button"
variant="default"
onClick={() => onStepChange(steps[currentIndex - 1].id)}
>
Back
</Button>
)}
{isLastStep && finalAction ? (
<Button
type={finalAction.type ?? (finalAction.form ? 'submit' : 'button')}
form={finalAction.form}
variant="primary"
loading={finalAction.loading}
disabled={finalAction.disabled}
onClick={finalAction.onClick}
>
{finalAction.label}
</Button>
) : (
<Button
type="button"
variant="primary"
disabled={nextDisabled}
loading={nextLoading}
onClick={onNext}
>
{nextLabel}
</Button>
)}
</footer>
</div>
</div>
)
+11
View File
@@ -1,3 +1,14 @@
/**
* Browser tab title segments.
*
* Use title case for `section` and `entity` values
* (e.g. "New Pipeline", not "New pipeline").
* Product names like "Database" or "Auth" are proper nouns and keep their
* original casing in `surface`.
*
* The final title is built as:
* entity | section | surface | project | org | brand
*/
export interface StudioPageTitleParts {
entity?: string
section?: string
@@ -57,7 +57,7 @@ DatabaseReplicationNewPage.getLayout = (page) => (
<DefaultLayout hideMobileMenu headerTitle="New pipeline">
<ProjectLayoutWithAuth
product="Database"
browserTitle={{ section: 'New pipeline' }}
browserTitle={{ section: 'New Pipeline' }}
isBlocking={false}
>
{page}
@@ -14,7 +14,7 @@ function DatabaseReplicationNewRoute() {
return (
<ProjectLayoutWithAuth
product="Database"
browserTitle={{ section: 'New pipeline' }}
browserTitle={{ section: 'New Pipeline' }}
isBlocking={false}
>
<DatabaseReplicationNewPage dehydratedState={undefined} />