index.tsx185 lines · main
| 1 | import { useQueryClient } from '@tanstack/react-query' |
| 2 | import { Background, ColorMode, ReactFlow, ReactFlowProvider, useReactFlow } from '@xyflow/react' |
| 3 | import { useParams } from 'common' |
| 4 | import { useTheme } from 'next-themes' |
| 5 | import { useEffect, useMemo } from 'react' |
| 6 | |
| 7 | import { getStatusName } from '../Pipeline.utils' |
| 8 | import { PrimaryDatabaseNode, ReadReplicaNode, ReplicationNode } from './Nodes' |
| 9 | import { getDagreGraphLayout } from './ReplicationDiagram.utils' |
| 10 | import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query' |
| 11 | import { useReplicationDestinationsQuery } from '@/data/replication/destinations-query' |
| 12 | import { replicationKeys } from '@/data/replication/keys' |
| 13 | import { ReplicationPipelineStatusResponse } from '@/data/replication/pipeline-status-query' |
| 14 | import { useReplicationPipelinesQuery } from '@/data/replication/pipelines-query' |
| 15 | import { timeout } from '@/lib/helpers' |
| 16 | |
| 17 | import '@xyflow/react/dist/style.css' |
| 18 | |
| 19 | import { SmoothstepEdge } from './Edges' |
| 20 | import { REPLICA_STATUS } from '@/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants' |
| 21 | |
| 22 | export const ReplicationDiagram = () => { |
| 23 | return ( |
| 24 | <ReactFlowProvider> |
| 25 | <ReplicationDiagramContent /> |
| 26 | </ReactFlowProvider> |
| 27 | ) |
| 28 | } |
| 29 | |
| 30 | const nodeTypes = { |
| 31 | primary: PrimaryDatabaseNode, |
| 32 | replication: ReplicationNode, |
| 33 | readReplica: ReadReplicaNode, |
| 34 | } |
| 35 | |
| 36 | const edgeTypes = { smoothstep: SmoothstepEdge } |
| 37 | |
| 38 | const 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 | } |