index.tsx 6.1 KB

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