index.tsx185 lines · main
1import { useQueryClient } from '@tanstack/react-query'
2import { Background, ColorMode, ReactFlow, ReactFlowProvider, useReactFlow } from '@xyflow/react'
3import { useParams } from 'common'
4import { useTheme } from 'next-themes'
5import { useEffect, useMemo } from 'react'
6
7import { getStatusName } from '../Pipeline.utils'
8import { PrimaryDatabaseNode, ReadReplicaNode, ReplicationNode } from './Nodes'
9import { getDagreGraphLayout } from './ReplicationDiagram.utils'
10import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
11import { useReplicationDestinationsQuery } from '@/data/replication/destinations-query'
12import { replicationKeys } from '@/data/replication/keys'
13import { ReplicationPipelineStatusResponse } from '@/data/replication/pipeline-status-query'
14import { useReplicationPipelinesQuery } from '@/data/replication/pipelines-query'
15import { timeout } from '@/lib/helpers'
16
17import '@xyflow/react/dist/style.css'
18
19import { SmoothstepEdge } from './Edges'
20import { REPLICA_STATUS } from '@/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants'
21
22export const ReplicationDiagram = () => {
23 return (
24 <ReactFlowProvider>
25 <ReplicationDiagramContent />
26 </ReactFlowProvider>
27 )
28}
29
30const nodeTypes = {
31 primary: PrimaryDatabaseNode,
32 replication: ReplicationNode,
33 readReplica: ReadReplicaNode,
34}
35
36const edgeTypes = { smoothstep: SmoothstepEdge }
37
38const ReplicationDiagramContent = () => {
39 const reactFlow = useReactFlow()
40 const { resolvedTheme } = useTheme()
41 const queryClient = useQueryClient()
42 const { ref: projectRef = 'default' } = useParams()
43
44 const { data: databases = [], isSuccess: isSuccessReplicas } = useReadReplicasQuery({
45 projectRef,
46 })
47 const readReplicas = useMemo(
48 () => databases.filter((x) => x.identifier !== projectRef),
49 [databases, projectRef]
50 )
51
52 const { data, isSuccess: isSuccessDestinations } = useReplicationDestinationsQuery({
53 projectRef,
54 })
55 const destinations = useMemo(() => data?.destinations ?? [], [data])
56
57 const { data: pipelinesData } = useReplicationPipelinesQuery({ projectRef })
58
59 const nodes = useMemo(() => {
60 return [
61 { id: projectRef, type: 'primary', data: {}, position: { x: 0, y: 5 } },
62 ...readReplicas.map((x) => ({
63 id: x.identifier,
64 type: 'readReplica',
65 data: {},
66 position: { x: 0, y: 0 },
67 })),
68 ...destinations.map((x) => ({
69 id: x.id.toString(),
70 type: 'replication',
71 data: {},
72 position: { x: 0, y: 0 },
73 })),
74 ]
75 }, [destinations, projectRef, readReplicas])
76
77 const edges = useMemo(() => {
78 return [
79 ...readReplicas.map((x) => {
80 const isReplicating = x.status === 'ACTIVE_HEALTHY'
81
82 return {
83 id: `${projectRef}-${x.identifier}`,
84 source: projectRef,
85 target: x.identifier,
86 type: 'smoothstep',
87 className: 'cursor-default!',
88 animated: isReplicating,
89 style: {
90 opacity: isReplicating ? 1 : 0.4,
91 strokeDasharray: isReplicating ? undefined : '5 5',
92 },
93 data: {
94 type: 'replica',
95 identifier: x.identifier,
96 shiftEdgeEnd: readReplicas.length + destinations.length > 1,
97 isReplicating,
98 isComingUp: [
99 REPLICA_STATUS.COMING_UP,
100 REPLICA_STATUS.INIT_READ_REPLICA,
101 REPLICA_STATUS.UNKNOWN,
102 ].includes(x.status),
103 isFailed: [REPLICA_STATUS.ACTIVE_UNHEALTHY, REPLICA_STATUS.INIT_FAILED].includes(
104 x.status
105 ),
106 },
107 }
108 }),
109 ...destinations.map((x) => {
110 const pipeline = (pipelinesData?.pipelines ?? []).find((p) => p.destination_id === x.id)
111 const pipelineStatus = queryClient.getQueryData(
112 replicationKeys.pipelinesStatus(projectRef, pipeline?.id)
113 ) as ReplicationPipelineStatusResponse
114 const statusName = getStatusName(pipelineStatus?.status)
115 const isReplicating = statusName === 'started'
116
117 return {
118 id: `${projectRef}-${x.id}`,
119 source: projectRef,
120 target: x.id.toString(),
121 type: 'smoothstep',
122 className: 'cursor-default!',
123 animated: isReplicating,
124 style: {
125 opacity: isReplicating ? 1 : 0.4,
126 strokeDasharray: isReplicating ? undefined : '5 5',
127 },
128 data: {
129 type: 'etl',
130 identifier: x.id,
131 shiftEdgeEnd: readReplicas.length + destinations.length > 1,
132 isReplicating,
133 isComingUp: ['starting'].includes(statusName ?? ''),
134 isFailed: ['failed'].includes(statusName ?? ''),
135 },
136 }
137 }),
138 ]
139 }, [destinations, pipelinesData?.pipelines, projectRef, queryClient, readReplicas])
140
141 const backgroundPatternColor =
142 resolvedTheme === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.4)'
143
144 const setReactFlow = async () => {
145 const graph = getDagreGraphLayout(nodes, edges)
146 reactFlow.setNodes(graph.nodes)
147 reactFlow.setEdges(graph.edges)
148
149 // [Joshen] Odd fix to ensure that react flow snaps back to center when adding nodes
150 await timeout(1)
151 reactFlow.fitView({ minZoom: 0.8, maxZoom: 0.9 })
152 }
153
154 useEffect(() => {
155 if (nodes.length > 0 && isSuccessDestinations && isSuccessReplicas) {
156 setReactFlow()
157 }
158 }, [nodes, isSuccessDestinations, isSuccessReplicas])
159
160 return (
161 <div className="nowheel relative min-h-[350px]">
162 <ReactFlow
163 // FIXME: https://github.com/xyflow/xyflow/issues/4876
164 colorMode={'' as unknown as ColorMode}
165 fitView
166 fitViewOptions={{ minZoom: 0.8, maxZoom: 0.9 }}
167 className="bg"
168 zoomOnPinch={false}
169 zoomOnScroll={false}
170 nodesDraggable={false}
171 nodesConnectable={false}
172 zoomOnDoubleClick={false}
173 edgesFocusable={false}
174 edgesReconnectable={false}
175 defaultNodes={[]}
176 defaultEdges={[]}
177 nodeTypes={nodeTypes}
178 edgeTypes={edgeTypes}
179 proOptions={{ hideAttribution: true }}
180 >
181 <Background color={backgroundPatternColor} />
182 </ReactFlow>
183 </div>
184 )
185}