UNPKG

react-erd

Version:

An easy-to-use component for rendering Entity Relationship Diagrams in React

504 lines 20.2 kB
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 };