Files
supabase/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx
T
Riccardo Busetti 324c724117 ref(replication): Improve replication copy and UI (#46793)
This PR improves the replication UI in the following ways:
- Adds a new selecion picker for destinations which is split by the
destination location and it's clearer and can scale more when we add
more destinations.
- Adds a much improved section on lag, highlighting new metrics that
could help debug issues more easily.
- Improves the copy across the whole code.
- Fixes the 2d topological view of replication with better status
handling.

### Screenshots

<img width="1270" height="777" alt="image"
src="https://github.com/user-attachments/assets/0ffc890e-2f80-47e5-bdb1-75071adda024"
/>

<img width="1665" height="656" alt="image"
src="https://github.com/user-attachments/assets/23a27a02-acb2-4891-af95-5bc1d6ec7bfe"
/>

<img width="1454" height="247" alt="image"
src="https://github.com/user-attachments/assets/c8799983-aa63-42b2-9370-ae4e009c1573"
/>

<img width="1120" height="340" alt="image"
src="https://github.com/user-attachments/assets/20a18ad6-e5a9-40ec-80d4-42d6f783d868"
/>

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

* **New Features**
  * Live slot health indicators, legend, and connection badges.
  * Grouped destination type dropdown with alpha badges.

* **Improvements**
* Clearer UI copy for external destinations, alpha disclaimers, and
onboarding flows.
* Consolidated "n/a" handling for lag displays and richer metric
tooltips.
* Simplified replication diagram visuals and clearer table/row
status/lag presentation.
* Replication status responses now include expanded slot health and lag
metrics.

* **Tests**
* New test suites covering destination selection and destination row
states.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-11 10:58:36 +00:00

168 lines
5.6 KiB
TypeScript

import { ArrowUpRight } from 'lucide-react'
import Link from 'next/link'
import { parseAsInteger, parseAsStringEnum, useQueryState } from 'nuqs'
import { useEffect } from 'react'
import { toast } from 'sonner'
import {
Button,
cn,
DialogSectionSeparator,
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetSection,
SheetTitle,
} from 'ui'
import { EnableReplicationCallout } from '../EnableReplicationCallout'
import { PipelineStatusName } from '../Replication.constants'
import { useDestinationInformation } from '../useDestinationInformation'
import { useIsETLPrivateAlpha } from '../useIsETLPrivateAlpha'
import { DestinationForm } from './DestinationForm'
import { DestinationType } from './DestinationPanel.types'
import { DestinationTypeSelection } from './DestinationTypeSelection'
import { ReadReplicaForm } from './ReadReplicaForm'
import { DocsButton } from '@/components/ui/DocsButton'
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
import { DOCS_URL } from '@/lib/constants'
interface DestinationPanelProps {
onSuccessCreateReadReplica?: () => void
}
export const DestinationPanel = ({ onSuccessCreateReadReplica }: DestinationPanelProps) => {
const enablePgReplicate = useIsETLPrivateAlpha()
const { hasAccess: hasETLReplicationAccess } = useCheckEntitlements('replication.etl')
const [urlDestinationType, setDestinationType] = useQueryState(
'destinationType',
parseAsStringEnum<DestinationType>([
'Read Replica',
'BigQuery',
'Analytics Bucket',
'DuckLake',
'Snowflake',
]).withOptions({
history: 'push',
clearOnDefault: true,
})
)
const [edit, setEdit] = useQueryState(
'edit',
parseAsInteger.withOptions({
history: 'push',
clearOnDefault: true,
})
)
const visible = urlDestinationType !== null || edit !== null
const editMode = edit !== null
const {
sourceId,
pipeline,
statusName,
replicationNotEnabled,
type: existingDestinationType,
destinationFetcher,
} = useDestinationInformation({ id: edit })
const destinationType = existingDestinationType ?? urlDestinationType
const invalidExistingDestination = destinationFetcher.error?.code === 404
const existingDestination = editMode
? {
sourceId,
destinationId: edit,
pipelineId: pipeline?.id,
statusName,
enabled:
statusName === PipelineStatusName.STARTED || statusName === PipelineStatusName.FAILED,
}
: undefined
const onClose = () => {
setDestinationType(null)
setEdit(null)
}
useEffect(() => {
if (edit !== null && invalidExistingDestination) {
toast(`Unable to find destination ID ${edit}`)
setEdit(null)
}
}, [edit, invalidExistingDestination, setEdit])
return (
<>
<Sheet open={visible} onOpenChange={onClose}>
<SheetContent size="default" showClose={false} className="md:w-[850px]!">
<div className="flex flex-col h-full" tabIndex={-1}>
<SheetHeader>
<SheetTitle>{editMode ? 'Edit destination' : 'Create a new destination'}</SheetTitle>
<SheetDescription>
{editMode
? 'Update the configuration for this destination'
: 'A destination can be a read replica or an external destination that receives your database changes in real time.'}
</SheetDescription>
</SheetHeader>
<DestinationTypeSelection />
<DialogSectionSeparator />
{destinationType === 'Read Replica' ? (
<ReadReplicaForm onClose={onClose} onSuccess={() => onSuccessCreateReadReplica?.()} />
) : !enablePgReplicate ? (
<SheetSection>
<div className={cn('border rounded-md p-6 flex flex-col gap-y-4')}>
<div className="flex flex-col gap-y-1">
<h4>Replicate data to external destinations in real time</h4>
<p className="text-sm text-foreground-light">
External destinations are in <span className="text-foreground">alpha</span>{' '}
and are being rolled out gradually. Request access below to join the waitlist.
Read replicas are available now.
</p>
</div>
<div className="flex gap-x-2">
<Button
asChild
type="secondary"
iconRight={<ArrowUpRight size={16} strokeWidth={1.5} />}
>
<Link
href="https://forms.supabase.com/pg_replicate"
target="_blank"
rel="noreferrer"
>
Request alpha access
</Link>
</Button>
<DocsButton href={`${DOCS_URL}/guides/database/replication#replication`} />
</div>
</div>
</SheetSection>
) : replicationNotEnabled ? (
<SheetSection>
<EnableReplicationCallout
className="p-6!"
type={destinationType}
hasAccess={hasETLReplicationAccess}
/>
</SheetSection>
) : (
<DestinationForm
visible={visible}
selectedType={destinationType ?? 'Read Replica'}
existingDestination={existingDestination}
onClose={onClose}
/>
)}
</div>
</SheetContent>
</Sheet>
</>
)
}