react-erd
Version:
An easy-to-use component for rendering Entity Relationship Diagrams in React
504 lines • 20.2 kB
JavaScript
import _createForOfIteratorHelperLoose from "@babel/runtime/helpers/esm/createForOfIteratorHelperLoose";
import _extends from "@babel/runtime/helpers/esm/extends";
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useReactFlow, ReactFlowProvider } from "reactflow";
import ReactFlow, { Handle, Position, ConnectionLineType, getSmoothStepPath } from "reactflow";
import Icon from "@mdi/react";
import { mdiKeyLink, mdiKey, mdiLinkVariant } from "@mdi/js";
import { jsx as _jsx } from "react/jsx-runtime";
import { jsxs as _jsxs } from "react/jsx-runtime";
import { Fragment as _Fragment } from "react/jsx-runtime";
var getRef = function getRef(schemaName, tableName, columnName) {
return [schemaName, tableName, columnName].filter(function (x) {
return x;
}).join(".");
};
var getEdges = function getEdges(schemas) {
return schemas.flatMap(function (schema) {
return schema.tables.flatMap(function (table) {
return table.columns.flatMap(function (column) {
return column.foreignKeys.map(function (foreignKey) {
return {
id: getRef(schema.name, table.name, column.name) + ":" + getRef(foreignKey.foreignSchemaName, foreignKey.foreignTableName, foreignKey.foreignColumnName),
source: getRef(schema.name, table.name),
sourceHandle: column.name,
target: getRef(foreignKey.foreignSchemaName, foreignKey.foreignTableName),
targetHandle: foreignKey.foreignColumnName,
type: "betweenTables",
deletable: !foreignKey.constrained
};
});
});
});
});
};
var X_MULTIPLIER = 350;
var Y_PADDING = 25;
var ROW_HEIGHT = 21;
var NODE_WIDTH = 250;
function RelationshipDiagram(_ref) {
var schemas = _ref.schemas,
onSchemasChange = _ref.onSchemasChange,
tableColors = _ref.tableColors,
onCreateForeignKey = _ref.onCreateForeignKey,
onDeleteForeignKey = _ref.onDeleteForeignKey,
onAttemptToRecreateExistingRelationship = _ref.onAttemptToRecreateExistingRelationship,
onAttemptToConnectColumnToItself = _ref.onAttemptToConnectColumnToItself,
onAttemptToDeleteConstrainedRelationship = _ref.onAttemptToDeleteConstrainedRelationship;
var reactFlowInstance = useReactFlow();
var schemasRef = useRef(schemas);
useEffect(function () {
schemasRef.current = schemas;
}, [schemas]);
var defaultEdges = useMemo(function () {
return getEdges(schemas);
}, [schemas]);
var _useState = useState(false),
isCreatingNewConnection = _useState[0],
setIsCreatingNewConnection = _useState[1];
var TableNodeComponent = useMemo(function () {
return function TableNodeComponent(_ref2) {
var _ref2$data = _ref2.data,
table = _ref2$data.table,
color = _ref2$data.color;
return /*#__PURE__*/_jsxs("div", {
style: {
width: NODE_WIDTH
},
children: [/*#__PURE__*/_jsx("div", {
className: "title",
style: {
borderTopColor: color
},
children: table.name
}), /*#__PURE__*/_jsx("ul", {
children: table.columns.map(function (column) {
return /*#__PURE__*/_jsxs("li", {
children: [function () {
var foreignKey = column.foreignKeys.filter(function (key) {
return key.constrained;
}).length >= 1;
var isPrimary = table.primaryKey === column.name;
if (foreignKey && isPrimary) {
return /*#__PURE__*/_jsx(Icon, {
path: mdiKeyLink,
className: "column-icon"
});
} else if (foreignKey) {
return /*#__PURE__*/_jsx(Icon, {
path: mdiLinkVariant,
className: "column-icon"
});
} else if (isPrimary) {
return /*#__PURE__*/_jsx(Icon, {
path: mdiKey,
className: "column-icon"
});
} else {
return /*#__PURE__*/_jsx("div", {
className: "column-icon"
});
}
}(), /*#__PURE__*/_jsx("div", {
className: "column-name",
children: column.name
}), /*#__PURE__*/_jsx("div", {
className: "column-type",
children: column.type
})]
}, column.name);
})
}), table.columns.map(function (column, index) {
var top = ROW_HEIGHT * 1.5 + index * ROW_HEIGHT + 5;
return /*#__PURE__*/_jsxs(Fragment, {
children: [/*#__PURE__*/_jsx(Handle, {
id: column.name,
type: "source",
position: Position.Left,
style: {
top: top,
transform: "translate(5px, -50%)",
pointerEvents: isCreatingNewConnection ? "none" : "all"
}
}), /*#__PURE__*/_jsx(Handle, {
id: column.name,
type: "target",
position: Position.Right,
style: {
top: top,
transform: "translate(-4px, -50%)",
pointerEvents: isCreatingNewConnection ? "all" : "none"
}
})]
}, column.name);
})]
});
};
}, [isCreatingNewConnection]);
var nodeTypes = useMemo(function () {
return {
table: TableNodeComponent
};
}, [TableNodeComponent]);
var BetweenTablesEdgeComponent = useMemo(function () {
return function BetweenTablesEdgeComponent(props) {
var id = props.id,
_props$style = props.style,
style = _props$style === void 0 ? {} : _props$style;
var _getSmoothStepPath = getSmoothStepPath(props),
edgePath = _getSmoothStepPath[0];
return /*#__PURE__*/_jsxs(_Fragment, {
children: [/*#__PURE__*/_jsx("path", {
id: id,
style: style,
className: "react-flow__edge-path",
d: edgePath
}), /*#__PURE__*/_jsx("line", {
x1: props.sourceX - 9,
x2: props.sourceX,
y1: props.sourceY,
y2: props.sourceY - 6,
style: style,
strokeLinecap: "round"
}), /*#__PURE__*/_jsx("line", {
x1: props.sourceX - 9,
x2: props.sourceX,
y1: props.sourceY,
y2: props.sourceY + 6,
style: style,
strokeLinecap: "round"
}), /*#__PURE__*/_jsx("line", {
x1: props.targetX + 10,
x2: props.targetX + 10,
y1: props.targetY - 8,
y2: props.targetY + 8,
style: style,
strokeLinecap: "round"
})]
});
};
}, []);
var edgeTypes = useMemo(function () {
return {
betweenTables: BetweenTablesEdgeComponent
};
}, [BetweenTablesEdgeComponent]);
var defaultNodes = useMemo(function () {
var nodes = [];
var allTables = schemas.flatMap(function (schema) {
return schema.tables.map(function (table) {
return _extends({}, table, {
schemaName: schema.name
});
});
});
var parentTables = allTables.filter(function (table) {
return table.columns.every(function (column) {
return column.foreignKeys.length === 0;
});
});
var maxX = 0;
function createNodesForTables(tables, parentXPosition, parentYBottom) {
if (parentXPosition === void 0) {
parentXPosition = 0;
}
if (parentYBottom === void 0) {
parentYBottom = 0;
}
var xPosition = parentXPosition + X_MULTIPLIER;
maxX = Math.max(xPosition, maxX);
var prevTableMaxY = 0;
var _loop = function _loop() {
var _step$value = _step.value,
index = _step$value[0],
table = _step$value[1];
var id = getRef(table.schemaName, table.name);
if (nodes.some(function (node) {
return node.data.id === id;
})) {
return "continue";
}
nodes;
var yPosition = Math.max(parentYBottom, prevTableMaxY);
var foreignKeysReferencingTable = allTables.flatMap(function (table) {
return table.columns.flatMap(function (column) {
return column.foreignKeys.filter(function (foreignKey) {
return getRef(foreignKey.foreignSchemaName, foreignKey.foreignTableName) === id;
});
});
});
nodes.push({
id: id,
data: {
id: id,
table: table,
foreignKeysReferencingTable: foreignKeysReferencingTable,
color: tableColors[nodes.length % tableColors.length]
},
position: {
x: xPosition,
y: yPosition
},
sourcePosition: Position.Left,
targetPosition: Position.Right,
type: "table"
});
var nextTables = allTables.filter(function (table) {
return table.columns.some(function (column) {
return column.foreignKeys.some(function (foreignKey) {
return getRef(foreignKey.foreignSchemaName, foreignKey.foreignTableName) === id;
});
});
});
var maxYPositionOfChildren = createNodesForTables(nextTables, xPosition, yPosition);
var nodeHeight = ROW_HEIGHT + table.columns.length * ROW_HEIGHT;
if (index === tables.length - 1) {
return {
v: yPosition + nodeHeight + Y_PADDING
};
} else {
prevTableMaxY = Math.max(maxYPositionOfChildren != null ? maxYPositionOfChildren : 0, yPosition + nodeHeight + Y_PADDING);
}
};
for (var _iterator = _createForOfIteratorHelperLoose(tables.entries()), _step; !(_step = _iterator()).done;) {
var _ret = _loop();
if (_ret === "continue") continue;
if (typeof _ret === "object") return _ret.v;
}
}
createNodesForTables(parentTables);
return nodes;
}, [schemas, tableColors]);
var handleConnectionStart = useCallback(function () {
setIsCreatingNewConnection(true);
}, []);
var handleConnectionEnd = useCallback(function () {
setIsCreatingNewConnection(false);
}, []);
var handleAddConnectionBetweenNodes = useCallback(function (connection) {
var _split = connection.source.split("."),
localSchemaName = _split[0],
localTableName = _split[1];
if (defaultEdges.some(function (edge) {
return edge.id === connection.source + "." + connection.sourceHandle + ":" + connection.target + "." + connection.targetHandle;
})) {
var _split2 = connection.target.split("."),
foreignSchemaName = _split2[0],
foreignTableName = _split2[1];
onAttemptToRecreateExistingRelationship == null ? void 0 : onAttemptToRecreateExistingRelationship({
localSchemaName: localSchemaName,
localTableName: localTableName,
localColumnName: connection.sourceHandle,
foreignSchemaName: foreignSchemaName,
foreignTableName: foreignTableName,
foreignColumnName: connection.targetHandle
});
return;
}
if (connection.target === connection.source && connection.targetHandle === connection.sourceHandle) {
onAttemptToConnectColumnToItself == null ? void 0 : onAttemptToConnectColumnToItself({
schemaName: localSchemaName,
tableName: localTableName,
columnName: connection.sourceHandle
});
return;
}
if (connection.target && connection.targetHandle && connection.source && connection.sourceHandle) {
var _connection$target$sp = connection.target.split("."),
targetSchema = _connection$target$sp[0],
targetTable = _connection$target$sp[1];
var _connection$source$sp = connection.source.split("."),
sourceSchema = _connection$source$sp[0],
sourceTable = _connection$source$sp[1];
var _newSchemas = schemasRef.current.map(function (schema) {
return _extends({}, schema, {
tables: schema.tables.map(function (table) {
return _extends({}, table, {
columns: table.columns.map(function (column) {
var ret = _extends({}, column);
if (connection.targetHandle && sourceSchema === schema.name && sourceTable === table.name && connection.sourceHandle === column.name) {
ret.foreignKeys.push({
foreignSchemaName: targetSchema,
foreignTableName: targetTable,
foreignColumnName: connection.targetHandle,
constrained: false
});
}
return ret;
})
});
})
});
});
onSchemasChange == null ? void 0 : onSchemasChange(_newSchemas);
onCreateForeignKey == null ? void 0 : onCreateForeignKey({
localSchemaName: sourceSchema,
localTableName: sourceTable,
localColumnName: connection.sourceHandle,
foreignSchemaName: targetSchema,
foreignTableName: targetTable,
foreignColumnName: connection.targetHandle
});
}
}, [defaultEdges, onAttemptToRecreateExistingRelationship, onAttemptToConnectColumnToItself, onSchemasChange, onCreateForeignKey]);
var newConnectionToCreateRef = useRef(null);
var handleEdgeUpdate = useCallback(function (_, newConnection) {
newConnectionToCreateRef.current = newConnection;
}, []);
var handleEdgeUpdateStart = useCallback(function () {
newConnectionToCreateRef.current = null;
}, []);
var handleEdgeUpdateEnd = useCallback(function (_, edgeToDelete) {
var onDeleteArg = function () {
var _edgeToDelete$source$ = edgeToDelete.source.split("."),
sourceSchema = _edgeToDelete$source$[0],
sourceTable = _edgeToDelete$source$[1];
var _edgeToDelete$target$ = edgeToDelete.target.split("."),
targetSchema = _edgeToDelete$target$[0],
targetTable = _edgeToDelete$target$[1];
return {
localSchemaName: sourceSchema,
localTableName: sourceTable,
localColumnName: edgeToDelete.sourceHandle,
foreignSchemaName: targetSchema,
foreignTableName: targetTable,
foreignColumnName: edgeToDelete.targetHandle
};
}();
if (!edgeToDelete.deletable) {
onAttemptToDeleteConstrainedRelationship == null ? void 0 : onAttemptToDeleteConstrainedRelationship(onDeleteArg);
}
onDeleteForeignKey == null ? void 0 : onDeleteForeignKey(onDeleteArg);
if (newConnectionToCreateRef.current) {
// this means the edge has been moved to a new handle - create a new foreign key
var onCreateArg = function () {
var _split3 = newConnectionToCreateRef.current.source.split("."),
sourceSchema = _split3[0],
sourceTable = _split3[1];
var _split4 = newConnectionToCreateRef.current.target.split("."),
targetSchema = _split4[0],
targetTable = _split4[1];
return {
localSchemaName: sourceSchema,
localTableName: sourceTable,
localColumnName: newConnectionToCreateRef.current.sourceHandle,
foreignSchemaName: targetSchema,
foreignTableName: targetTable,
foreignColumnName: newConnectionToCreateRef.current.targetHandle
};
}();
onCreateForeignKey == null ? void 0 : onCreateForeignKey(onCreateArg);
}
var newSchemas = schemasRef.current.map(function (schema) {
return _extends({}, schema, {
tables: schema.tables.map(function (table) {
return _extends({}, table, {
columns: table.columns.map(function (column) {
var ret = _extends({}, column);
var matchesColumn = function matchesColumn(edgeLike) {
return edgeLike.source === getRef(schema.name, table.name) && edgeLike.sourceHandle === column.name;
};
if (matchesColumn(edgeToDelete)) {
ret.foreignKeys = ret.foreignKeys.filter(function (foreignKey) {
var wasDeleted = getRef(foreignKey.foreignSchemaName, foreignKey.foreignTableName) === edgeToDelete.target && foreignKey.foreignColumnName === edgeToDelete.targetHandle;
return !wasDeleted;
});
}
if (newConnectionToCreateRef.current && matchesColumn(newConnectionToCreateRef.current)) {
var _split5 = newConnectionToCreateRef.current.target.split("."),
targetSchema = _split5[0],
targetTable = _split5[1];
ret.foreignKeys = [].concat(ret.foreignKeys, [{
foreignSchemaName: targetSchema,
foreignTableName: targetTable,
foreignColumnName: newConnectionToCreateRef.current.targetHandle,
constrained: false
}]);
}
return ret;
})
});
})
});
});
onSchemasChange == null ? void 0 : onSchemasChange(newSchemas);
}, [onAttemptToDeleteConstrainedRelationship, onCreateForeignKey, onDeleteForeignKey, onSchemasChange]);
var _useState2 = useState(null),
hoveredNodeId = _useState2[0],
setHoveredNodeId = _useState2[1];
var handleNodeMouseEnter = useCallback(function (_, node) {
setHoveredNodeId(node.id);
}, []);
var handleNodeMouseLeave = useCallback(function () {
setHoveredNodeId(null);
}, []);
useEffect(function () {
var checkIfShouldHighlightEdge = function checkIfShouldHighlightEdge(edge) {
return hoveredNodeId && [edge.source, edge.target].includes(hoveredNodeId);
};
reactFlowInstance.setEdges([].concat(defaultEdges).sort(function (a, b) {
return Number(checkIfShouldHighlightEdge(a)) - Number(checkIfShouldHighlightEdge(b));
}).map(function (edge) {
var _find;
var shouldHighlight = hoveredNodeId && checkIfShouldHighlightEdge(edge);
return _extends({}, edge, {
style: _extends({}, edge.style, {
zIndex: 500,
stroke: shouldHighlight ? (_find = (defaultNodes != null ? defaultNodes : []).find(function (node) {
return node.id === edge.target;
})) == null ? void 0 : _find.data.color : "rgb(var(--react-erd__secondary-color))",
strokeWidth: shouldHighlight ? 2 : 1,
transition: "stroke 0.3s"
})
});
}));
}, [defaultEdges, hoveredNodeId, defaultNodes, reactFlowInstance]);
var ids = useRef(new WeakMap());
var currId = useRef(0);
var getObjectId = useCallback(function (object) {
if (ids.current.has(object)) {
return ids.current.get(object);
} else {
var id = String(currId.current);
currId.current += 1;
ids.current.set(object, id);
return id;
}
}, []);
if (!defaultNodes) {
return /*#__PURE__*/_jsx("div", {
children: "Loading"
});
}
return /*#__PURE__*/_jsx(_Fragment, {
children: /*#__PURE__*/_jsx("div", {
className: "react-erd__container",
style: {
"--row-height": ROW_HEIGHT + "px"
},
children: /*#__PURE__*/_jsx(ReactFlow, {
nodeTypes: nodeTypes,
edgeTypes: edgeTypes,
defaultNodes: defaultNodes,
defaultEdges: defaultEdges,
fitView: true,
connectionRadius: 30,
connectionLineType: ConnectionLineType.SmoothStep,
onNodeMouseEnter: handleNodeMouseEnter,
onNodeMouseLeave: handleNodeMouseLeave,
onEdgeUpdate: handleEdgeUpdate,
onEdgeUpdateStart: handleEdgeUpdateStart,
onEdgeUpdateEnd: handleEdgeUpdateEnd,
onConnectStart: handleConnectionStart,
onConnectEnd: handleConnectionEnd,
onConnect: handleAddConnectionBetweenNodes
}, getObjectId(schemas))
})
});
}
function RelationshipDiagramWrapper(props) {
return /*#__PURE__*/_jsx(ReactFlowProvider, {
children: /*#__PURE__*/_jsx(RelationshipDiagram, _extends({}, props))
});
}
export { RelationshipDiagramWrapper as RelationshipDiagram };