Files
Stephen MorganandJoshen Lim 9dc5c0faf3 feat: privatelink read replica (#48642)
Frontend changes for new privatelink functionality.
The updates to the API are already live, but will be putting the UI
changes behind a feature flag while we do some full end to end testing.

Changes to Integration page:
<img width="744" height="456" alt="image"
src="https://github.com/user-attachments/assets/02026083-4b61-4f36-927b-1bac42cb73e2"
/>

Changes to create associations page:
<img width="859" height="411" alt="image"
src="https://github.com/user-attachments/assets/303b4ec5-3b5a-4eaf-8d22-45e1e63a0705"
/>

Fixes SEC-919

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

- **New Features**
- AWS PrivateLink connections can target the primary database or a read
replica.
- Connection lists and confirmation dialogs now identify the selected
database clearly.
- Replica details include improved status information and a “Manage
replica” link.

- **Bug Fixes**
- Replica removal errors now provide clearer guidance and link to
integration settings when PrivateLink issues occur.

- **Updates**
- Infrastructure diagrams focus on visualization, with replica
management actions moved to dedicated management views.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-08-04 20:47:48 +12:00

193 lines
5.7 KiB
TypeScript

import { BaseEdge, EdgeLabelRenderer, getSmoothStepPath, type EdgeProps } from '@xyflow/react'
import { useParams } from 'common'
import { ArrowRight, Loader2, Square, X, type LucideIcon } from 'lucide-react'
import { useMemo } from 'react'
import { cn } from 'ui'
import { getStatusName } from '../Pipeline.utils'
import { REPLICA_STATUS, STATUS_REFRESH_FREQUENCY_MS } from '../Replication.constants'
import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
import { useReplicationPipelineStatusQuery } from '@/data/replication/pipeline-status-query'
import { useReplicationPipelinesQuery } from '@/data/replication/pipelines-query'
import {
PipelineStatusRequestStatus,
usePipelineRequestStatus,
} from '@/state/replication-pipeline-request-status'
type EdgeData = {
type: string
identifier: string
shiftEdgeEnd: boolean
}
interface ReplicationState {
isComingUp: boolean
isReplicating: boolean
isFailed: boolean
}
interface EdgeVisual {
Icon: LucideIcon
// CSS color shared by the icon and the connecting line so they always match.
color: string
opacity: number
dashArray: string
shouldAnimate: boolean
shouldSpin?: boolean
isFilled?: boolean
strokeWidth?: number
}
// Picks the icon + line appearance for a replication state. Both the icon and the line are derived
// here from the same state so they always stay in sync. We deliberately don't surface lag: the line
// just communicates whether data is moving, stopped, starting, or broken.
const getEdgeVisual = ({ isComingUp, isReplicating, isFailed }: ReplicationState): EdgeVisual => {
if (isFailed) {
return {
Icon: X,
color: 'hsl(var(--destructive-default))',
opacity: 1,
dashArray: '5 5',
shouldAnimate: false,
strokeWidth: 4,
}
}
if (isComingUp) {
return {
Icon: Loader2,
color: 'var(--foreground-light)',
opacity: 1,
dashArray: '5',
shouldAnimate: true,
shouldSpin: true,
}
}
if (isReplicating) {
return {
Icon: ArrowRight,
color: 'hsl(var(--brand-default))',
opacity: 1,
dashArray: '5',
shouldAnimate: true,
}
}
return {
Icon: Square,
color: 'var(--foreground-lighter)',
opacity: 0.5,
dashArray: '5 5',
shouldAnimate: false,
isFilled: true,
}
}
export const SmoothstepEdge = ({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerEnd,
data,
}: EdgeProps) => {
const { ref: projectRef = 'default' } = useParams()
const { type, identifier, shiftEdgeEnd } = (data || {}) as EdgeData
const isReplica = type === 'replica'
// Subscribe to the same live status the nodes use, so the line and the node update together.
const { data: databases = [] } = useReadReplicasQuery(
{ projectRef },
{ enabled: isReplica, refetchInterval: STATUS_REFRESH_FREQUENCY_MS }
)
const replica = databases.find((x) => x.identifier === identifier)
const { data: pipelinesData } = useReplicationPipelinesQuery(
{ projectRef },
{ enabled: !isReplica }
)
const pipeline = (pipelinesData?.pipelines ?? []).find(
(p) => p.destination_id.toString() === identifier
)
const { data: pipelineStatusData } = useReplicationPipelineStatusQuery(
{ projectRef, pipelineId: pipeline?.id },
{ enabled: !isReplica && !!pipeline?.id, refetchInterval: STATUS_REFRESH_FREQUENCY_MS }
)
const { getRequestStatus } = usePipelineRequestStatus()
const requestStatus = pipeline?.id
? getRequestStatus(pipeline.id)
: PipelineStatusRequestStatus.None
const replicationState = useMemo<ReplicationState>(() => {
if (isReplica) {
const status = replica?.status
return {
isReplicating: status === 'ACTIVE_HEALTHY',
isComingUp:
status !== undefined &&
[
REPLICA_STATUS.COMING_UP,
REPLICA_STATUS.INIT_READ_REPLICA,
REPLICA_STATUS.UNKNOWN,
].includes(status),
isFailed:
status !== undefined &&
[REPLICA_STATUS.ACTIVE_UNHEALTHY, REPLICA_STATUS.INIT_FAILED].includes(status),
}
}
const isTransitioning = requestStatus !== PipelineStatusRequestStatus.None
const statusName = getStatusName(pipelineStatusData?.status)
return {
isReplicating: statusName === 'started' && !isTransitioning,
isComingUp: isTransitioning || statusName === 'starting' || statusName === 'stopping',
isFailed: statusName === 'failed',
}
}, [isReplica, replica?.status, pipelineStatusData?.status, requestStatus])
const [edgePath, labelX, labelY] = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
})
const { Icon, color, opacity, dashArray, shouldAnimate, shouldSpin, isFilled, strokeWidth } =
getEdgeVisual(replicationState)
return (
<>
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{
...style,
stroke: color,
opacity,
strokeDasharray: dashArray,
animation: shouldAnimate ? 'dashdraw 0.5s linear infinite' : undefined,
}}
/>
<EdgeLabelRenderer>
<div
className="bg-surface-100 p-1 rounded-sm absolute nodrag nopan border"
style={{
transform: `translate(-50%, -50%) translate(${shiftEdgeEnd ? targetX - 30 : labelX}px,${shiftEdgeEnd ? targetY : labelY}px)`,
}}
>
<Icon
size={12}
strokeWidth={strokeWidth ?? 2}
fill={isFilled ? 'currentColor' : 'none'}
className={cn(shouldSpin && 'animate-spin')}
style={{ color }}
/>
</div>
</EdgeLabelRenderer>
</>
)
}