UNPKG

react-erd

Version:

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

502 lines (501 loc) 19.4 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/RelationshipDiagram.tsx var RelationshipDiagram_exports = {}; __export(RelationshipDiagram_exports, { RelationshipDiagram: () => RelationshipDiagramWrapper }); module.exports = __toCommonJS(RelationshipDiagram_exports); var import_react = require("react"); var import_reactflow = require("reactflow"); var import_react2 = require("@mdi/react"); var import_js = require("@mdi/js"); var import_jsx_runtime = require("react/jsx-runtime"); var getRef = (schemaName, tableName, columnName) => [schemaName, tableName, columnName].filter((x) => x).join("."); var getEdges = (schemas) => schemas.flatMap((schema) => { return schema.tables.flatMap( (table) => table.columns.flatMap((column) => { return column.foreignKeys.map((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({ schemas, onSchemasChange, tableColors, onCreateForeignKey, onDeleteForeignKey, onAttemptToRecreateExistingRelationship, onAttemptToConnectColumnToItself, onAttemptToDeleteConstrainedRelationship }) { const reactFlowInstance = (0, import_reactflow.useReactFlow)(); const schemasRef = (0, import_react.useRef)(schemas); (0, import_react.useEffect)(() => { schemasRef.current = schemas; }, [schemas]); const defaultEdges = (0, import_react.useMemo)(() => getEdges(schemas), [schemas]); const [isCreatingNewConnection, setIsCreatingNewConnection] = (0, import_react.useState)(false); const TableNodeComponent = (0, import_react.useMemo)( () => function TableNodeComponent2({ data: { table, color } }) { return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { width: NODE_WIDTH }, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "title", style: { borderTopColor: color }, children: table.name }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: table.columns.map((column) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [ (() => { const foreignKey = column.foreignKeys.filter((key) => key.constrained).length >= 1; const isPrimary = table.primaryKey === column.name; if (foreignKey && isPrimary) { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.Icon, { path: import_js.mdiKeyLink, className: "column-icon" }); } else if (foreignKey) { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.Icon, { path: import_js.mdiLinkVariant, className: "column-icon" }); } else if (isPrimary) { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react2.Icon, { path: import_js.mdiKey, className: "column-icon" }); } else { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "column-icon" }); } })(), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "column-name", children: column.name }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "column-type", children: column.type }) ] }, column.name)) }), table.columns.map((column, index) => { const top = ROW_HEIGHT * 1.5 + index * ROW_HEIGHT + 5; return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react.Fragment, { children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_reactflow.Handle, { id: column.name, type: "source", position: import_reactflow.Position.Left, style: { top, transform: `translate(5px, -50%)`, pointerEvents: isCreatingNewConnection ? "none" : "all" } } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_reactflow.Handle, { id: column.name, type: "target", position: import_reactflow.Position.Right, style: { top, transform: `translate(-4px, -50%)`, pointerEvents: isCreatingNewConnection ? "all" : "none" } } ) ] }, column.name); }) ] }); }, [isCreatingNewConnection] ); const nodeTypes = (0, import_react.useMemo)( () => ({ table: TableNodeComponent }), [TableNodeComponent] ); const BetweenTablesEdgeComponent = (0, import_react.useMemo)( () => function BetweenTablesEdgeComponent2(props) { const { id, style = {} } = props; const [edgePath] = (0, import_reactflow.getSmoothStepPath)(props); return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "path", { id, style, className: "react-flow__edge-path", d: edgePath } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "line", { x1: props.sourceX - 9, x2: props.sourceX, y1: props.sourceY, y2: props.sourceY - 6, style, strokeLinecap: "round" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "line", { x1: props.sourceX - 9, x2: props.sourceX, y1: props.sourceY, y2: props.sourceY + 6, style, strokeLinecap: "round" } ), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "line", { x1: props.targetX + 10, x2: props.targetX + 10, y1: props.targetY - 8, y2: props.targetY + 8, style, strokeLinecap: "round" } ) ] }); }, [] ); const edgeTypes = (0, import_react.useMemo)( () => ({ betweenTables: BetweenTablesEdgeComponent }), [BetweenTablesEdgeComponent] ); const defaultNodes = (0, import_react.useMemo)(() => { const nodes = []; const allTables = schemas.flatMap( (schema) => schema.tables.map((table) => ({ ...table, schemaName: schema.name })) ); const parentTables = allTables.filter( (table) => table.columns.every((column) => column.foreignKeys.length === 0) ); let maxX = 0; function createNodesForTables(tables, parentXPosition = 0, parentYBottom = 0) { const xPosition = parentXPosition + X_MULTIPLIER; maxX = Math.max(xPosition, maxX); let prevTableMaxY = 0; for (const [index, table] of tables.entries()) { const id = getRef(table.schemaName, table.name); if (nodes.some((node) => node.data.id === id)) { continue; } nodes; const yPosition = Math.max(parentYBottom, prevTableMaxY); const foreignKeysReferencingTable = allTables.flatMap( (table2) => table2.columns.flatMap( (column) => column.foreignKeys.filter( (foreignKey) => getRef( foreignKey.foreignSchemaName, foreignKey.foreignTableName ) === id ) ) ); nodes.push({ id, data: { id, table, foreignKeysReferencingTable, color: tableColors[nodes.length % tableColors.length] }, position: { x: xPosition, y: yPosition }, sourcePosition: import_reactflow.Position.Left, targetPosition: import_reactflow.Position.Right, type: "table" }); const nextTables = allTables.filter( (table2) => table2.columns.some( (column) => column.foreignKeys.some( (foreignKey) => getRef( foreignKey.foreignSchemaName, foreignKey.foreignTableName ) === id ) ) ); const maxYPositionOfChildren = createNodesForTables( nextTables, xPosition, yPosition ); const nodeHeight = ROW_HEIGHT + table.columns.length * ROW_HEIGHT; if (index === tables.length - 1) { return yPosition + nodeHeight + Y_PADDING; } else { prevTableMaxY = Math.max( maxYPositionOfChildren ?? 0, yPosition + nodeHeight + Y_PADDING ); } } } createNodesForTables(parentTables); return nodes; }, [schemas, tableColors]); const handleConnectionStart = (0, import_react.useCallback)(() => { setIsCreatingNewConnection(true); }, []); const handleConnectionEnd = (0, import_react.useCallback)(() => { setIsCreatingNewConnection(false); }, []); const handleAddConnectionBetweenNodes = (0, import_react.useCallback)( (connection) => { const [localSchemaName, localTableName] = connection.source.split("."); if (defaultEdges.some( (edge) => edge.id === `${connection.source}.${connection.sourceHandle}:${connection.target}.${connection.targetHandle}` )) { const [foreignSchemaName, foreignTableName] = connection.target.split("."); onAttemptToRecreateExistingRelationship?.({ localSchemaName, localTableName, localColumnName: connection.sourceHandle, foreignSchemaName, foreignTableName, foreignColumnName: connection.targetHandle }); return; } if (connection.target === connection.source && connection.targetHandle === connection.sourceHandle) { onAttemptToConnectColumnToItself?.({ schemaName: localSchemaName, tableName: localTableName, columnName: connection.sourceHandle }); return; } if (connection.target && connection.targetHandle && connection.source && connection.sourceHandle) { const [targetSchema, targetTable] = connection.target.split("."); const [sourceSchema, sourceTable] = connection.source.split("."); const newSchemas = schemasRef.current.map((schema) => ({ ...schema, tables: schema.tables.map((table) => ({ ...table, columns: table.columns.map((column) => { const ret = { ...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?.(newSchemas); onCreateForeignKey?.({ localSchemaName: sourceSchema, localTableName: sourceTable, localColumnName: connection.sourceHandle, foreignSchemaName: targetSchema, foreignTableName: targetTable, foreignColumnName: connection.targetHandle }); } }, [ defaultEdges, onAttemptToRecreateExistingRelationship, onAttemptToConnectColumnToItself, onSchemasChange, onCreateForeignKey ] ); const newConnectionToCreateRef = (0, import_react.useRef)(null); const handleEdgeUpdate = (0, import_react.useCallback)( (_, newConnection) => { newConnectionToCreateRef.current = newConnection; }, [] ); const handleEdgeUpdateStart = (0, import_react.useCallback)(() => { newConnectionToCreateRef.current = null; }, []); const handleEdgeUpdateEnd = (0, import_react.useCallback)( (_, edgeToDelete) => { const onDeleteArg = (() => { const [sourceSchema, sourceTable] = edgeToDelete.source.split("."); const [targetSchema, targetTable] = edgeToDelete.target.split("."); return { localSchemaName: sourceSchema, localTableName: sourceTable, localColumnName: edgeToDelete.sourceHandle, foreignSchemaName: targetSchema, foreignTableName: targetTable, foreignColumnName: edgeToDelete.targetHandle }; })(); if (!edgeToDelete.deletable) { onAttemptToDeleteConstrainedRelationship?.(onDeleteArg); } onDeleteForeignKey?.(onDeleteArg); if (newConnectionToCreateRef.current) { const onCreateArg = (() => { const [sourceSchema, sourceTable] = newConnectionToCreateRef.current.source.split("."); const [targetSchema, targetTable] = newConnectionToCreateRef.current.target.split("."); return { localSchemaName: sourceSchema, localTableName: sourceTable, localColumnName: newConnectionToCreateRef.current.sourceHandle, foreignSchemaName: targetSchema, foreignTableName: targetTable, foreignColumnName: newConnectionToCreateRef.current.targetHandle }; })(); onCreateForeignKey?.(onCreateArg); } const newSchemas = schemasRef.current.map((schema) => ({ ...schema, tables: schema.tables.map((table) => ({ ...table, columns: table.columns.map((column) => { const ret = { ...column }; const matchesColumn = (edgeLike) => edgeLike.source === getRef(schema.name, table.name) && edgeLike.sourceHandle === column.name; if (matchesColumn(edgeToDelete)) { ret.foreignKeys = ret.foreignKeys.filter((foreignKey) => { const wasDeleted = getRef( foreignKey.foreignSchemaName, foreignKey.foreignTableName ) === edgeToDelete.target && foreignKey.foreignColumnName === edgeToDelete.targetHandle; return !wasDeleted; }); } if (newConnectionToCreateRef.current && matchesColumn(newConnectionToCreateRef.current)) { const [targetSchema, targetTable] = newConnectionToCreateRef.current.target.split("."); ret.foreignKeys = [ ...ret.foreignKeys, { foreignSchemaName: targetSchema, foreignTableName: targetTable, foreignColumnName: newConnectionToCreateRef.current.targetHandle, constrained: false } ]; } return ret; }) })) })); onSchemasChange?.(newSchemas); }, [ onAttemptToDeleteConstrainedRelationship, onCreateForeignKey, onDeleteForeignKey, onSchemasChange ] ); const [hoveredNodeId, setHoveredNodeId] = (0, import_react.useState)(null); const handleNodeMouseEnter = (0, import_react.useCallback)((_, node) => { setHoveredNodeId(node.id); }, []); const handleNodeMouseLeave = (0, import_react.useCallback)(() => { setHoveredNodeId(null); }, []); (0, import_react.useEffect)(() => { const checkIfShouldHighlightEdge = (edge) => hoveredNodeId && [edge.source, edge.target].includes(hoveredNodeId); reactFlowInstance.setEdges( [...defaultEdges].sort( (a, b) => Number(checkIfShouldHighlightEdge(a)) - Number(checkIfShouldHighlightEdge(b)) ).map((edge) => { const shouldHighlight = hoveredNodeId && checkIfShouldHighlightEdge(edge); return { ...edge, style: { ...edge.style, zIndex: 500, stroke: shouldHighlight ? (defaultNodes ?? []).find((node) => node.id === edge.target)?.data.color : "rgb(var(--react-erd__secondary-color))", strokeWidth: shouldHighlight ? 2 : 1, transition: "stroke 0.3s" } }; }) ); }, [defaultEdges, hoveredNodeId, defaultNodes, reactFlowInstance]); const ids = (0, import_react.useRef)(/* @__PURE__ */ new WeakMap()); const currId = (0, import_react.useRef)(0); const getObjectId = (0, import_react.useCallback)((object) => { if (ids.current.has(object)) { return ids.current.get(object); } else { const id = String(currId.current); currId.current += 1; ids.current.set(object, id); return id; } }, []); if (!defaultNodes) { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: "Loading" }); } return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "div", { className: "react-erd__container", style: { "--row-height": `${ROW_HEIGHT}px` }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)( import_reactflow.ReactFlow, { nodeTypes, edgeTypes, defaultNodes, defaultEdges, fitView: true, connectionRadius: 30, connectionLineType: import_reactflow.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__ */ (0, import_jsx_runtime.jsx)(import_reactflow.ReactFlowProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RelationshipDiagram, { ...props }) }); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { RelationshipDiagram }); //# sourceMappingURL=RelationshipDiagram.cjs.map