mirror of
https://github.com/chartdb/chartdb.git
synced 2026-05-19 20:49:09 -05:00
fix: general performance improvements on canvas (#751)
This commit is contained in:
@@ -84,6 +84,9 @@ import type { AreaNodeType } from './area-node/area-node';
|
||||
import { AreaNode } from './area-node/area-node';
|
||||
import type { Area } from '@/lib/domain/area';
|
||||
|
||||
const HIGHLIGHTED_EDGE_Z_INDEX = 1;
|
||||
const DEFAULT_EDGE_Z_INDEX = 0;
|
||||
|
||||
export type EdgeType = RelationshipEdgeType | DependencyEdgeType;
|
||||
|
||||
export type NodeType = TableNodeType | AreaNodeType;
|
||||
@@ -132,7 +135,7 @@ export interface CanvasProps {
|
||||
}
|
||||
|
||||
export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
const { getEdge, getInternalNode, getEdges, getNode } = useReactFlow();
|
||||
const { getEdge, getInternalNode, getNode } = useReactFlow();
|
||||
const [selectedTableIds, setSelectedTableIds] = useState<string[]>([]);
|
||||
const [selectedRelationshipIds, setSelectedRelationshipIds] = useState<
|
||||
string[]
|
||||
@@ -282,22 +285,36 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
}, [edges, setSelectedRelationshipIds, selectedRelationshipIds]);
|
||||
|
||||
useEffect(() => {
|
||||
const tablesSelectedEdges = getEdges()
|
||||
.filter(
|
||||
(edge) =>
|
||||
selectedTableIds.includes(edge.source) ||
|
||||
selectedTableIds.includes(edge.target)
|
||||
)
|
||||
.map((edge) => edge.id);
|
||||
const selectedTableIdsSet = new Set(selectedTableIds);
|
||||
const selectedRelationshipIdsSet = new Set(selectedRelationshipIds);
|
||||
|
||||
const allSelectedEdges = [
|
||||
...tablesSelectedEdges,
|
||||
...selectedRelationshipIds,
|
||||
];
|
||||
setEdges((prevEdges) => {
|
||||
// Check if any edge needs updating
|
||||
let hasChanges = false;
|
||||
|
||||
setEdges((edges) =>
|
||||
edges.map((edge): EdgeType => {
|
||||
const selected = allSelectedEdges.includes(edge.id);
|
||||
const newEdges = prevEdges.map((edge): EdgeType => {
|
||||
const shouldBeHighlighted =
|
||||
selectedRelationshipIdsSet.has(edge.id) ||
|
||||
selectedTableIdsSet.has(edge.source) ||
|
||||
selectedTableIdsSet.has(edge.target);
|
||||
|
||||
const currentHighlighted = edge.data?.highlighted ?? false;
|
||||
const currentAnimated = edge.animated ?? false;
|
||||
const currentZIndex = edge.zIndex ?? 0;
|
||||
|
||||
// Skip if no changes needed
|
||||
if (
|
||||
currentHighlighted === shouldBeHighlighted &&
|
||||
currentAnimated === shouldBeHighlighted &&
|
||||
currentZIndex ===
|
||||
(shouldBeHighlighted
|
||||
? HIGHLIGHTED_EDGE_Z_INDEX
|
||||
: DEFAULT_EDGE_Z_INDEX)
|
||||
) {
|
||||
return edge;
|
||||
}
|
||||
|
||||
hasChanges = true;
|
||||
|
||||
if (edge.type === 'dependency-edge') {
|
||||
const dependencyEdge = edge as DependencyEdgeType;
|
||||
@@ -305,10 +322,12 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
...dependencyEdge,
|
||||
data: {
|
||||
...dependencyEdge.data!,
|
||||
highlighted: selected,
|
||||
highlighted: shouldBeHighlighted,
|
||||
},
|
||||
animated: selected,
|
||||
zIndex: selected ? 1 : 0,
|
||||
animated: shouldBeHighlighted,
|
||||
zIndex: shouldBeHighlighted
|
||||
? HIGHLIGHTED_EDGE_Z_INDEX
|
||||
: DEFAULT_EDGE_Z_INDEX,
|
||||
};
|
||||
} else {
|
||||
const relationshipEdge = edge as RelationshipEdgeType;
|
||||
@@ -316,34 +335,47 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
...relationshipEdge,
|
||||
data: {
|
||||
...relationshipEdge.data!,
|
||||
highlighted: selected,
|
||||
highlighted: shouldBeHighlighted,
|
||||
},
|
||||
animated: selected,
|
||||
zIndex: selected ? 1 : 0,
|
||||
animated: shouldBeHighlighted,
|
||||
zIndex: shouldBeHighlighted
|
||||
? HIGHLIGHTED_EDGE_Z_INDEX
|
||||
: DEFAULT_EDGE_Z_INDEX,
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
}, [selectedRelationshipIds, selectedTableIds, setEdges, getEdges]);
|
||||
});
|
||||
|
||||
return hasChanges ? newEdges : prevEdges;
|
||||
});
|
||||
}, [selectedRelationshipIds, selectedTableIds, setEdges]);
|
||||
|
||||
useEffect(() => {
|
||||
setNodes([
|
||||
...tables.map((table) => {
|
||||
const isOverlapping =
|
||||
(overlapGraph.graph.get(table.id) ?? []).length > 0;
|
||||
const node = tableToTableNode(table, filteredSchemas);
|
||||
setNodes((prevNodes) => {
|
||||
const newNodes = [
|
||||
...tables.map((table) => {
|
||||
const isOverlapping =
|
||||
(overlapGraph.graph.get(table.id) ?? []).length > 0;
|
||||
const node = tableToTableNode(table, filteredSchemas);
|
||||
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
isOverlapping,
|
||||
highlightOverlappingTables,
|
||||
},
|
||||
};
|
||||
}),
|
||||
...areas.map(areaToAreaNode),
|
||||
]);
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
isOverlapping,
|
||||
highlightOverlappingTables,
|
||||
},
|
||||
};
|
||||
}),
|
||||
...areas.map(areaToAreaNode),
|
||||
];
|
||||
|
||||
// Check if nodes actually changed
|
||||
if (equal(prevNodes, newNodes)) {
|
||||
return prevNodes;
|
||||
}
|
||||
|
||||
return newNodes;
|
||||
});
|
||||
}, [
|
||||
tables,
|
||||
areas,
|
||||
|
||||
@@ -17,203 +17,234 @@ export type RelationshipEdgeType = Edge<
|
||||
'relationship-edge'
|
||||
>;
|
||||
|
||||
export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
source,
|
||||
target,
|
||||
selected,
|
||||
data,
|
||||
}) => {
|
||||
const { getInternalNode, getEdge } = useReactFlow();
|
||||
const { openRelationshipFromSidebar, selectSidebarSection } = useLayout();
|
||||
const { checkIfRelationshipRemoved, checkIfNewRelationship } = useDiff();
|
||||
|
||||
const { relationships } = useChartDB();
|
||||
|
||||
const relationship = data?.relationship;
|
||||
|
||||
const openRelationshipInEditor = useCallback(() => {
|
||||
selectSidebarSection('relationships');
|
||||
openRelationshipFromSidebar(id);
|
||||
}, [id, openRelationshipFromSidebar, selectSidebarSection]);
|
||||
|
||||
const edgeNumber = useMemo(
|
||||
() =>
|
||||
relationships
|
||||
.filter(
|
||||
(relationship) =>
|
||||
(relationship.targetTableId === target &&
|
||||
relationship.sourceTableId === source) ||
|
||||
(relationship.targetTableId === source &&
|
||||
relationship.sourceTableId === target)
|
||||
)
|
||||
.findIndex((relationship) => relationship.id === id),
|
||||
[relationships, id, source, target]
|
||||
);
|
||||
|
||||
const sourceNode = getInternalNode(source);
|
||||
const targetNode = getInternalNode(target);
|
||||
const edge = getEdge(id);
|
||||
|
||||
const sourceHandle: 'left' | 'right' = edge?.sourceHandle?.startsWith?.(
|
||||
RIGHT_HANDLE_ID_PREFIX
|
||||
)
|
||||
? 'right'
|
||||
: 'left';
|
||||
|
||||
const sourceWidth = sourceNode?.measured.width ?? 0;
|
||||
const sourceLeftX =
|
||||
sourceHandle === 'left' ? sourceX + 3 : sourceX - sourceWidth - 10;
|
||||
const sourceRightX =
|
||||
sourceHandle === 'left' ? sourceX + sourceWidth + 9 : sourceX;
|
||||
|
||||
const targetWidth = targetNode?.measured.width ?? 0;
|
||||
const targetLeftX = targetX - 1;
|
||||
const targetRightX = targetX + targetWidth + 10;
|
||||
|
||||
const { sourceSide, targetSide } = useMemo(() => {
|
||||
const distances = {
|
||||
leftToLeft: Math.abs(sourceLeftX - targetLeftX),
|
||||
leftToRight: Math.abs(sourceLeftX - targetRightX),
|
||||
rightToLeft: Math.abs(sourceRightX - targetLeftX),
|
||||
rightToRight: Math.abs(sourceRightX - targetRightX),
|
||||
};
|
||||
|
||||
const minDistance = Math.min(
|
||||
distances.leftToLeft,
|
||||
distances.leftToRight,
|
||||
distances.rightToLeft,
|
||||
distances.rightToRight
|
||||
);
|
||||
|
||||
const minDistanceKey = Object.keys(distances).find(
|
||||
(key) => distances[key as keyof typeof distances] === minDistance
|
||||
) as keyof typeof distances;
|
||||
|
||||
switch (minDistanceKey) {
|
||||
case 'leftToRight':
|
||||
return { sourceSide: 'left', targetSide: 'right' };
|
||||
case 'rightToLeft':
|
||||
return { sourceSide: 'right', targetSide: 'left' };
|
||||
case 'rightToRight':
|
||||
return { sourceSide: 'right', targetSide: 'right' };
|
||||
default:
|
||||
return { sourceSide: 'left', targetSide: 'left' };
|
||||
}
|
||||
}, [sourceLeftX, sourceRightX, targetLeftX, targetRightX]);
|
||||
|
||||
const [edgePath] = useMemo(
|
||||
() =>
|
||||
getSmoothStepPath({
|
||||
sourceX: sourceSide === 'left' ? sourceLeftX : sourceRightX,
|
||||
sourceY,
|
||||
targetX: targetSide === 'left' ? targetLeftX : targetRightX,
|
||||
targetY,
|
||||
borderRadius: 14,
|
||||
sourcePosition:
|
||||
sourceSide === 'left' ? Position.Left : Position.Right,
|
||||
targetPosition:
|
||||
targetSide === 'left' ? Position.Left : Position.Right,
|
||||
offset: (edgeNumber + 1) * 14,
|
||||
}),
|
||||
[
|
||||
sourceSide,
|
||||
targetSide,
|
||||
sourceLeftX,
|
||||
sourceRightX,
|
||||
targetLeftX,
|
||||
targetRightX,
|
||||
export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> =
|
||||
React.memo(
|
||||
({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
edgeNumber,
|
||||
]
|
||||
);
|
||||
source,
|
||||
target,
|
||||
selected,
|
||||
data,
|
||||
}) => {
|
||||
const { getInternalNode, getEdge } = useReactFlow();
|
||||
const { openRelationshipFromSidebar, selectSidebarSection } =
|
||||
useLayout();
|
||||
const { checkIfRelationshipRemoved, checkIfNewRelationship } =
|
||||
useDiff();
|
||||
|
||||
const sourceMarker = useMemo(
|
||||
() =>
|
||||
getCardinalityMarkerId({
|
||||
cardinality: relationship?.sourceCardinality ?? 'one',
|
||||
selected: selected ?? false,
|
||||
side: sourceSide as 'left' | 'right',
|
||||
}),
|
||||
[relationship?.sourceCardinality, selected, sourceSide]
|
||||
);
|
||||
const targetMarker = useMemo(
|
||||
() =>
|
||||
getCardinalityMarkerId({
|
||||
cardinality: relationship?.targetCardinality ?? 'one',
|
||||
selected: selected ?? false,
|
||||
side: targetSide as 'left' | 'right',
|
||||
}),
|
||||
[relationship?.targetCardinality, selected, targetSide]
|
||||
);
|
||||
const { relationships } = useChartDB();
|
||||
|
||||
const isDiffNewRelationship = useMemo(
|
||||
() =>
|
||||
relationship?.id
|
||||
? checkIfNewRelationship({ relationshipId: relationship.id })
|
||||
: false,
|
||||
[checkIfNewRelationship, relationship?.id]
|
||||
);
|
||||
const relationship = data?.relationship;
|
||||
|
||||
const isDiffRelationshipRemoved = useMemo(
|
||||
() =>
|
||||
relationship?.id
|
||||
? checkIfRelationshipRemoved({
|
||||
relationshipId: relationship.id,
|
||||
})
|
||||
: false,
|
||||
[checkIfRelationshipRemoved, relationship?.id]
|
||||
);
|
||||
const openRelationshipInEditor = useCallback(() => {
|
||||
selectSidebarSection('relationships');
|
||||
openRelationshipFromSidebar(id);
|
||||
}, [id, openRelationshipFromSidebar, selectSidebarSection]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<path
|
||||
id={id}
|
||||
d={edgePath}
|
||||
markerStart={`url(#${sourceMarker})`}
|
||||
markerEnd={`url(#${targetMarker})`}
|
||||
fill="none"
|
||||
className={cn([
|
||||
'react-flow__edge-path',
|
||||
`!stroke-2 ${selected ? '!stroke-pink-600' : '!stroke-slate-400'}`,
|
||||
{
|
||||
'!stroke-green-500 !stroke-[3px]':
|
||||
isDiffNewRelationship,
|
||||
'!stroke-red-500 !stroke-[3px]':
|
||||
isDiffRelationshipRemoved,
|
||||
},
|
||||
])}
|
||||
onClick={(e) => {
|
||||
if (e.detail === 2) {
|
||||
openRelationshipInEditor();
|
||||
const edgeNumber = useMemo(() => {
|
||||
let index = 0;
|
||||
for (const rel of relationships) {
|
||||
if (
|
||||
(rel.targetTableId === target &&
|
||||
rel.sourceTableId === source) ||
|
||||
(rel.targetTableId === source &&
|
||||
rel.sourceTableId === target)
|
||||
) {
|
||||
if (rel.id === id) return index;
|
||||
index++;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d={edgePath}
|
||||
fill="none"
|
||||
strokeOpacity={0}
|
||||
strokeWidth={20}
|
||||
// eslint-disable-next-line tailwindcss/no-custom-classname
|
||||
className="react-flow__edge-interaction"
|
||||
onClick={(e) => {
|
||||
if (e.detail === 2) {
|
||||
openRelationshipInEditor();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
// <BaseEdge
|
||||
// id={id}
|
||||
// path={edgePath}
|
||||
// markerStart="url(#cardinality_one)"
|
||||
// markerEnd="url(#cardinality_one)"
|
||||
// className={`!stroke-2 ${selected ? '!stroke-slate-500' : '!stroke-slate-300'}`}
|
||||
// />
|
||||
}
|
||||
return -1;
|
||||
}, [relationships, id, source, target]);
|
||||
|
||||
const sourceNode = useMemo(
|
||||
() => getInternalNode(source),
|
||||
[getInternalNode, source]
|
||||
);
|
||||
const targetNode = useMemo(
|
||||
() => getInternalNode(target),
|
||||
[getInternalNode, target]
|
||||
);
|
||||
const edge = useMemo(() => getEdge(id), [getEdge, id]);
|
||||
|
||||
const sourceHandle: 'left' | 'right' = useMemo(
|
||||
() =>
|
||||
edge?.sourceHandle?.startsWith?.(RIGHT_HANDLE_ID_PREFIX)
|
||||
? 'right'
|
||||
: 'left',
|
||||
[edge?.sourceHandle]
|
||||
);
|
||||
|
||||
const sourceWidth = sourceNode?.measured.width ?? 0;
|
||||
const sourceLeftX =
|
||||
sourceHandle === 'left'
|
||||
? sourceX + 3
|
||||
: sourceX - sourceWidth - 10;
|
||||
const sourceRightX =
|
||||
sourceHandle === 'left' ? sourceX + sourceWidth + 9 : sourceX;
|
||||
|
||||
const targetWidth = targetNode?.measured.width ?? 0;
|
||||
const targetLeftX = targetX - 1;
|
||||
const targetRightX = targetX + targetWidth + 10;
|
||||
|
||||
const { sourceSide, targetSide } = useMemo(() => {
|
||||
const distances = {
|
||||
leftToLeft: Math.abs(sourceLeftX - targetLeftX),
|
||||
leftToRight: Math.abs(sourceLeftX - targetRightX),
|
||||
rightToLeft: Math.abs(sourceRightX - targetLeftX),
|
||||
rightToRight: Math.abs(sourceRightX - targetRightX),
|
||||
};
|
||||
|
||||
const minDistance = Math.min(
|
||||
distances.leftToLeft,
|
||||
distances.leftToRight,
|
||||
distances.rightToLeft,
|
||||
distances.rightToRight
|
||||
);
|
||||
|
||||
const minDistanceKey = Object.keys(distances).find(
|
||||
(key) =>
|
||||
distances[key as keyof typeof distances] === minDistance
|
||||
) as keyof typeof distances;
|
||||
|
||||
switch (minDistanceKey) {
|
||||
case 'leftToRight':
|
||||
return { sourceSide: 'left', targetSide: 'right' };
|
||||
case 'rightToLeft':
|
||||
return { sourceSide: 'right', targetSide: 'left' };
|
||||
case 'rightToRight':
|
||||
return { sourceSide: 'right', targetSide: 'right' };
|
||||
default:
|
||||
return { sourceSide: 'left', targetSide: 'left' };
|
||||
}
|
||||
}, [sourceLeftX, sourceRightX, targetLeftX, targetRightX]);
|
||||
|
||||
const edgePath = useMemo(() => {
|
||||
// Round values to prevent tiny changes from triggering recalculation
|
||||
const roundedSourceX = Math.round(
|
||||
sourceSide === 'left' ? sourceLeftX : sourceRightX
|
||||
);
|
||||
const roundedTargetX = Math.round(
|
||||
targetSide === 'left' ? targetLeftX : targetRightX
|
||||
);
|
||||
const roundedSourceY = Math.round(sourceY);
|
||||
const roundedTargetY = Math.round(targetY);
|
||||
|
||||
const [path] = getSmoothStepPath({
|
||||
sourceX: roundedSourceX,
|
||||
sourceY: roundedSourceY,
|
||||
targetX: roundedTargetX,
|
||||
targetY: roundedTargetY,
|
||||
borderRadius: 14,
|
||||
sourcePosition:
|
||||
sourceSide === 'left' ? Position.Left : Position.Right,
|
||||
targetPosition:
|
||||
targetSide === 'left' ? Position.Left : Position.Right,
|
||||
offset: (edgeNumber + 1) * 14,
|
||||
});
|
||||
return path;
|
||||
}, [
|
||||
sourceLeftX,
|
||||
sourceRightX,
|
||||
targetLeftX,
|
||||
targetRightX,
|
||||
sourceY,
|
||||
targetY,
|
||||
sourceSide,
|
||||
targetSide,
|
||||
edgeNumber,
|
||||
]);
|
||||
|
||||
const sourceMarker = useMemo(
|
||||
() =>
|
||||
getCardinalityMarkerId({
|
||||
cardinality: relationship?.sourceCardinality ?? 'one',
|
||||
selected: selected ?? false,
|
||||
side: sourceSide as 'left' | 'right',
|
||||
}),
|
||||
[relationship?.sourceCardinality, selected, sourceSide]
|
||||
);
|
||||
const targetMarker = useMemo(
|
||||
() =>
|
||||
getCardinalityMarkerId({
|
||||
cardinality: relationship?.targetCardinality ?? 'one',
|
||||
selected: selected ?? false,
|
||||
side: targetSide as 'left' | 'right',
|
||||
}),
|
||||
[relationship?.targetCardinality, selected, targetSide]
|
||||
);
|
||||
|
||||
const isDiffNewRelationship = useMemo(
|
||||
() =>
|
||||
relationship?.id
|
||||
? checkIfNewRelationship({
|
||||
relationshipId: relationship.id,
|
||||
})
|
||||
: false,
|
||||
[checkIfNewRelationship, relationship?.id]
|
||||
);
|
||||
|
||||
const isDiffRelationshipRemoved = useMemo(
|
||||
() =>
|
||||
relationship?.id
|
||||
? checkIfRelationshipRemoved({
|
||||
relationshipId: relationship.id,
|
||||
})
|
||||
: false,
|
||||
[checkIfRelationshipRemoved, relationship?.id]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<path
|
||||
id={id}
|
||||
d={edgePath}
|
||||
markerStart={`url(#${sourceMarker})`}
|
||||
markerEnd={`url(#${targetMarker})`}
|
||||
fill="none"
|
||||
className={cn([
|
||||
'react-flow__edge-path',
|
||||
`!stroke-2 ${selected ? '!stroke-pink-600' : '!stroke-slate-400'}`,
|
||||
{
|
||||
'!stroke-green-500 !stroke-[3px]':
|
||||
isDiffNewRelationship,
|
||||
'!stroke-red-500 !stroke-[3px]':
|
||||
isDiffRelationshipRemoved,
|
||||
},
|
||||
])}
|
||||
onClick={(e) => {
|
||||
if (e.detail === 2) {
|
||||
openRelationshipInEditor();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d={edgePath}
|
||||
fill="none"
|
||||
strokeOpacity={0}
|
||||
strokeWidth={20}
|
||||
// eslint-disable-next-line tailwindcss/no-custom-classname
|
||||
className="react-flow__edge-interaction"
|
||||
onClick={(e) => {
|
||||
if (e.detail === 2) {
|
||||
openRelationshipInEditor();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
// <BaseEdge
|
||||
// id={id}
|
||||
// path={edgePath}
|
||||
// markerStart="url(#cardinality_one)"
|
||||
// markerEnd="url(#cardinality_one)"
|
||||
// className={`!stroke-2 ${selected ? '!stroke-slate-500' : '!stroke-slate-300'}`}
|
||||
// />
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
RelationshipEdge.displayName = 'RelationshipEdge';
|
||||
|
||||
@@ -46,6 +46,27 @@ export interface TableNodeFieldProps {
|
||||
isConnectable: boolean;
|
||||
}
|
||||
|
||||
const arePropsEqual = (
|
||||
prevProps: TableNodeFieldProps,
|
||||
nextProps: TableNodeFieldProps
|
||||
) => {
|
||||
return (
|
||||
prevProps.field.id === nextProps.field.id &&
|
||||
prevProps.field.name === nextProps.field.name &&
|
||||
prevProps.field.primaryKey === nextProps.field.primaryKey &&
|
||||
prevProps.field.nullable === nextProps.field.nullable &&
|
||||
prevProps.field.comments === nextProps.field.comments &&
|
||||
prevProps.field.unique === nextProps.field.unique &&
|
||||
prevProps.field.type.id === nextProps.field.type.id &&
|
||||
prevProps.field.type.name === nextProps.field.type.name &&
|
||||
prevProps.focused === nextProps.focused &&
|
||||
prevProps.highlighted === nextProps.highlighted &&
|
||||
prevProps.visible === nextProps.visible &&
|
||||
prevProps.isConnectable === nextProps.isConnectable &&
|
||||
prevProps.tableNodeId === nextProps.tableNodeId
|
||||
);
|
||||
};
|
||||
|
||||
export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
({ field, focused, tableNodeId, highlighted, visible, isConnectable }) => {
|
||||
const { removeField, relationships, readonly, updateField } =
|
||||
@@ -64,17 +85,25 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
connection.fromHandle.id?.startsWith(
|
||||
LEFT_HANDLE_ID_PREFIX
|
||||
)),
|
||||
[connection, tableNodeId]
|
||||
);
|
||||
const numberOfEdgesToField = useMemo(
|
||||
() =>
|
||||
relationships.filter(
|
||||
(relationship) =>
|
||||
relationship.targetTableId === tableNodeId &&
|
||||
relationship.targetFieldId === field.id
|
||||
).length,
|
||||
[relationships, tableNodeId, field.id]
|
||||
[
|
||||
connection.inProgress,
|
||||
connection.fromNode?.id,
|
||||
connection.fromHandle?.id,
|
||||
tableNodeId,
|
||||
]
|
||||
);
|
||||
const numberOfEdgesToField = useMemo(() => {
|
||||
let count = 0;
|
||||
for (const rel of relationships) {
|
||||
if (
|
||||
rel.targetTableId === tableNodeId &&
|
||||
rel.targetFieldId === field.id
|
||||
) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}, [relationships, tableNodeId, field.id]);
|
||||
|
||||
const previousNumberOfEdgesToFieldRef = useRef(numberOfEdgesToField);
|
||||
|
||||
@@ -82,8 +111,12 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
if (
|
||||
previousNumberOfEdgesToFieldRef.current !== numberOfEdgesToField
|
||||
) {
|
||||
updateNodeInternals(tableNodeId);
|
||||
previousNumberOfEdgesToFieldRef.current = numberOfEdgesToField;
|
||||
const timer = setTimeout(() => {
|
||||
updateNodeInternals(tableNodeId);
|
||||
previousNumberOfEdgesToFieldRef.current =
|
||||
numberOfEdgesToField;
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [tableNodeId, updateNodeInternals, numberOfEdgesToField]);
|
||||
|
||||
@@ -112,39 +145,63 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
checkIfFieldHasChange,
|
||||
} = useDiff();
|
||||
|
||||
const isDiffFieldRemoved = useMemo(
|
||||
() => checkIfFieldRemoved({ fieldId: field.id }),
|
||||
[checkIfFieldRemoved, field.id]
|
||||
);
|
||||
const [diffState, setDiffState] = useState<{
|
||||
isDiffFieldRemoved: boolean;
|
||||
isDiffNewField: boolean;
|
||||
fieldDiffChangedName: string | null;
|
||||
fieldDiffChangedType: DBField['type'] | null;
|
||||
isDiffFieldChanged: boolean;
|
||||
}>({
|
||||
isDiffFieldRemoved: false,
|
||||
isDiffNewField: false,
|
||||
fieldDiffChangedName: null,
|
||||
fieldDiffChangedType: null,
|
||||
isDiffFieldChanged: false,
|
||||
});
|
||||
|
||||
const isDiffNewField = useMemo(
|
||||
() => checkIfNewField({ fieldId: field.id }),
|
||||
[checkIfNewField, field.id]
|
||||
);
|
||||
useEffect(() => {
|
||||
// Calculate diff state asynchronously
|
||||
const timer = requestAnimationFrame(() => {
|
||||
setDiffState({
|
||||
isDiffFieldRemoved: checkIfFieldRemoved({
|
||||
fieldId: field.id,
|
||||
}),
|
||||
isDiffNewField: checkIfNewField({ fieldId: field.id }),
|
||||
fieldDiffChangedName: getFieldNewName({
|
||||
fieldId: field.id,
|
||||
}),
|
||||
fieldDiffChangedType: getFieldNewType({
|
||||
fieldId: field.id,
|
||||
}),
|
||||
isDiffFieldChanged: checkIfFieldHasChange({
|
||||
fieldId: field.id,
|
||||
tableId: tableNodeId,
|
||||
}),
|
||||
});
|
||||
});
|
||||
return () => cancelAnimationFrame(timer);
|
||||
}, [
|
||||
checkIfFieldRemoved,
|
||||
checkIfNewField,
|
||||
getFieldNewName,
|
||||
getFieldNewType,
|
||||
checkIfFieldHasChange,
|
||||
field.id,
|
||||
tableNodeId,
|
||||
]);
|
||||
|
||||
const fieldDiffChangedName = useMemo(
|
||||
() => getFieldNewName({ fieldId: field.id }),
|
||||
[getFieldNewName, field.id]
|
||||
);
|
||||
const {
|
||||
isDiffFieldRemoved,
|
||||
isDiffNewField,
|
||||
fieldDiffChangedName,
|
||||
fieldDiffChangedType,
|
||||
isDiffFieldChanged,
|
||||
} = diffState;
|
||||
|
||||
const fieldDiffChangedType = useMemo(
|
||||
() => getFieldNewType({ fieldId: field.id }),
|
||||
[getFieldNewType, field.id]
|
||||
);
|
||||
|
||||
const isDiffFieldChanged = useMemo(
|
||||
() =>
|
||||
checkIfFieldHasChange({
|
||||
fieldId: field.id,
|
||||
tableId: tableNodeId,
|
||||
}),
|
||||
[checkIfFieldHasChange, field.id, tableNodeId]
|
||||
);
|
||||
|
||||
const enterEditMode = (e: React.MouseEvent) => {
|
||||
const enterEditMode = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setEditMode(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -359,7 +416,8 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
arePropsEqual
|
||||
);
|
||||
|
||||
TableNodeField.displayName = 'TableNodeField';
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import React, { useCallback, useState, useMemo } from 'react';
|
||||
import React, {
|
||||
useCallback,
|
||||
useState,
|
||||
useMemo,
|
||||
useRef,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import type { NodeProps, Node } from '@xyflow/react';
|
||||
import { NodeResizer, useStore } from '@xyflow/react';
|
||||
import { Button } from '@/components/button/button';
|
||||
@@ -95,34 +101,89 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
||||
return table.color;
|
||||
}, [tableChangedColor, table.color]);
|
||||
|
||||
const isDiffTableChanged = useMemo(
|
||||
() => checkIfTableHasChange({ tableId: table.id }),
|
||||
[checkIfTableHasChange, table.id]
|
||||
const [diffState, setDiffState] = useState<{
|
||||
isDiffTableChanged: boolean;
|
||||
isDiffNewTable: boolean;
|
||||
isDiffTableRemoved: boolean;
|
||||
}>({
|
||||
isDiffTableChanged: false,
|
||||
isDiffNewTable: false,
|
||||
isDiffTableRemoved: false,
|
||||
});
|
||||
|
||||
const hasMountedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Skip on initial mount to improve performance
|
||||
const calculateDiff = () => {
|
||||
setDiffState({
|
||||
isDiffTableChanged: checkIfTableHasChange({
|
||||
tableId: table.id,
|
||||
}),
|
||||
isDiffNewTable: checkIfNewTable({ tableId: table.id }),
|
||||
isDiffTableRemoved: checkIfTableRemoved({
|
||||
tableId: table.id,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
if (!hasMountedRef.current) {
|
||||
hasMountedRef.current = true;
|
||||
// Defer diff calculation
|
||||
requestAnimationFrame(calculateDiff);
|
||||
} else {
|
||||
calculateDiff();
|
||||
}
|
||||
}, [
|
||||
checkIfTableHasChange,
|
||||
checkIfNewTable,
|
||||
checkIfTableRemoved,
|
||||
table.id,
|
||||
]);
|
||||
|
||||
const { isDiffTableChanged, isDiffNewTable, isDiffTableRemoved } =
|
||||
diffState;
|
||||
|
||||
const selectedRelEdges: RelationshipEdgeType[] = useMemo(() => {
|
||||
if (edges.length === 0) return [];
|
||||
|
||||
const relEdges: RelationshipEdgeType[] = [];
|
||||
for (const edge of edges) {
|
||||
if (
|
||||
edge.type === 'relationship-edge' &&
|
||||
(edge.source === id || edge.target === id) &&
|
||||
(edge.selected || edge.data?.highlighted)
|
||||
) {
|
||||
relEdges.push(edge as RelationshipEdgeType);
|
||||
}
|
||||
}
|
||||
return relEdges;
|
||||
}, [edges, id]);
|
||||
|
||||
const highlightedFieldIds = useMemo(() => {
|
||||
const fieldIds = new Set<string>();
|
||||
selectedRelEdges.forEach((edge) => {
|
||||
if (edge.data?.relationship.sourceFieldId) {
|
||||
fieldIds.add(edge.data.relationship.sourceFieldId);
|
||||
}
|
||||
|
||||
if (edge.data?.relationship.targetFieldId) {
|
||||
fieldIds.add(edge.data.relationship.targetFieldId);
|
||||
}
|
||||
});
|
||||
|
||||
return fieldIds;
|
||||
}, [selectedRelEdges]);
|
||||
|
||||
const focused = useMemo(
|
||||
() => (!!selected && !dragging) || isHovering,
|
||||
[selected, dragging, isHovering]
|
||||
);
|
||||
|
||||
const isDiffNewTable = useMemo(
|
||||
() => checkIfNewTable({ tableId: table.id }),
|
||||
[checkIfNewTable, table.id]
|
||||
);
|
||||
|
||||
const isDiffTableRemoved = useMemo(
|
||||
() => checkIfTableRemoved({ tableId: table.id }),
|
||||
[checkIfTableRemoved, table.id]
|
||||
);
|
||||
|
||||
const selectedRelEdges = edges.filter(
|
||||
(edge) =>
|
||||
(edge.source === id || edge.target === id) &&
|
||||
(edge.selected || edge.data?.highlighted) &&
|
||||
edge.type === 'relationship-edge'
|
||||
) as RelationshipEdgeType[];
|
||||
|
||||
const focused = (!!selected && !dragging) || isHovering;
|
||||
|
||||
const openTableInEditor = () => {
|
||||
const openTableInEditor = useCallback(() => {
|
||||
selectSidebarSection('tables');
|
||||
openTableFromSidebar(table.id);
|
||||
};
|
||||
}, [selectSidebarSection, openTableFromSidebar, table.id]);
|
||||
|
||||
const expandTable = useCallback(() => {
|
||||
updateTable(table.id, {
|
||||
@@ -147,47 +208,56 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
||||
});
|
||||
}, [table.id, updateTable]);
|
||||
|
||||
const isMustDisplayedField = useCallback(
|
||||
(field: DBField) => {
|
||||
return (
|
||||
relationships.some(
|
||||
(relationship) =>
|
||||
relationship.sourceFieldId === field.id ||
|
||||
relationship.targetFieldId === field.id
|
||||
) || field.primaryKey
|
||||
);
|
||||
},
|
||||
[relationships]
|
||||
);
|
||||
const relatedFieldIds = useMemo(() => {
|
||||
const fieldIds = new Set<string>();
|
||||
relationships.forEach((rel) => {
|
||||
if (rel.sourceFieldId) fieldIds.add(rel.sourceFieldId);
|
||||
if (rel.targetFieldId) fieldIds.add(rel.targetFieldId);
|
||||
});
|
||||
return fieldIds;
|
||||
}, [relationships]);
|
||||
|
||||
const visibleFields = useMemo(() => {
|
||||
if (expanded) {
|
||||
if (expanded || fields.length <= TABLE_MINIMIZED_FIELDS) {
|
||||
return fields;
|
||||
}
|
||||
|
||||
const mustDisplayedFields = fields.filter((field: DBField) =>
|
||||
isMustDisplayedField(field)
|
||||
);
|
||||
const nonMustDisplayedFields = fields.filter(
|
||||
(field: DBField) => !isMustDisplayedField(field)
|
||||
);
|
||||
const mustDisplayedFields: DBField[] = [];
|
||||
const nonMustDisplayedFields: DBField[] = [];
|
||||
|
||||
for (const field of fields) {
|
||||
if (relatedFieldIds.has(field.id) || field.primaryKey) {
|
||||
mustDisplayedFields.push(field);
|
||||
} else {
|
||||
nonMustDisplayedFields.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
// Take required fields up to limit
|
||||
const visibleMustDisplayedFields = mustDisplayedFields.slice(
|
||||
0,
|
||||
TABLE_MINIMIZED_FIELDS
|
||||
);
|
||||
const remainingSlots =
|
||||
TABLE_MINIMIZED_FIELDS - visibleMustDisplayedFields.length;
|
||||
const visibleNonMustDisplayedFields = nonMustDisplayedFields.slice(
|
||||
0,
|
||||
remainingSlots
|
||||
);
|
||||
|
||||
return [
|
||||
// Fill remaining slots with non-required fields
|
||||
const visibleNonMustDisplayedFields =
|
||||
remainingSlots > 0
|
||||
? nonMustDisplayedFields.slice(0, remainingSlots)
|
||||
: [];
|
||||
|
||||
// Combine and maintain original order
|
||||
const visibleFieldsSet = new Set([
|
||||
...visibleMustDisplayedFields,
|
||||
...visibleNonMustDisplayedFields,
|
||||
].sort((a, b) => fields.indexOf(a) - fields.indexOf(b));
|
||||
}, [expanded, fields, isMustDisplayedField]);
|
||||
]);
|
||||
const result = fields.filter((field) =>
|
||||
visibleFieldsSet.has(field)
|
||||
);
|
||||
|
||||
return result;
|
||||
}, [expanded, fields, relatedFieldIds]);
|
||||
|
||||
const editTableName = useCallback(() => {
|
||||
if (!editMode) return;
|
||||
@@ -206,10 +276,10 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
||||
useKeyPressEvent('Enter', editTableName);
|
||||
useKeyPressEvent('Escape', abortEdit);
|
||||
|
||||
const enterEditMode = (e: React.MouseEvent) => {
|
||||
const enterEditMode = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setEditMode(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (table.name.trim()) {
|
||||
@@ -217,35 +287,46 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
||||
}
|
||||
}, [table.name]);
|
||||
|
||||
const tableClassName = useMemo(
|
||||
() =>
|
||||
cn(
|
||||
'flex w-full flex-col border-2 bg-slate-50 dark:bg-slate-950 rounded-lg shadow-sm transition-transform duration-300',
|
||||
selected
|
||||
? 'border-pink-600'
|
||||
: 'border-slate-500 dark:border-slate-700',
|
||||
isOverlapping
|
||||
? 'ring-2 ring-offset-slate-50 dark:ring-offset-slate-900 ring-blue-500 ring-offset-2'
|
||||
: '',
|
||||
!highlightOverlappingTables && isOverlapping
|
||||
? 'animate-scale'
|
||||
: '',
|
||||
highlightOverlappingTables && isOverlapping
|
||||
? 'animate-scale-2'
|
||||
: '',
|
||||
isDiffTableChanged && !isDiffNewTable && !isDiffTableRemoved
|
||||
? 'outline outline-[3px] outline-sky-500 dark:outline-sky-900 outline-offset-[5px]'
|
||||
: '',
|
||||
isDiffNewTable
|
||||
? 'outline outline-[3px] outline-green-500 dark:outline-green-900 outline-offset-[5px]'
|
||||
: '',
|
||||
isDiffTableRemoved
|
||||
? 'outline outline-[3px] outline-red-500 dark:outline-red-900 outline-offset-[5px]'
|
||||
: ''
|
||||
),
|
||||
[
|
||||
selected,
|
||||
isOverlapping,
|
||||
highlightOverlappingTables,
|
||||
isDiffTableChanged,
|
||||
isDiffNewTable,
|
||||
isDiffTableRemoved,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<TableNodeContextMenu table={table}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex w-full flex-col border-2 bg-slate-50 dark:bg-slate-950 rounded-lg shadow-sm transition-transform duration-300',
|
||||
selected
|
||||
? 'border-pink-600'
|
||||
: 'border-slate-500 dark:border-slate-700',
|
||||
isOverlapping
|
||||
? 'ring-2 ring-offset-slate-50 dark:ring-offset-slate-900 ring-blue-500 ring-offset-2'
|
||||
: '',
|
||||
!highlightOverlappingTables && isOverlapping
|
||||
? 'animate-scale'
|
||||
: '',
|
||||
highlightOverlappingTables && isOverlapping
|
||||
? 'animate-scale-2'
|
||||
: '',
|
||||
isDiffTableChanged &&
|
||||
!isDiffNewTable &&
|
||||
!isDiffTableRemoved
|
||||
? 'outline outline-[3px] outline-sky-500 dark:outline-sky-900 outline-offset-[5px]'
|
||||
: '',
|
||||
isDiffNewTable
|
||||
? 'outline outline-[3px] outline-green-500 dark:outline-green-900 outline-offset-[5px]'
|
||||
: '',
|
||||
isDiffTableRemoved
|
||||
? 'outline outline-[3px] outline-red-500 dark:outline-red-900 outline-offset-[5px]'
|
||||
: ''
|
||||
)}
|
||||
className={tableClassName}
|
||||
onClick={(e) => {
|
||||
if (e.detail === 2) {
|
||||
openTableInEditor();
|
||||
@@ -421,20 +502,14 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
||||
: `${TABLE_MINIMIZED_FIELDS * 2}rem`, // h-8 per field
|
||||
}}
|
||||
>
|
||||
{fields.map((field: DBField) => (
|
||||
{visibleFields.map((field: DBField) => (
|
||||
<TableNodeField
|
||||
key={field.id}
|
||||
focused={focused}
|
||||
tableNodeId={id}
|
||||
field={field}
|
||||
highlighted={selectedRelEdges.some(
|
||||
(edge) =>
|
||||
edge.data?.relationship
|
||||
.sourceFieldId === field.id ||
|
||||
edge.data?.relationship
|
||||
.targetFieldId === field.id
|
||||
)}
|
||||
visible={visibleFields.includes(field)}
|
||||
highlighted={highlightedFieldIds.has(field.id)}
|
||||
visible={true}
|
||||
isConnectable={!table.isView}
|
||||
/>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user