DefaultEdge.tsx203 lines · main
1import {
2 BaseEdge,
3 Edge,
4 EdgeLabelRenderer,
5 EdgeProps,
6 getSmoothStepPath,
7 Position,
8 useReactFlow,
9} from '@xyflow/react'
10import { ArrowLeft, ArrowRight } from 'lucide-react'
11import { useState } from 'react'
12import { Badge, cn } from 'ui'
13
14import { useSchemaGraphContext } from './SchemaGraphContext'
15import { EdgeData } from './Schemas.constants'
16import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState'
17import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent'
18
19export const DefaultEdge = ({
20 id,
21 animated,
22 data,
23 deletable,
24 selectable,
25 source,
26 sourceX,
27 sourceY,
28 sourceHandleId,
29 sourcePosition = Position.Bottom,
30 target,
31 targetX,
32 targetY,
33 targetHandleId,
34 targetPosition = Position.Top,
35 selected,
36 pathOptions,
37 ...props
38}: EdgeProps<Edge<EdgeData>>) => {
39 const { isDownloading } = useSchemaGraphContext()
40 const [edgePath, labelX, labelY] = getSmoothStepPath({
41 sourceX,
42 sourceY,
43 targetX,
44 targetY,
45 sourcePosition,
46 targetPosition,
47 borderRadius: pathOptions?.borderRadius,
48 offset: pathOptions?.offset,
49 stepPosition: pathOptions?.stepPosition,
50 })
51 return (
52 <>
53 <BaseEdge
54 id={id}
55 path={edgePath}
56 className={cn(selected ? 'stroke-brand!' : isDownloading ? 'stroke-black!' : undefined)}
57 stroke="#000000"
58 {...props}
59 />
60 {data && selected ? (
61 <EdgeRelationInfo
62 source={source}
63 target={target}
64 edgePath={edgePath}
65 labelX={labelX}
66 labelY={labelY}
67 sourceX={sourceX}
68 targetX={targetX}
69 data={data}
70 />
71 ) : null}
72 </>
73 )
74}
75
76const EdgeRelationInfo = ({
77 data,
78 source,
79 target,
80 labelX,
81 labelY,
82 targetX,
83 sourceX,
84}: {
85 data: EdgeData
86 edgePath: string
87 source: string
88 target: string
89 labelX: number
90 labelY: number
91 sourceX: number
92 targetX: number
93}) => {
94 const [show, setShow] = useState(false)
95 const reactFlowInstance = useReactFlow()
96
97 const checkIfShouldBeDisplayed = useStaticEffectEvent(
98 (relationInfoElement: HTMLDivElement | null) => {
99 if (!relationInfoElement) return
100 const sourceNode = reactFlowInstance.getNode(source)
101 const targetNode = reactFlowInstance.getNode(target)
102 if (!sourceNode || !targetNode) return
103
104 const relationInfoRect = relationInfoElement.getBoundingClientRect()
105 // Get the origin position of the relation information badge in the ReactFlow coordinates
106 const relationInfoOriginPositionInReactFlow = reactFlowInstance.screenToFlowPosition({
107 x: relationInfoRect.x,
108 y: relationInfoRect.y,
109 })
110 // Get the end position (origin + dimensions) of the relation information badge in the ReactFlow coordinates
111 const relationInfoTargetPositionInReactFlow = reactFlowInstance.screenToFlowPosition({
112 x: relationInfoRect.x + relationInfoRect.width,
113 y: relationInfoRect.y + relationInfoRect.height,
114 })
115 // Create a ReactFlow Rect from the computed position above
116 const relationInfoReactFlowRect = {
117 x: relationInfoOriginPositionInReactFlow.x,
118 y: relationInfoOriginPositionInReactFlow.y,
119 width: relationInfoTargetPositionInReactFlow.x - relationInfoOriginPositionInReactFlow.x,
120 height: relationInfoTargetPositionInReactFlow.y - relationInfoOriginPositionInReactFlow.y,
121 }
122 // Check whether the relation information badge is intersecting with either the source or target node
123 const isNodeIntersectingWithSource = reactFlowInstance.isNodeIntersecting(
124 sourceNode,
125 relationInfoReactFlowRect
126 )
127 const isNodeIntersectingWithTarget = reactFlowInstance.isNodeIntersecting(
128 targetNode,
129 relationInfoReactFlowRect
130 )
131
132 // If it is, hide it as they are too close
133 setShow(!isNodeIntersectingWithSource && !isNodeIntersectingWithTarget)
134 }
135 )
136
137 return (
138 <EdgeLabelRenderer>
139 <Badge
140 ref={checkIfShouldBeDisplayed}
141 className={cn(
142 'absolute pointer-events-auto z-50 p-1 rounded-[4px] gap-1 outline outline-1 outline-brand',
143 show ? 'opacity-100' : 'opacity-0'
144 )}
145 style={{
146 transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
147 }}
148 >
149 {
150 // Show the columns in the order of the schema instead of the Postgre relation order
151 sourceX < targetX ? (
152 <>
153 <EdgeNodeData
154 schema={data.sourceSchemaName}
155 table={data.sourceName}
156 column={data.sourceColumnName}
157 />
158 <ArrowRight size={12} />
159 <EdgeNodeData
160 schema={data.targetSchemaName}
161 table={data.targetName}
162 column={data.targetColumnName}
163 />
164 </>
165 ) : (
166 <>
167 <EdgeNodeData
168 schema={data.targetSchemaName}
169 table={data.targetName}
170 column={data.targetColumnName}
171 />
172 <ArrowLeft size={12} />
173 <EdgeNodeData
174 schema={data.sourceSchemaName}
175 table={data.sourceName}
176 column={data.sourceColumnName}
177 />
178 </>
179 )
180 }
181 </Badge>
182 </EdgeLabelRenderer>
183 )
184}
185
186const EdgeNodeData = ({
187 schema,
188 table,
189 column,
190}: {
191 schema: string
192 table: string
193 column: string
194}) => {
195 const { selectedSchema } = useQuerySchemaState()
196
197 return (
198 <Badge className="normal-case text-[8px]">
199 {selectedSchema === schema ? '' : `${schema}.`}
200 {table}.{column}
201 </Badge>
202 )
203}