mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 11:25:06 +03:00
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>
193 lines
5.7 KiB
TypeScript
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>
|
|
</>
|
|
)
|
|
}
|