mirror of
https://github.com/supabase/supabase.git
synced 2026-06-14 05:06:27 +08:00
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 -->
137 lines
4.2 KiB
TypeScript
137 lines
4.2 KiB
TypeScript
import { Background, ColorMode, ReactFlow, ReactFlowProvider, useReactFlow } from '@xyflow/react'
|
|
import { useParams } from 'common'
|
|
import { useTheme } from 'next-themes'
|
|
import { useEffect, useMemo } from 'react'
|
|
|
|
import { PrimaryDatabaseNode, ReadReplicaNode, ReplicationNode } from './Nodes'
|
|
import { getDagreGraphLayout } from './ReplicationDiagram.utils'
|
|
import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
|
|
import { useReplicationDestinationsQuery } from '@/data/replication/destinations-query'
|
|
import { timeout } from '@/lib/helpers'
|
|
|
|
import '@xyflow/react/dist/style.css'
|
|
|
|
import { SmoothstepEdge } from './Edges'
|
|
|
|
export const ReplicationDiagram = () => {
|
|
return (
|
|
<ReactFlowProvider>
|
|
<ReplicationDiagramContent />
|
|
</ReactFlowProvider>
|
|
)
|
|
}
|
|
|
|
const nodeTypes = {
|
|
primary: PrimaryDatabaseNode,
|
|
replication: ReplicationNode,
|
|
readReplica: ReadReplicaNode,
|
|
}
|
|
|
|
const edgeTypes = { smoothstep: SmoothstepEdge }
|
|
|
|
const ReplicationDiagramContent = () => {
|
|
const reactFlow = useReactFlow()
|
|
const { resolvedTheme } = useTheme()
|
|
const { ref: projectRef = 'default' } = useParams()
|
|
|
|
const { data: databases = [], isSuccess: isSuccessReplicas } = useReadReplicasQuery({
|
|
projectRef,
|
|
})
|
|
const readReplicas = useMemo(
|
|
() => databases.filter((x) => x.identifier !== projectRef),
|
|
[databases, projectRef]
|
|
)
|
|
|
|
const { data, isSuccess: isSuccessDestinations } = useReplicationDestinationsQuery({
|
|
projectRef,
|
|
})
|
|
const destinations = useMemo(() => data?.destinations ?? [], [data])
|
|
|
|
const nodes = useMemo(() => {
|
|
return [
|
|
{ id: projectRef, type: 'primary', data: {}, position: { x: 0, y: 5 } },
|
|
...readReplicas.map((x) => ({
|
|
id: x.identifier,
|
|
type: 'readReplica',
|
|
data: {},
|
|
position: { x: 0, y: 0 },
|
|
})),
|
|
...destinations.map((x) => ({
|
|
id: x.id.toString(),
|
|
type: 'replication',
|
|
data: {},
|
|
position: { x: 0, y: 0 },
|
|
})),
|
|
]
|
|
}, [destinations, projectRef, readReplicas])
|
|
|
|
const edges = useMemo(() => {
|
|
const shiftEdgeEnd = readReplicas.length + destinations.length > 1
|
|
|
|
return [
|
|
...readReplicas.map((x) => ({
|
|
id: `${projectRef}-${x.identifier}`,
|
|
source: projectRef,
|
|
target: x.identifier,
|
|
type: 'smoothstep',
|
|
className: 'cursor-default!',
|
|
// The edge subscribes to live status itself (see Edges.tsx) so it stays in sync with nodes.
|
|
data: { type: 'replica', identifier: x.identifier, shiftEdgeEnd },
|
|
})),
|
|
...destinations.map((x) => ({
|
|
id: `${projectRef}-${x.id}`,
|
|
source: projectRef,
|
|
target: x.id.toString(),
|
|
type: 'smoothstep',
|
|
className: 'cursor-default!',
|
|
data: { type: 'etl', identifier: x.id.toString(), shiftEdgeEnd },
|
|
})),
|
|
]
|
|
}, [destinations, projectRef, readReplicas])
|
|
|
|
const backgroundPatternColor =
|
|
resolvedTheme === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.4)'
|
|
|
|
const setReactFlow = async () => {
|
|
const graph = getDagreGraphLayout(nodes, edges)
|
|
reactFlow.setNodes(graph.nodes)
|
|
reactFlow.setEdges(graph.edges)
|
|
|
|
// [Joshen] Odd fix to ensure that react flow snaps back to center when adding nodes
|
|
await timeout(1)
|
|
reactFlow.fitView({ minZoom: 0.8, maxZoom: 0.9 })
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (nodes.length > 0 && isSuccessDestinations && isSuccessReplicas) {
|
|
setReactFlow()
|
|
}
|
|
}, [nodes, isSuccessDestinations, isSuccessReplicas])
|
|
|
|
return (
|
|
<div className="nowheel relative min-h-[350px]">
|
|
<ReactFlow
|
|
// FIXME: https://github.com/xyflow/xyflow/issues/4876
|
|
colorMode={'' as unknown as ColorMode}
|
|
fitView
|
|
fitViewOptions={{ minZoom: 0.8, maxZoom: 0.9 }}
|
|
className="bg"
|
|
zoomOnPinch={false}
|
|
zoomOnScroll={false}
|
|
nodesDraggable={false}
|
|
nodesConnectable={false}
|
|
zoomOnDoubleClick={false}
|
|
edgesFocusable={false}
|
|
edgesReconnectable={false}
|
|
defaultNodes={[]}
|
|
defaultEdges={[]}
|
|
nodeTypes={nodeTypes}
|
|
edgeTypes={edgeTypes}
|
|
proOptions={{ hideAttribution: true }}
|
|
>
|
|
<Background color={backgroundPatternColor} />
|
|
</ReactFlow>
|
|
</div>
|
|
)
|
|
}
|