Nodes.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import { Handle, Position } from '@xyflow/react'
  2. import { useParams } from 'common'
  3. import { AnalyticsBucket, BigQuery, Database } from 'icons'
  4. import { ComponentType, PropsWithChildren, useMemo } from 'react'
  5. import { AWS_REGIONS } from 'shared-data'
  6. import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
  7. import { getStatusName } from '../Pipeline.utils'
  8. import { getStatusLabel } from '../ReadReplicas/ReadReplicas.utils'
  9. import { STATUS_REFRESH_FREQUENCY_MS } from '../Replication.constants'
  10. import { getReplicationDestinationType, type ReplicationDestinationType } from './Nodes.utils'
  11. import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
  12. import { formatDatabaseID } from '@/data/read-replicas/replicas.utils'
  13. import { useReplicationDestinationsQuery } from '@/data/replication/destinations-query'
  14. import { useReplicationPipelineStatusQuery } from '@/data/replication/pipeline-status-query'
  15. import { useReplicationPipelinesQuery } from '@/data/replication/pipelines-query'
  16. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  17. import { BASE_PATH } from '@/lib/constants'
  18. export const NODE_WIDTH = 480
  19. const destinationIconByType: Record<
  20. ReplicationDestinationType,
  21. ComponentType<{ className?: string; size?: string | number }>
  22. > = {
  23. BigQuery,
  24. 'Analytics Bucket': AnalyticsBucket,
  25. DuckLake: Database,
  26. }
  27. const NodeContainer = ({ className, children }: PropsWithChildren<{ className?: string }>) => {
  28. return (
  29. <div
  30. style={{ width: NODE_WIDTH / 2 + 55 }}
  31. className={cn(
  32. 'flex items-start justify-between p-3 rounded-sm bg-surface-100 border border-default',
  33. className
  34. )}
  35. >
  36. {children}
  37. </div>
  38. )
  39. }
  40. export const PrimaryDatabaseNode = () => {
  41. const { ref: projectRef } = useParams()
  42. const { data: project } = useSelectedProjectQuery()
  43. const { data: databases = [] } = useReadReplicasQuery({ projectRef })
  44. const hasReadReplicas = databases.some((x) => x.identifier !== projectRef)
  45. const { data: destinationsData } = useReplicationDestinationsQuery({ projectRef })
  46. const hasDestinations = (destinationsData?.destinations ?? []).length > 0
  47. const region = Object.values(AWS_REGIONS).find((x) => x.code === project?.region)
  48. const hasReplication = hasReadReplicas || hasDestinations
  49. return (
  50. <NodeContainer>
  51. <div className="text-sm flex flex-col gap-y-0.5">
  52. <p>Primary Database</p>
  53. <p className="text-foreground-light">{region?.displayName}</p>
  54. <p className="text-foreground-light">{region?.code}</p>
  55. </div>
  56. {!!project && (
  57. <img
  58. alt="region icon"
  59. className="w-8 rounded-xs mt-0.5"
  60. src={`${BASE_PATH}/img/regions/${project?.region}.svg`}
  61. />
  62. )}
  63. <Handle
  64. type="source"
  65. position={Position.Right}
  66. className={hasReplication ? 'opacity-25' : 'opacity-0'}
  67. />
  68. </NodeContainer>
  69. )
  70. }
  71. export const ReplicationNode = ({ id }: { id: string }) => {
  72. const { ref: projectRef } = useParams()
  73. const { data: destinationsData } = useReplicationDestinationsQuery({ projectRef })
  74. const destination = (destinationsData?.destinations ?? []).find((x) => x.id.toString() === id)
  75. const { data: pipelinesData } = useReplicationPipelinesQuery({
  76. projectRef,
  77. })
  78. const pipeline = (pipelinesData?.pipelines ?? []).find((x) => x.destination_id.toString() === id)
  79. const { data: pipelineStatusData } = useReplicationPipelineStatusQuery(
  80. { projectRef, pipelineId: pipeline?.id },
  81. { refetchInterval: STATUS_REFRESH_FREQUENCY_MS }
  82. )
  83. const statusName = getStatusName(pipelineStatusData?.status)
  84. const type = getReplicationDestinationType(destination?.config)
  85. const DestinationIcon = type ? destinationIconByType[type] : undefined
  86. return (
  87. <NodeContainer className="justify-start gap-x-3">
  88. {DestinationIcon ? <DestinationIcon size={20} className="text-foreground-light" /> : null}
  89. <div className="text-sm flex flex-col gap-y-0.5">
  90. <div className="flex items-center">
  91. <p>{type}</p>
  92. <Tooltip>
  93. <TooltipTrigger>
  94. <div className="w-6 h-full flex items-center justify-center">
  95. <div
  96. className={cn(
  97. 'w-2 h-2 rounded-full',
  98. statusName === 'started'
  99. ? 'bg-brand'
  100. : statusName === 'failed'
  101. ? 'bg-destructive'
  102. : 'bg-selection'
  103. )}
  104. />
  105. </div>
  106. </TooltipTrigger>
  107. <TooltipContent side="bottom" className="capitalize">
  108. {statusName}
  109. </TooltipContent>
  110. </Tooltip>
  111. </div>
  112. <p className="text-foreground-light">{destination?.name}</p>
  113. <p className="text-foreground-light">ID: {destination?.id}</p>
  114. </div>
  115. <Handle type="target" position={Position.Left} className="opacity-25" />
  116. </NodeContainer>
  117. )
  118. }
  119. export const ReadReplicaNode = ({ id }: { id: string }) => {
  120. const { ref: projectRef } = useParams()
  121. const { data: databases = [] } = useReadReplicasQuery({ projectRef })
  122. const database = databases.find((x) => x.identifier === id)
  123. const region = Object.values(AWS_REGIONS).find((x) => x.code === database?.region)
  124. const formattedId = formatDatabaseID(database?.identifier ?? '')
  125. const statusLabel = useMemo(
  126. () => getStatusLabel({ status: database?.status }),
  127. [database?.status]
  128. )
  129. return (
  130. <NodeContainer className="justify-start gap-x-3">
  131. <Database size={20} className="text-foreground-light" />
  132. <div className="flex flex-col gap-y-0.5">
  133. <div className="flex items-center">
  134. <p className="text-sm">Read Replica</p>
  135. <Tooltip>
  136. <TooltipTrigger>
  137. <div className="w-6 h-full flex items-center justify-center">
  138. <div
  139. className={cn(
  140. 'w-2 h-2 rounded-full',
  141. database?.status === 'ACTIVE_HEALTHY' ? 'bg-brand' : 'bg-selection'
  142. )}
  143. />
  144. </div>
  145. </TooltipTrigger>
  146. <TooltipContent side="bottom">{statusLabel}</TooltipContent>
  147. </Tooltip>
  148. </div>
  149. <p className="text-sm text-foreground-light">{region?.displayName}</p>
  150. <div className="flex gap-x-2 items-center text-sm text-foreground-light">
  151. <span>ID: {formattedId}</span>
  152. <span>•</span>
  153. <span>{region?.code}</span>
  154. </div>
  155. </div>
  156. <Handle type="target" position={Position.Left} className="opacity-25" />
  157. </NodeContainer>
  158. )
  159. }