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