diff --git a/package-lock.json b/package-lock.json index 3e77f62..69c46fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "qbx", "version": "0.0.0", "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@mui/material": "^7.1.1", "@univerjs-pro/engine-pivot": "^0.7.0", "@univerjs-pro/sheets-pivot": "^0.7.0", "@univerjs-pro/sheets-pivot-ui": "^0.7.0", @@ -54,7 +57,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -110,7 +112,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.27.1", @@ -144,7 +145,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -186,7 +186,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -196,7 +195,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -230,7 +228,6 @@ "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", - "devOptional": true, "license": "MIT", "dependencies": { "@babel/types": "^7.27.1" @@ -287,7 +284,6 @@ "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -302,7 +298,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -321,7 +316,6 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -331,7 +325,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", - "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -341,6 +334,158 @@ "node": ">=6.9.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", + "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", + "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", @@ -1064,7 +1209,6 @@ "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -1079,7 +1223,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1089,7 +1232,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1099,14 +1241,12 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "devOptional": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1146,6 +1286,225 @@ "node": ">=18" } }, + "node_modules/@mui/core-downloads-tracker": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.1.1.tgz", + "integrity": "sha512-yBckQs4aQ8mqukLnPC6ivIRv6guhaXi8snVl00VtyojBbm+l6VbVhyTSZ68Abcx7Ah8B+GZhrB7BOli+e+9LkQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/material": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.1.1.tgz", + "integrity": "sha512-mTpdmdZCaHCGOH3SrYM41+XKvNL0iQfM9KlYgpSjgadXx/fEKhhvOktxm8++Xw6FFeOHoOiV+lzOI8X1rsv71A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.1", + "@mui/core-downloads-tracker": "^7.1.1", + "@mui/system": "^7.1.1", + "@mui/types": "^7.4.3", + "@mui/utils": "^7.1.1", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^19.1.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^7.1.1", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/react-is": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", + "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", + "license": "MIT" + }, + "node_modules/@mui/private-theming": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.1.tgz", + "integrity": "sha512-M8NbLUx+armk2ZuaxBkkMk11ultnWmrPlN0Xe3jUEaBChg/mcxa5HWIWS1EE4DF36WRACaAHVAvyekWlDQf0PQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.1", + "@mui/utils": "^7.1.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.1.tgz", + "integrity": "sha512-R2wpzmSN127j26HrCPYVQ53vvMcT5DaKLoWkrfwUYq3cYytL6TQrCH8JBH3z79B6g4nMZZVoaXrxO757AlShaw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.1", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.1.1.tgz", + "integrity": "sha512-Kj1uhiqnj4Zo7PDjAOghtXJtNABunWvhcRU0O7RQJ7WOxeynoH6wXPcilphV8QTFtkKaip8EiNJRiCD+B3eROA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.1", + "@mui/private-theming": "^7.1.1", + "@mui/styled-engine": "^7.1.1", + "@mui/types": "^7.4.3", + "@mui/utils": "^7.1.1", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.3.tgz", + "integrity": "sha512-2UCEiK29vtiZTeLdS2d4GndBKacVyxGvReznGXGr+CzW/YhjIX+OHUdCIczZjzcRAgKBGmE9zCIgoV9FleuyRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.1" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.1.tgz", + "integrity": "sha512-BkOt2q7MBYl7pweY2JWwfrlahhp+uGLR8S+EhiyRaofeRYUWL2YKbSGQvN4hgSN1i8poN0PaUiii1kEMrchvzg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.1", + "@mui/types": "^7.4.3", + "@types/prop-types": "^15.7.14", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils/node_modules/react-is": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", + "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", + "license": "MIT" + }, "node_modules/@noble/ed25519": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-2.2.3.tgz", @@ -1167,6 +1526,16 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -2854,6 +3223,18 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.1.4", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz", @@ -2885,6 +3266,15 @@ "redux": "^4.0.0" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resize-observer-browser": { "version": "0.1.11", "resolved": "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.11.tgz", @@ -5234,6 +5624,21 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -5350,7 +5755,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5575,6 +5979,31 @@ "node": ">= 0.10" } }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6022,7 +6451,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -6182,6 +6610,15 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -6288,7 +6725,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -6664,6 +7100,12 @@ "node": ">= 0.8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -7038,7 +7480,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -7087,6 +7528,27 @@ "node": ">= 0.10" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -7157,7 +7619,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -7173,6 +7634,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -7239,6 +7706,12 @@ "immediate": "~3.0.5" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/localforage": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", @@ -7427,7 +7900,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/n-gram": { @@ -7637,7 +8109,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -7646,6 +8117,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -7676,6 +8165,12 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, "node_modules/path-to-regexp": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", @@ -7686,11 +8181,19 @@ "node": ">=16" } }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "devOptional": true, "license": "ISC" }, "node_modules/picomatch": { @@ -8412,11 +8915,30 @@ "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", "license": "MIT" }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -8701,6 +9223,15 @@ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -8768,6 +9299,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8781,6 +9318,18 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/tailwind-merge": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.0.tgz", @@ -9225,6 +9774,21 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index 519474a..0d93485 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "copy-assets": "npm run copy-luckysheet-assets && npm run copy-univerjs-assets" }, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@mui/material": "^7.1.1", "@univerjs-pro/engine-pivot": "^0.7.0", "@univerjs-pro/sheets-pivot": "^0.7.0", "@univerjs-pro/sheets-pivot-ui": "^0.7.0", diff --git a/src/App.jsx b/src/App.jsx index b49fca5..ad000b2 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -3,7 +3,31 @@ import './App.css' import InfiniteCanvas from './components/InfiniteCanvas' import AdvancedCharts from './components/AdvancedCharts' import DataflowCanvas from './components/DataflowCanvas' +import ERDiagramCanvas from './components/ERDiagramCanvas' import { FaDatabase, FaChartBar, FaProjectDiagram, FaSitemap, FaFolder, FaCog, FaChevronDown, FaChevronRight, FaTachometerAlt } from 'react-icons/fa' +import { Breadcrumbs, Link, Typography } from '@mui/material' +import { createTheme, ThemeProvider } from '@mui/material/styles' + +// Create a custom Material UI theme to match your application's dark theme +const darkTheme = createTheme({ + palette: { + mode: 'dark', + primary: { + main: '#00a99d', + }, + text: { + primary: '#ffffff', + secondary: '#aaaaaa', + }, + background: { + default: '#121212', + paper: '#1a1a1a', + }, + }, + typography: { + fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif', + }, +}); function App() { // Initialize activeTab based on URL pathname if present @@ -92,15 +116,16 @@ function App() { }, []); return ( -
+ +
{/* Logo and Title */}
@@ -392,6 +417,43 @@ function App() { }}> {activeTab === 'canvas' && ( <> +
+

+ Overview +

+ + { + e.preventDefault(); + setIsDropdownOpen(true); // Ensure dropdown is open + }} + sx={{ fontWeight: 500 }} + > + Qubit + + Overview + +
- -
-

- - Entity Relationship Diagram -

-

- View table relationships • Explore schema structure • Analyze foreign keys -

+ ER Diagram +
+ )}
+ ) } diff --git a/src/assets/img/plan-plus-icon.svg b/src/assets/img/plan-plus-icon.svg new file mode 100644 index 0000000..3533304 --- /dev/null +++ b/src/assets/img/plan-plus-icon.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/img/planPlusLogo.png b/src/assets/img/planPlusLogo.png new file mode 100644 index 0000000..78c8ce3 Binary files /dev/null and b/src/assets/img/planPlusLogo.png differ diff --git a/src/components/AddTableModal.jsx b/src/components/AddTableModal.jsx new file mode 100644 index 0000000..cc86bdf --- /dev/null +++ b/src/components/AddTableModal.jsx @@ -0,0 +1,707 @@ +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + TextField, + Select, + MenuItem, + FormControl, + InputLabel, + IconButton, + Box, + Typography, + Divider, + Grid, + Paper, + Chip, + Alert +} from '@mui/material'; +import { + FaPlus as AddIcon, + FaTrash as DeleteIcon, + FaTimes as CloseIcon, + FaTable as TableIcon, + FaKey as KeyIcon, + FaLink as LinkIcon +} from 'react-icons/fa'; + +const AddTableModal = ({ + open, + onClose, + onAddTable, + schemas = [], + existingTables = [], + tableTypes = [ + { value: 'fact', label: 'Fact Table' }, + { value: 'dimension', label: 'Dimension Table' }, + { value: 'stage', label: 'Stage Table' } + ], + columnTypes = [ + 'INTEGER', + 'VARCHAR(255)', + 'VARCHAR(100)', + 'VARCHAR(50)', + 'TEXT', + 'DECIMAL(10,2)', + 'DECIMAL(15,2)', + 'DATE', + 'TIMESTAMP', + 'BOOLEAN', + 'BIGINT', + 'SMALLINT', + 'FLOAT', + 'DOUBLE' + ] +}) => { + // Main form state + const [formData, setFormData] = useState({ + name: '', + description: '', + tableType: '', + schema: '' + }); + + // Dynamic sections state + const [columns, setColumns] = useState([]); + const [keys, setKeys] = useState([]); + const [relations, setRelations] = useState([]); + + // Validation and UI state + const [errors, setErrors] = useState({}); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Reset form when modal opens/closes + useEffect(() => { + if (open) { + resetForm(); + } + }, [open]); + + const resetForm = () => { + setFormData({ + name: '', + description: '', + tableType: '', + schema: '' + }); + setColumns([]); + setKeys([]); + setRelations([]); + setErrors({}); + setIsSubmitting(false); + }; + + // Form field handlers + const handleFormChange = (field, value) => { + setFormData(prev => ({ + ...prev, + [field]: value + })); + // Clear error when user starts typing + if (errors[field]) { + setErrors(prev => ({ + ...prev, + [field]: null + })); + } + }; + + // Column management + const addColumn = () => { + const newColumn = { + id: Date.now(), + name: '', + type: 'VARCHAR(255)', + isPrimaryKey: false, + isForeignKey: false, + isNullable: true + }; + setColumns(prev => [...prev, newColumn]); + }; + + const updateColumn = (id, field, value) => { + setColumns(prev => prev.map(col => + col.id === id ? { ...col, [field]: value } : col + )); + }; + + const removeColumn = (id) => { + setColumns(prev => prev.filter(col => col.id !== id)); + // Remove any keys that reference this column + setKeys(prev => prev.filter(key => key.columnId !== id)); + }; + + // Key management + const addKey = () => { + const newKey = { + id: Date.now(), + name: '', + columnId: '', + sequence: 1, + keyType: 'PRIMARY' // PRIMARY, FOREIGN, UNIQUE + }; + setKeys(prev => [...prev, newKey]); + }; + + const updateKey = (id, field, value) => { + setKeys(prev => prev.map(key => + key.id === id ? { ...key, [field]: value } : key + )); + }; + + const removeKey = (id) => { + setKeys(prev => prev.filter(key => key.id !== id)); + }; + + // Relation management + const addRelation = () => { + const newRelation = { + id: Date.now(), + targetTable: '', + sourceKey: '', + targetKey: '', + relationType: '1:N' // 1:1, 1:N, N:M + }; + setRelations(prev => [...prev, newRelation]); + }; + + const updateRelation = (id, field, value) => { + setRelations(prev => prev.map(rel => + rel.id === id ? { ...rel, [field]: value } : rel + )); + }; + + const removeRelation = (id) => { + setRelations(prev => prev.filter(rel => rel.id !== id)); + }; + + // Get available keys for relations + const getAvailableKeys = () => { + return keys.map(key => { + const column = columns.find(col => col.id === key.columnId); + return { + id: key.id, + name: key.name, + columnName: column?.name || 'Unknown Column' + }; + }); + }; + + // Get keys from selected target table + const getTargetTableKeys = (tableId) => { + const targetTable = existingTables.find(table => table.id === tableId); + if (!targetTable || !targetTable.columns) return []; + + return targetTable.columns + .filter(col => col.is_primary_key || col.is_foreign_key) + .map(col => ({ + id: col.name, + name: col.name, + type: col.is_primary_key ? 'PRIMARY' : 'FOREIGN' + })); + }; + + // Form validation + const validateForm = () => { + const newErrors = {}; + + if (!formData.name.trim()) { + newErrors.name = 'Table name is required'; + } + + if (!formData.schema) { + newErrors.schema = 'Schema selection is required'; + } + + if (!formData.tableType) { + newErrors.tableType = 'Table type is required'; + } + + // Validate columns + const columnErrors = {}; + columns.forEach(col => { + if (!col.name.trim()) { + columnErrors[col.id] = 'Column name is required'; + } + }); + + if (Object.keys(columnErrors).length > 0) { + newErrors.columns = columnErrors; + } + + // Validate keys + const keyErrors = {}; + keys.forEach(key => { + if (!key.name.trim()) { + keyErrors[key.id] = 'Key name is required'; + } + if (!key.columnId) { + keyErrors[key.id] = 'Column selection is required'; + } + }); + + if (Object.keys(keyErrors).length > 0) { + newErrors.keys = keyErrors; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + // Form submission + const handleSubmit = async () => { + if (!validateForm()) { + return; + } + + setIsSubmitting(true); + + try { + // Prepare table data + const tableData = { + name: formData.name.trim(), + description: formData.description.trim(), + table_type: formData.tableType, + schema: formData.schema, + columns: columns.map(col => ({ + name: col.name.trim(), + data_type: col.type, + is_primary_key: keys.some(key => key.columnId === col.id && key.keyType === 'PRIMARY'), + is_foreign_key: keys.some(key => key.columnId === col.id && key.keyType === 'FOREIGN'), + is_nullable: col.isNullable + })), + keys: keys.map(key => ({ + name: key.name.trim(), + column_name: columns.find(col => col.id === key.columnId)?.name || '', + key_type: key.keyType, + sequence: key.sequence + })), + relations: relations.map(rel => ({ + target_table: rel.targetTable, + source_key: rel.sourceKey, + target_key: rel.targetKey, + relation_type: rel.relationType + })) + }; + + // Call the parent component's add table function + await onAddTable(tableData); + + // Close modal and reset form + onClose(); + resetForm(); + } catch (error) { + console.error('Error adding table:', error); + setErrors({ submit: 'Failed to add table. Please try again.' }); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + + Add New Table + + + + + + + + {errors.submit && ( + + {errors.submit} + + )} + + {/* Basic Table Information */} + + + Table Information + + + + + handleFormChange('name', e.target.value)} + error={!!errors.name} + helperText={errors.name} + required + /> + + + + + Table Type + + + + + + + Schema + + + + + + handleFormChange('description', e.target.value)} + multiline + rows={3} + placeholder="Enter table description..." + /> + + + + + {/* Columns Section */} + + + + Columns + + + + + {columns.length === 0 ? ( + + No columns added yet. Click "Add Column" to get started. + + ) : ( + + {columns.map((column) => ( + + + + updateColumn(column.id, 'name', e.target.value)} + error={!!errors.columns?.[column.id]} + helperText={errors.columns?.[column.id]} + size="small" + /> + + + + Data Type + + + + + + updateColumn(column.id, 'isNullable', !column.isNullable)} + color={column.isNullable ? "default" : "primary"} + /> + + + + removeColumn(column.id)} + color="error" + size="small" + > + + + + + + ))} + + )} + + + {/* Keys Section */} + + + + + Keys + + + + + {keys.length === 0 ? ( + + {columns.length === 0 + ? "Add columns first before defining keys." + : "No keys defined yet. Click \"Add Key\" to create primary or foreign keys." + } + + ) : ( + + {keys.map((key) => ( + + + + updateKey(key.id, 'name', e.target.value)} + error={!!errors.keys?.[key.id]} + helperText={errors.keys?.[key.id]} + size="small" + /> + + + + Column + + + + + + Key Type + + + + + updateKey(key.id, 'sequence', parseInt(e.target.value) || 1)} + size="small" + inputProps={{ min: 1 }} + /> + + + removeKey(key.id)} + color="error" + size="small" + > + + + + + + ))} + + )} + + + {/* Relations Section */} + + + + + Relations + + + + + {relations.length === 0 ? ( + + {keys.length === 0 + ? "Define keys first before creating relations." + : existingTables.length === 0 + ? "No existing tables available for relations." + : "No relations defined yet. Click \"Add Relation\" to create table relationships." + } + + ) : ( + + {relations.map((relation) => ( + + + + + Target Table + + + + + + Source Key + + + + + + Target Key + + + + + + Relation Type + + + + + removeRelation(relation.id)} + color="error" + size="small" + > + + + + + + ))} + + )} + + + + + + + + + ); +}; + +export default AddTableModal; \ No newline at end of file diff --git a/src/components/ERDiagramCanvas.jsx b/src/components/ERDiagramCanvas.jsx new file mode 100644 index 0000000..9fff256 --- /dev/null +++ b/src/components/ERDiagramCanvas.jsx @@ -0,0 +1,2137 @@ +import { useState, useCallback, useRef, useEffect, useMemo } from 'react'; +import ReactFlow, { + MiniMap, + Controls, + Background, + useNodesState, + useEdgesState, + addEdge, + Panel, + useReactFlow, + ReactFlowProvider, + Handle, + Position, + BaseEdge, + EdgeLabelRenderer, + getBezierPath, + MarkerType +} from 'reactflow'; +import 'reactflow/dist/style.css'; +import axios from 'axios'; + +// Import icons from react-icons +import { + FaDatabase, + FaTable, + FaPlus, + FaTimes, + FaKey, + FaLink, + FaLayerGroup, + FaColumns, + FaProjectDiagram, + FaChevronDown, + FaChevronRight, + FaServer, + FaCloud, + FaHdd +} from 'react-icons/fa'; +import { CustomDatabaseIcon, CustomDocumentIcon, CustomDimensionIcon } from './CustomIcons'; +import { Breadcrumbs, Link, Typography, Menu, MenuItem, ListItemIcon, ListItemText } from '@mui/material'; +import AddTableModal from './AddTableModal'; + +// Custom styles for ER Diagram +const generateERStyles = () => { + return ` + .react-flow__node { + z-index: 1; + margin: 20px; + } + + .er-database-wrapper { + z-index: -2; + pointer-events: all; + border-radius: 25px; + cursor: grab; + user-select: none; + transition: all 0.2s ease; + padding: 100px; + border: 3px solid rgba(0, 169, 157, 0.4); + background: rgba(0, 169, 157, 0.03); + box-shadow: 0 0 30px rgba(0, 169, 157, 0.1); + } + + .er-database-wrapper:hover { + border-color: rgba(0, 169, 157, 0.6); + background: rgba(0, 169, 157, 0.05); + box-shadow: 0 0 40px rgba(0, 169, 157, 0.15); + } + + .er-database-wrapper:active { + cursor: grabbing; + } + + .er-database-label { + position: absolute; + top: 20px; + left: 20px; + background: rgba(0, 169, 157, 0.95); + color: white; + padding: 12px 16px; + border-radius: 8px; + font-size: 14px; + font-weight: bold; + box-shadow: 0 4px 12px rgba(0, 169, 157, 0.3); + display: flex; + align-items: center; + gap: 10px; + z-index: 15; + } + + .er-schema-group { + z-index: -1; + pointer-events: all; + border-radius: 20px; + cursor: grab; + user-select: none; + transition: all 0.2s ease; + padding: 80px; + border: 2px dashed rgba(138, 43, 226, 0.3); + background: rgba(138, 43, 226, 0.05); + position: relative; + overflow: visible; + } + + .er-schema-group:hover { + border-color: rgba(138, 43, 226, 0.5); + background: rgba(138, 43, 226, 0.08); + box-shadow: 0 0 20px rgba(138, 43, 226, 0.1); + } + + .er-schema-group:active { + cursor: grabbing; + } + + .er-schema-label { + position: absolute; + top: 15px; + left: 15px; + background: rgba(138, 43, 226, 0.9); + color: white; + padding: 8px 12px; + border-radius: 6px; + font-size: 12px; + font-weight: bold; + box-shadow: 0 2px 8px rgba(138, 43, 226, 0.3); + display: flex; + align-items: center; + gap: 8px; + z-index: 10; + } + + .er-table-node { + background: white; + border: 2px solid #e1e5e9; + border-radius: 10px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + width: 260px; + min-height: 200px; + transition: all 0.2s ease; + } + + .er-table-node:hover { + border-color: #8a2be2; + box-shadow: 0 4px 16px rgba(138, 43, 226, 0.2); + } + + .er-table-header { + background: linear-gradient(135deg, #8a2be2, #9932cc); + color: white; + padding: 14px 16px; + border-radius: 8px 8px 0 0; + display: flex; + align-items: center; + gap: 10px; + font-weight: bold; + font-size: 15px; + } + + .er-table-header.stage { + background: linear-gradient(135deg, #00a99d, #52c41a); + } + + .er-table-header.fact { + background: linear-gradient(135deg, #fa8c16, #faad14); + } + + .er-table-header.dimension { + background: linear-gradient(135deg, #52c41a, #73d13d); + } + + .er-column-list { + padding: 0; + margin: 0; + list-style: none; + max-height: 300px; + overflow-y: auto; + } + + .er-column-item { + padding: 10px 16px; + border-bottom: 1px solid #f0f0f0; + display: flex; + align-items: center; + gap: 10px; + font-size: 13px; + transition: background-color 0.2s ease; + } + + .er-column-item:hover { + background-color: #f8f9fa; + } + + .er-column-item:last-child { + border-bottom: none; + } + + .er-column-name { + flex: 1; + font-weight: 500; + color: #333; + } + + .er-column-type { + color: #666; + font-size: 11px; + background: #f5f5f5; + padding: 2px 6px; + border-radius: 3px; + } + + .er-primary-key { + color: #faad14; + } + + .er-foreign-key { + color: #8a2be2; + } + + .er-relationship-edge { + stroke: #8a2be2; + stroke-width: 2; + } + + .er-relationship-edge .react-flow__edge-path { + stroke: #8a2be2; + stroke-width: 2; + } + + .er-relationship-edge-animated { + stroke: #8a2be2; + stroke-width: 2; + stroke-dasharray: 5; + animation: dashdraw 0.5s linear infinite; + } + + @keyframes dashdraw { + to { + stroke-dashoffset: -10; + } + } + + /* Enhanced arrow markers */ + .react-flow__edge.selected .react-flow__edge-path { + stroke: #faad14 !important; + stroke-width: 3 !important; + } + + .react-flow__edge:hover .react-flow__edge-path { + stroke: #52c41a !important; + stroke-width: 3 !important; + } + + /* Custom arrow styling */ + .er-custom-arrow { + pointer-events: none; + z-index: 10; + } + + /* Cardinality indicators */ + .er-cardinality-indicator { + background: rgba(255, 255, 255, 0.95) !important; + border: 1px solid #8a2be2 !important; + border-radius: 3px !important; + padding: 2px 6px !important; + font-size: 10px !important; + font-weight: bold !important; + color: #8a2be2 !important; + box-shadow: 0 1px 3px rgba(0,0,0,0.2) !important; + } + + /* Relationship direction indicators */ + .er-direction-arrow { + fill: #8a2be2; + stroke: #8a2be2; + stroke-width: 1; + } + + .er-direction-arrow.selected { + fill: #faad14; + stroke: #faad14; + } + + .er-relationship-label { + background: rgba(138, 43, 226, 0.9); + color: white; + padding: 4px 8px; + border-radius: 4px; + font-size: 10px; + font-weight: bold; + } + + .er-relationship-label:hover + .relationship-tooltip { + opacity: 1 !important; + } + + .er-add-button { + position: fixed; + bottom: 30px; + right: 30px; + width: 60px; + height: 60px; + border-radius: 50%; + background: linear-gradient(135deg, #8a2be2, #9932cc); + border: none; + color: white; + font-size: 24px; + cursor: pointer; + box-shadow: 0 4px 16px rgba(138, 43, 226, 0.3); + transition: all 0.2s ease; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + } + + .er-add-button:hover { + transform: scale(1.1); + box-shadow: 0 6px 20px rgba(138, 43, 226, 0.4); + } + + .er-add-button:active { + transform: scale(0.95); + } + + .er-breadcrumb-dropdown { + display: inline-flex; + align-items: center; + cursor: pointer; + padding: 4px 8px; + border-radius: 4px; + transition: background-color 0.2s ease; + } + + .er-breadcrumb-dropdown:hover { + background-color: rgba(0, 169, 157, 0.1); + } + + .er-breadcrumb-dropdown .dropdown-arrow { + margin-left: 4px; + font-size: 12px; + transition: transform 0.2s ease; + } + + .er-breadcrumb-dropdown.open .dropdown-arrow { + transform: rotate(180deg); + } + + .er-service-menu { + background: rgba(26, 26, 26, 0.95) !important; + border: 1px solid rgba(0, 169, 157, 0.3) !important; + border-radius: 8px !important; + backdrop-filter: blur(10px) !important; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important; + } + + .er-service-menu .MuiMenuItem-root { + color: #ffffff !important; + padding: 12px 16px !important; + border-radius: 4px !important; + margin: 4px 8px !important; + transition: all 0.2s ease !important; + } + + .er-service-menu .MuiMenuItem-root:hover { + background-color: rgba(0, 169, 157, 0.1) !important; + } + + .er-service-menu .MuiListItemIcon-root { + color: #00a99d !important; + min-width: 32px !important; + } + + /* Ensure ReactFlow markers are visible */ + .react-flow__edges { + z-index: 1; + } + + .react-flow__edge { + pointer-events: all; + } + + .react-flow__edge path { + stroke: #8a2be2; + stroke-width: 2; + } + + .react-flow__edge.selected path { + stroke: #faad14 !important; + stroke-width: 3 !important; + } + + /* Custom arrows are always visible */ + .er-custom-arrow { + pointer-events: none; + z-index: 10; + } + `; +}; + +// Custom Table Node Component for ER Diagram +const ERTableNode = ({ data, id }) => { + const getTableIcon = () => { + switch(data.table_type) { + case 'stage': + return ; + case 'fact': + return ; + case 'dimension': + return ; + default: + return ; + } + }; + + return ( +
+ + + +
+ {getTableIcon()} + {data.name} +
+ +
    + {data.columns && data.columns.map((column, index) => ( +
  • + {column.is_primary_key && } + {column.is_foreign_key && } + {column.name} + {column.data_type} +
  • + ))} +
+
+ ); +}; + +// Custom Edge Component for Relationships +const ERRelationshipEdge = ({ + id, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + data, + selected +}) => { + const [edgePath, labelX, labelY] = getBezierPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }); + + // Determine relationship type and styling + const relationshipType = data?.relationship_type || '1:N'; + const isOneToMany = relationshipType === '1:N'; + const isOneToOne = relationshipType === '1:1'; + const isManyToMany = relationshipType === 'N:M'; + + // Calculate arrow position and angle + const dx = targetX - sourceX; + const dy = targetY - sourceY; + const angle = Math.atan2(dy, dx) * (180 / Math.PI); + + // Arrow position (closer to target) + const arrowDistance = 20; + const arrowX = targetX - (arrowDistance * Math.cos(Math.atan2(dy, dx))); + const arrowY = targetY - (arrowDistance * Math.sin(Math.atan2(dy, dx))); + + // Start arrow position for many-to-many + const startArrowX = sourceX + (arrowDistance * Math.cos(Math.atan2(dy, dx))); + const startArrowY = sourceY + (arrowDistance * Math.sin(Math.atan2(dy, dx))); + + return ( + <> + {/* Main edge path */} + + + {/* Custom Arrow at target */} + + + {/* Custom Arrow at source for many-to-many */} + {isManyToMany && ( + + )} + + {/* Relationship cardinality indicators - positioned directly on the line */} + + {/* Source cardinality (near source table) - positioned on the line */} +
+ {isOneToMany || isOneToOne ? '1' : 'N'} +
+ + {/* Target cardinality (near target table) - positioned on the line */} +
+ {isOneToOne ? '1' : 'N'} +
+ + {/* Main relationship label - centered on the line */} +
+
+ {relationshipType} +
+ {/* Column mapping tooltip - shown on hover */} + {data?.source_column && data?.target_column && ( +
+ {data.source_column} → {data.target_column} +
+ )} +
+
+ + ); +}; + +// Database Wrapper Node Component +const ERDatabaseWrapperNode = ({ data, id }) => { + // Auto-calculate dimensions based on schema count and content + const schemaCount = data.schemaCount || 0; + const totalTables = data.totalTables || 0; + + // Dynamic sizing for database wrapper - much larger to contain all schemas + const autoWidth = Math.max(1400, data.contentWidth + 200); // Base width + content + padding + const autoHeight = Math.max(1000, data.contentHeight + 200); // Base height + content + padding + + return ( +
+
+ + {data.name} + + ({schemaCount} schemas • {totalTables} tables) + +
+
+ ); +}; + +// Schema Group Node Component with Auto-sizing +const ERSchemaGroupNode = ({ data, id }) => { + // Auto-calculate dimensions based on table count and content + const tableCount = data.tableCount || 0; + const tablesPerRow = Math.ceil(Math.sqrt(tableCount)); + const rows = Math.ceil(tableCount / tablesPerRow); + + // Dynamic sizing based on content + const autoWidth = Math.max(800, tablesPerRow * 320 + 160); // Base width + table width + padding + const autoHeight = Math.max(600, rows * 280 + 200); // Base height + table height + padding + + return ( +
+
+ + {data.name} + + ({tableCount} tables) + +
+ {tableCount === 0 && ( +
+ +
+ No tables in this schema +
+ )} +
+ ); +}; + +// Node types for ER Diagram +const nodeTypes = { + erTable: ERTableNode, + erSchemaGroup: ERSchemaGroupNode, + erDatabaseWrapper: ERDatabaseWrapperNode, +}; + +// Edge types for ER Diagram +const edgeTypes = { + erRelationship: ERRelationshipEdge, +}; + +// Mock data for services and data sources +const mockServices = [ + { + id: 'plan-plus', + name: 'Plan Plus', + icon: , + dataSources: [ + { id: 'postgres-main', name: 'PostgreSQL Main', type: 'PostgreSQL', icon: }, + { id: 'mysql-analytics', name: 'MySQL Analytics', type: 'MySQL', icon: }, + { id: 'mongodb-logs', name: 'MongoDB Logs', type: 'MongoDB', icon: } + ] + }, + { + id: 'project-1', + name: 'Project 1', + icon: , + dataSources: [ + { id: 'snowflake-dw', name: 'Snowflake DW', type: 'Snowflake', icon: }, + { id: 'redshift-analytics', name: 'Redshift Analytics', type: 'Redshift', icon: } + ] + }, + { + id: 'project-2', + name: 'Project 2', + icon: , + dataSources: [ + { id: 'bigquery-main', name: 'BigQuery Main', type: 'BigQuery', icon: }, + { id: 'postgres-backup', name: 'PostgreSQL Backup', type: 'PostgreSQL', icon: } + ] + } +]; + +// Hierarchical Breadcrumb Component +const HierarchicalBreadcrumb = ({ + selectedService, + selectedDataSource, + onServiceChange, + onDataSourceChange +}) => { + const [serviceMenuAnchor, setServiceMenuAnchor] = useState(null); + const [dataSourceMenuAnchor, setDataSourceMenuAnchor] = useState(null); + + const handleServiceClick = (event) => { + setServiceMenuAnchor(event.currentTarget); + }; + + const handleDataSourceClick = (event) => { + setDataSourceMenuAnchor(event.currentTarget); + }; + + const handleServiceSelect = (service) => { + onServiceChange(service); + setServiceMenuAnchor(null); + // Reset data source when service changes + if (service.dataSources.length > 0) { + onDataSourceChange(service.dataSources[0]); + } + }; + + const handleDataSourceSelect = (dataSource) => { + onDataSourceChange(dataSource); + setDataSourceMenuAnchor(null); + }; + + const handleCloseMenus = () => { + setServiceMenuAnchor(null); + setDataSourceMenuAnchor(null); + }; + + return ( + <> + + e.preventDefault()} + sx={{ fontWeight: 500 }} + > + Qubit + + + ER Diagram + + {/* DBTEZ Services Dropdown */} +
+ + DBTEZ Services + + +
+ + {/* Selected Service */} + {selectedService && ( + + {selectedService.icon} + {selectedService.name} + + )} +
+ + {/* Services Menu */} + + {mockServices.map((service) => ( + handleServiceSelect(service)} + selected={selectedService?.id === service.id} + > + + {service.icon} + + + + ))} + + + {/* Data Sources Menu */} + {selectedService && ( + + {selectedService.dataSources.map((dataSource) => ( + handleDataSourceSelect(dataSource)} + selected={selectedDataSource?.id === dataSource.id} + > + + {dataSource.icon} + + + + ))} + + )} + + ); +}; + +// Main ER Diagram Canvas Component +const ERDiagramCanvasContent = () => { + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + const [isLoading, setIsLoading] = useState(true); + const [databases, setDatabases] = useState([]); + const [selectedDatabase, setSelectedDatabase] = useState(null); + const [showAddMenu, setShowAddMenu] = useState(false); + + // ReactFlow instance for programmatic control + const { fitView } = useReactFlow(); + + // Service and Data Source selection state + const [selectedService, setSelectedService] = useState(mockServices[0]); // Default to Plan Plus + const [selectedDataSource, setSelectedDataSource] = useState(mockServices[0].dataSources[0]); // Default to first data source + const [isUsingMockData, setIsUsingMockData] = useState(false); + const [apiError, setApiError] = useState(null); + const [dataSourceMenuAnchor, setDataSourceMenuAnchor] = useState(null); + + // Add Table Modal state + const [isAddTableModalOpen, setIsAddTableModalOpen] = useState(false); + const [availableSchemas, setAvailableSchemas] = useState([]); + const [existingTables, setExistingTables] = useState([]); + + // API Configuration + const API_BASE_URL = 'https://sandbox.kezel.io/api'; + const token = "abdhsg"; + const orgSlug = "sN05Pjv11qvH"; + + // API endpoints + const ENDPOINTS = { + DATABASE_LIST: `${API_BASE_URL}/qbt_database_list_get`, + SCHEMA_LIST: `${API_BASE_URL}/qbt_schema_list_get`, + TABLE_LIST: `${API_BASE_URL}/qbt_table_list_get`, + COLUMN_LIST: `${API_BASE_URL}/qbt_column_list_get`, + SCHEMA_CREATE: `${API_BASE_URL}/qbt_schema_create`, + SCHEMA_DELETE: `${API_BASE_URL}/qbt_schema_delete`, + SCHEMA_UPDATE: `${API_BASE_URL}/qbt_schema_update`, + TABLE_CREATE: `${API_BASE_URL}/qbt_table_create`, + TABLE_UPDATE: `${API_BASE_URL}/qbt_table_update`, + TABLE_DELETE: `${API_BASE_URL}/qbt_table_delete`, + COLUMN_CREATE: `${API_BASE_URL}/qbt_column_create`, + COLUMN_UPDATE: `${API_BASE_URL}/qbt_column_update`, + COLUMN_DELETE: `${API_BASE_URL}/qbt_column_delete` + }; + + // Mock database data representing ONE database with multiple schemas (fallback only) + // const mockDatabaseData = { + // id: 1, + // name: "Sample Database", + // slug: "sample_database", + // description: "Sample Database Structure (Mock Data)", + // service: selectedService?.name || "Unknown Service", + // dataSource: selectedDataSource?.name || "Unknown DataSource", + // schemas: [ + // { + // sch: "FINANCE_MART", + // tables: [ + // { + // id: 1, + // name: "accounts", + // table_type: "dimension", + // columns: [ + // { name: "account_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, + // { name: "account_number", data_type: "VARCHAR(20)", is_primary_key: false, is_foreign_key: false }, + // { name: "account_name", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false }, + // { name: "account_type", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false }, + // { name: "balance", data_type: "DECIMAL(15,2)", is_primary_key: false, is_foreign_key: false }, + // { name: "created_date", data_type: "TIMESTAMP", is_primary_key: false, is_foreign_key: false } + // ] + // }, + // { + // id: 2, + // name: "transactions", + // table_type: "fact", + // columns: [ + // { name: "transaction_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, + // { name: "account_id", data_type: "INTEGER", is_primary_key: false, is_foreign_key: true }, + // { name: "transaction_date", data_type: "DATE", is_primary_key: false, is_foreign_key: false }, + // { name: "amount", data_type: "DECIMAL(12,2)", is_primary_key: false, is_foreign_key: false }, + // { name: "transaction_type", data_type: "VARCHAR(20)", is_primary_key: false, is_foreign_key: false }, + // { name: "description", data_type: "VARCHAR(255)", is_primary_key: false, is_foreign_key: false } + // ] + // }, + // { + // id: 3, + // name: "customers", + // table_type: "dimension", + // columns: [ + // { name: "customer_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, + // { name: "first_name", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false }, + // { name: "last_name", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false }, + // { name: "email", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false }, + // { name: "phone", data_type: "VARCHAR(20)", is_primary_key: false, is_foreign_key: false }, + // { name: "registration_date", data_type: "TIMESTAMP", is_primary_key: false, is_foreign_key: false } + // ] + // }, + // { + // id: 4, + // name: "financial_reports", + // table_type: "fact", + // columns: [ + // { name: "report_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, + // { name: "account_id", data_type: "INTEGER", is_primary_key: false, is_foreign_key: true }, + // { name: "report_date", data_type: "DATE", is_primary_key: false, is_foreign_key: false }, + // { name: "balance_amount", data_type: "DECIMAL(15,2)", is_primary_key: false, is_foreign_key: false }, + // { name: "profit_loss", data_type: "DECIMAL(12,2)", is_primary_key: false, is_foreign_key: false } + // ] + // } + // ] + // }, + // { + // sch: "Schema100", + // tables: [ + // { + // id: 5, + // name: "products", + // table_type: "dimension", + // columns: [ + // { name: "product_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, + // { name: "product_name", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false }, + // { name: "category", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false }, + // { name: "price", data_type: "DECIMAL(10,2)", is_primary_key: false, is_foreign_key: false }, + // { name: "stock_quantity", data_type: "INTEGER", is_primary_key: false, is_foreign_key: false } + // ] + // }, + // { + // id: 6, + // name: "orders", + // table_type: "fact", + // columns: [ + // { name: "order_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, + // { name: "customer_id", data_type: "INTEGER", is_primary_key: false, is_foreign_key: true }, + // { name: "product_id", data_type: "INTEGER", is_primary_key: false, is_foreign_key: true }, + // { name: "order_date", data_type: "DATE", is_primary_key: false, is_foreign_key: false }, + // { name: "quantity", data_type: "INTEGER", is_primary_key: false, is_foreign_key: false }, + // { name: "total_amount", data_type: "DECIMAL(12,2)", is_primary_key: false, is_foreign_key: false } + // ] + // }, + // { + // id: 7, + // name: "inventory", + // table_type: "stage", + // columns: [ + // { name: "inventory_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, + // { name: "product_id", data_type: "INTEGER", is_primary_key: false, is_foreign_key: true }, + // { name: "warehouse_location", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false }, + // { name: "stock_level", data_type: "INTEGER", is_primary_key: false, is_foreign_key: false }, + // { name: "last_updated", data_type: "TIMESTAMP", is_primary_key: false, is_foreign_key: false } + // ] + // }, + // { + // id: 8, + // name: "suppliers", + // table_type: "dimension", + // columns: [ + // { name: "supplier_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, + // { name: "supplier_name", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false }, + // { name: "contact_email", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false }, + // { name: "phone_number", data_type: "VARCHAR(20)", is_primary_key: false, is_foreign_key: false } + // ] + // } + // ] + // }, + // { + // sch: "New_sch", + // tables: [ + // { + // id: 9, + // name: "analytics_summary", + // table_type: "fact", + // columns: [ + // { name: "summary_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, + // { name: "date", data_type: "DATE", is_primary_key: false, is_foreign_key: false }, + // { name: "total_revenue", data_type: "DECIMAL(15,2)", is_primary_key: false, is_foreign_key: false }, + // { name: "total_orders", data_type: "INTEGER", is_primary_key: false, is_foreign_key: false }, + // { name: "unique_customers", data_type: "INTEGER", is_primary_key: false, is_foreign_key: false } + // ] + // }, + // { + // id: 10, + // name: "user_sessions", + // table_type: "stage", + // columns: [ + // { name: "session_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, + // { name: "customer_id", data_type: "INTEGER", is_primary_key: false, is_foreign_key: true }, + // { name: "session_start", data_type: "TIMESTAMP", is_primary_key: false, is_foreign_key: false }, + // { name: "session_end", data_type: "TIMESTAMP", is_primary_key: false, is_foreign_key: false }, + // { name: "pages_viewed", data_type: "INTEGER", is_primary_key: false, is_foreign_key: false }, + // { name: "device_type", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false } + // ] + // }, + // { + // id: 11, + // name: "reports", + // table_type: "dimension", + // columns: [ + // { name: "report_id", data_type: "INTEGER", is_primary_key: true, is_foreign_key: false }, + // { name: "report_name", data_type: "VARCHAR(100)", is_primary_key: false, is_foreign_key: false }, + // { name: "report_type", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false }, + // { name: "created_by", data_type: "VARCHAR(50)", is_primary_key: false, is_foreign_key: false }, + // { name: "created_date", data_type: "TIMESTAMP", is_primary_key: false, is_foreign_key: false } + // ] + // } + // ] + // } + // ] + // }; + + // API Functions for fetching real data + const fetchDatabases = async () => { + try { + const response = await axios.post( + ENDPOINTS.DATABASE_LIST, + { + token: token, + org: orgSlug, + }, + { + headers: { + 'Content-Type': 'application/json' + } + } + ); + + console.log('Database list response:', response.data); + const databases = response.data.items || []; + console.log(`Found ${databases.length} databases:`, databases); + + return databases.filter(db => db.con); // Filter out databases without connection slug + } catch (error) { + console.error('Error fetching databases:', error); + throw error; + } + }; + + const fetchSchemas = async (dbSlug) => { + try { + console.log(`Fetching schemas for database slug: ${dbSlug}`); + const response = await axios.post( + ENDPOINTS.SCHEMA_LIST, + { + token: token, + org: orgSlug, + con: dbSlug + }, + { + headers: { + 'Content-Type': 'application/json' + } + } + ); + + console.log(`Schema list for database ${dbSlug}:`, response.data); + + let schemas = []; + if (Array.isArray(response.data.items)) { + schemas = response.data.items.map(item => ({ + name: item.name, + sch: item.sch, // Use 'sch' as the slug + description: item.description || "", + created_at: item.created_at, + is_validated: item.is_validated, + database: dbSlug + })); + } + + console.log(`Number of schemas found for database ${dbSlug}: ${schemas.length}`); + console.log('Schema details:', schemas.map(s => ({ name: s.name, sch: s.sch }))); + return schemas; + } catch (error) { + console.error(`Error fetching schemas for database ${dbSlug}:`, error); + throw error; + } + }; + + const fetchTables = async (dbSlug, schemaSlug) => { + try { + console.log(`Fetching tables for database: ${dbSlug}, schema: ${schemaSlug}`); + const response = await axios.post( + ENDPOINTS.TABLE_LIST, + { + token: token, + org: orgSlug, + con: dbSlug, + sch: schemaSlug + }, + { + headers: { + 'Content-Type': 'application/json' + } + } + ); + + console.log(`Table list for schema ${schemaSlug}:`, response.data); + + let tables = []; + if (Array.isArray(response.data.items)) { + tables = response.data.items.map(item => ({ + id: item.id || `${dbSlug}-${schemaSlug}-${item.name}`, + name: item.name, + tbl: item.tbl, // Table slug + description: item.description || "", + table_type: item.table_type || "dimension", // Default to dimension if not specified + created_at: item.created_at, + database: dbSlug, + schema: schemaSlug + })); + } + + console.log(`Number of tables found for schema ${schemaSlug}: ${tables.length}`); + return tables; + } catch (error) { + console.error(`Error fetching tables for schema ${schemaSlug}:`, error); + throw error; + } + }; + + const fetchColumns = async (dbSlug, schemaSlug, tableSlug) => { + try { + console.log(`Fetching columns for database: ${dbSlug}, schema: ${schemaSlug}, table: ${tableSlug}`); + const response = await axios.post( + ENDPOINTS.COLUMN_LIST, + { + token: token, + org: orgSlug, + con: dbSlug, + sch: schemaSlug, + tbl: tableSlug + }, + { + headers: { + 'Content-Type': 'application/json' + } + } + ); + + console.log(`Column list for table ${tableSlug}:`, response.data); + + let columns = []; + if (Array.isArray(response.data.items)) { + columns = response.data.items.map((item, index) => ({ + name: item.name, + col: item.col, // Column slug + data_type: item.data_type || "VARCHAR(255)", + description: item.description || "", + is_primary_key: index === 0, // Assume first column is primary key + is_foreign_key: item.name.toLowerCase().includes('_id') && index > 0, // Simple heuristic for foreign keys + is_nullable: item.is_nullable || false, + database: dbSlug, + schema: schemaSlug, + table: tableSlug + })); + } + + console.log(`Number of columns found for table ${tableSlug}: ${columns.length}`); + return columns; + } catch (error) { + console.error(`Error fetching columns for table ${tableSlug}:`, error); + throw error; + } + }; + + // Function to fetch complete database structure with schemas, tables, and columns + const fetchCompleteDatabase = async (dbSlug) => { + try { + console.log(`Fetching complete structure for database: ${dbSlug}`); + + // Fetch schemas + const schemas = await fetchSchemas(dbSlug); + + // Fetch tables and columns for each schema + const schemasWithTables = await Promise.all( + schemas.map(async (schema) => { + try { + const tables = await fetchTables(dbSlug, schema.sch); + + // Fetch columns for each table + const tablesWithColumns = await Promise.all( + tables.map(async (table) => { + try { + const columns = await fetchColumns(dbSlug, schema.sch, table.tbl); + return { + ...table, + columns: columns + }; + } catch (columnError) { + console.warn(`Error fetching columns for table ${table.name}:`, columnError); + return { + ...table, + columns: [] // Return table with empty columns if column fetch fails + }; + } + }) + ); + + return { + ...schema, + tables: tablesWithColumns + }; + } catch (tableError) { + console.warn(`Error fetching tables for schema ${schema.sch}:`, tableError); + return { + ...schema, + tables: [] // Return schema with empty tables if table fetch fails + }; + } + }) + ); + + console.log(`Complete database structure for ${dbSlug}:`, schemasWithTables); + return schemasWithTables; + } catch (error) { + console.error(`Error fetching complete database structure for ${dbSlug}:`, error); + throw error; + } + }; + + // Generate dummy ER relationship data based on single database structure + const generateDummyERData = (database) => { + const erData = []; + + if (database.schemas && database.schemas.length > 0) { + database.schemas.forEach(schema => { + if (schema.tables && schema.tables.length > 0) { + // Create relationships between tables in the same schema + for (let i = 0; i < schema.tables.length - 1; i++) { + const sourceTable = schema.tables[i]; + const targetTable = schema.tables[i + 1]; + + // Create a dummy relationship with varied types + const relationshipTypes = ['1:N', '1:1', 'N:M']; + const randomType = relationshipTypes[Math.floor(Math.random() * relationshipTypes.length)]; + + erData.push({ + source_column_set: [ + { + table_id: sourceTable.id, + column_name: sourceTable.columns?.[0]?.name || 'id', + table_name: sourceTable.name, + schema_name: schema.sch + } + ], + destination_column_set: [ + { + table_id: targetTable.id, + column_name: `${sourceTable.name}_id`, + table_name: targetTable.name, + schema_name: schema.sch + } + ], + relationship_type: randomType + }); + } + } + }); + } + + return erData; + }; + + // Fetch databases and generate ER diagram using real API + useEffect(() => { + const fetchERData = async () => { + try { + setIsLoading(true); + + console.log(`Fetching ER data for service: ${selectedService?.name}, data source: ${selectedDataSource?.name}`); + + try { + // Fetch databases from API + const databases = await fetchDatabases(); + console.log('Fetched databases:', databases); + + if (databases && databases.length > 0) { + // Select database based on selectedDataSource or use first available + const targetDatabase = databases.find(db => + selectedDataSource?.name === db.name || + selectedDataSource?.name === db.con || + selectedDataSource?.slug === db.con + ) || databases[0]; // Fallback to first database if no match + + console.log('Selected database for ER diagram:', targetDatabase); + console.log('Available databases:', databases.map(db => ({ name: db.name, con: db.con }))); + + // Fetch complete structure (schemas, tables, columns) for the selected database + const schemasWithTables = await fetchCompleteDatabase(targetDatabase.con); + + // Create the complete database object using actual API data + const completeDatabase = { + id: targetDatabase.id, + name: targetDatabase.name, + slug: targetDatabase.con, + description: targetDatabase.description || `Database: ${targetDatabase.name}`, + service: selectedService?.name, + dataSource: selectedDataSource?.name, + schemas: schemasWithTables + }; + + console.log('Complete database structure:', completeDatabase); + console.log('Schemas in database:', completeDatabase.schemas.map(s => ({ + name: s.name, + sch: s.sch, + tableCount: s.tables?.length || 0, + tables: s.tables?.map(t => t.name) || [] + }))); + + setDatabases([completeDatabase]); // Wrap in array for compatibility + setSelectedDatabase(completeDatabase); // Set selected database for modal + generateERDiagram(completeDatabase); + setIsUsingMockData(false); + setApiError(null); + console.log('Successfully fetched and generated ER diagram from API data'); + + } else { + throw new Error('No databases found from API'); + } + } catch (apiError) { + // Handle specific error types + let errorMessage = 'Unknown API error'; + if (apiError.code === 'ERR_NETWORK' || apiError.message.includes('CORS')) { + errorMessage = 'CORS error - API not accessible from browser'; + console.warn('CORS error detected, using mock data:', apiError.message); + } else if (apiError.response?.status === 403) { + errorMessage = '403 Forbidden - Check API token and permissions'; + console.warn('403 Forbidden error, using mock data. Check your API token and permissions.'); + } else if (apiError.response?.status === 401) { + errorMessage = '401 Unauthorized - Invalid API token'; + console.warn('401 Unauthorized error, using mock data. Check your API token.'); + } else { + errorMessage = `API Error: ${apiError.message}`; + console.warn('API error, using mock data:', apiError.message); + } + + setApiError(errorMessage); + setIsUsingMockData(true); + + // Fallback to mock data + const singleDbData = { + ...mockDatabaseData, + service: selectedService?.name, + dataSource: selectedDataSource?.name + }; + + setDatabases([singleDbData]); // Wrap in array for compatibility + setSelectedDatabase(singleDbData); // Set selected database for modal + generateERDiagram(singleDbData); + console.log('Using mock data for ER diagram due to API error'); + } + } catch (error) { + console.error('Unexpected error in fetchERData:', error); + setApiError('Unexpected error occurred'); + setIsUsingMockData(true); + + // Fallback to mock data + const singleDbData = { + ...mockDatabaseData, + service: selectedService?.name, + dataSource: selectedDataSource?.name + }; + + setDatabases([singleDbData]); + setSelectedDatabase(singleDbData); // Set selected database for modal + generateERDiagram(singleDbData); + } finally { + setIsLoading(false); + } + }; + + // Only fetch if we have selected service and data source + if (selectedService && selectedDataSource) { + fetchERData(); + } + }, [selectedService, selectedDataSource]); // Re-fetch when service or data source changes + + // Auto-fit view when nodes are updated + useEffect(() => { + if (nodes.length > 0 && !isLoading) { + const timer = setTimeout(() => { + fitView({ + padding: 0.1, + includeHiddenNodes: false, + minZoom: 0.15, + maxZoom: 0.8, + duration: 600 + }); + }, 300); + + return () => clearTimeout(timer); + } + }, [nodes, isLoading, fitView]); + + // Auto-alignment layout calculator + const calculateOptimalLayout = (databaseData) => { + const layouts = []; + let totalWidth = 0; + let maxHeight = 0; + + databaseData.forEach((db) => { + if (db.schemas && db.schemas.length > 0) { + const dbLayout = { schemas: [], totalWidth: 0, maxHeight: 0 }; + + db.schemas.forEach((schema) => { + if (schema.tables && schema.tables.length > 0) { + const tableCount = schema.tables.length; + const tablesPerRow = Math.min(4, Math.ceil(Math.sqrt(tableCount))); // Max 4 tables per row + const rows = Math.ceil(tableCount / tablesPerRow); + + // Calculate optimal schema dimensions + const schemaWidth = Math.max(800, tablesPerRow * 320 + 160); + const schemaHeight = Math.max(600, rows * 280 + 200); + + dbLayout.schemas.push({ + schema, + width: schemaWidth, + height: schemaHeight, + tablesPerRow, + rows + }); + + dbLayout.totalWidth += schemaWidth + 100; // Add spacing + dbLayout.maxHeight = Math.max(dbLayout.maxHeight, schemaHeight); + } + }); + + layouts.push(dbLayout); + totalWidth = Math.max(totalWidth, dbLayout.totalWidth); + maxHeight += dbLayout.maxHeight + 100; // Add vertical spacing + } + }); + + return { layouts, totalWidth, maxHeight }; + }; + + // Handle adding a new table + const handleAddTable = async (tableData) => { + try { + console.log('Adding new table:', tableData); + + // Here you would typically make an API call to create the table + // For now, we'll add it to the current diagram + + // Find the target schema + const targetSchema = availableSchemas.find(schema => schema.sch === tableData.schema); + if (!targetSchema) { + throw new Error('Target schema not found'); + } + + // Create a new table object + const newTable = { + id: `new-table-${Date.now()}`, + name: tableData.name, + description: tableData.description, + table_type: tableData.table_type, + columns: tableData.columns, + schema: tableData.schema, + database: selectedDatabase?.name || 'Unknown' + }; + + // Add the table to existing tables list + setExistingTables(prev => [...prev, newTable]); + + // Update the current database structure and regenerate the diagram + const updatedDatabase = { ...selectedDatabase }; + const schemaIndex = updatedDatabase.schemas.findIndex(s => s.sch === tableData.schema); + + if (schemaIndex !== -1) { + if (!updatedDatabase.schemas[schemaIndex].tables) { + updatedDatabase.schemas[schemaIndex].tables = []; + } + updatedDatabase.schemas[schemaIndex].tables.push(newTable); + + // Regenerate the ER diagram with the new table + generateERDiagram(updatedDatabase); + + console.log('Table added successfully:', newTable); + } + + } catch (error) { + console.error('Error adding table:', error); + throw error; + } + }; + + // Generate ER diagram with Database Wrapper structure + const generateERDiagram = (database) => { + const newNodes = []; + const newEdges = []; + + // Generate dummy relationships + const relationships = generateDummyERData(database); + + // Process the selected database + console.log('Generating ER diagram for database:', database?.name); + console.log('Total schemas in database:', database?.schemas?.length); + console.log('Schema details:', database?.schemas?.map(s => ({ name: s.name, sch: s.sch, tableCount: s.tables?.length || 0 }))); + + if (database && database.schemas && database.schemas.length > 0) { + // Calculate database wrapper dimensions + let totalSchemaWidth = 0; + let maxSchemaHeight = 0; + let totalTables = 0; + + // Calculate layout for all schemas + const schemaLayouts = database.schemas.map(schema => { + const tableCount = (schema.tables && schema.tables.length) || 0; + totalTables += tableCount; + + if (tableCount > 0) { + const tablesPerRow = Math.min(3, Math.ceil(Math.sqrt(tableCount))); // Max 3 tables per row for better fit + const rows = Math.ceil(tableCount / tablesPerRow); + + // Calculate schema dimensions with proper padding for tables + const tableWidth = 260; // Width of each table node + const tableHeight = 280; // Height of each table node (including spacing) + const tableSpacingCalc = 330; // Horizontal spacing between tables + const tableRowSpacingCalc = 320; // Vertical spacing between table rows + const schemaPadding = 200; // Padding around the schema content + + // Calculate required width and height based on table layout + const tableStartX = 90; // Starting X position within schema + const tableStartY = 120; // Starting Y position within schema + const requiredWidth = tableStartX + (tablesPerRow * tableSpacingCalc) + 100; // Extra margin + const requiredHeight = tableStartY + (rows * tableRowSpacingCalc) + 100; // Extra margin + + const schemaWidth = Math.max(900, requiredWidth); + const schemaHeight = Math.max(650, requiredHeight); + + console.log(`Schema "${schema.name || schema.sch}" layout: ${tableCount} tables, ${tablesPerRow} per row, ${rows} rows`); + console.log(`Required dimensions: ${requiredWidth}x${requiredHeight}, Final: ${schemaWidth}x${schemaHeight}`); + + totalSchemaWidth += schemaWidth + 150; // Add spacing between schemas + maxSchemaHeight = Math.max(maxSchemaHeight, schemaHeight); + + return { + schema, + width: schemaWidth, + height: schemaHeight, + tablesPerRow, + rows, + tableCount + }; + } else { + // Handle schemas with no tables - show empty schema container + const schemaWidth = 900; // Minimum width for empty schema + const schemaHeight = 400; // Minimum height for empty schema + + totalSchemaWidth += schemaWidth + 150; // Add spacing between schemas + maxSchemaHeight = Math.max(maxSchemaHeight, schemaHeight); + + return { + schema, + width: schemaWidth, + height: schemaHeight, + tablesPerRow: 0, + rows: 0, + tableCount: 0 + }; + } + }); + + console.log('Schema layouts created:', schemaLayouts.length); + console.log('Schema layout details:', schemaLayouts.map(sl => ({ + name: sl.schema.name || sl.schema.sch, + sch: sl.schema.sch, + tableCount: sl.tableCount, + width: sl.width, + height: sl.height + }))); + + // Create Database Wrapper Node + const databaseWrapperId = `database-${database.slug}`; + const databaseWrapperWidth = Math.max(2000, totalSchemaWidth + 300); + const databaseWrapperHeight = Math.max(1400, maxSchemaHeight + 400); + + newNodes.push({ + id: databaseWrapperId, + type: 'erDatabaseWrapper', + position: { x: 50, y: 50 }, + data: { + name: database.name, + schemaCount: database.schemas.length, + totalTables: totalTables, + width: databaseWrapperWidth, + height: databaseWrapperHeight, + contentWidth: totalSchemaWidth, + contentHeight: maxSchemaHeight + }, + draggable: true, + selectable: false, + style: { + width: databaseWrapperWidth, + height: databaseWrapperHeight, + zIndex: -2 + } + }); + + // Create Schema Group Nodes within Database Wrapper + let schemaXOffset = 180; // Start position within database wrapper + const schemaYOffset = 180; // Y position within database wrapper + + schemaLayouts.forEach((schemaLayout, schemaIndex) => { + const schemaGroupId = `schema-${database.slug}-${schemaLayout.schema.sch}`; + + // Create schema group node + newNodes.push({ + id: schemaGroupId, + type: 'erSchemaGroup', + position: { x: schemaXOffset, y: schemaYOffset }, + data: { + name: schemaLayout.schema.name || schemaLayout.schema.sch, // Use schema name, fallback to sch + sch: schemaLayout.schema.sch, // Keep sch for identification + tableCount: schemaLayout.tableCount, + width: schemaLayout.width, + height: schemaLayout.height + }, + draggable: true, + selectable: false, + parentNode: databaseWrapperId, + extent: 'parent', + style: { + width: schemaLayout.width, + height: schemaLayout.height, + zIndex: -1 + } + }); + + // Create table nodes within schema (only if tables exist) + if (schemaLayout.schema.tables && schemaLayout.schema.tables.length > 0) { + const tableSpacing = 330; // Horizontal spacing between tables + const tableRowSpacing = 320; // Vertical spacing between table rows + const tableStartX = 90; // Starting X position within schema (accounting for schema padding) + const tableStartY = 120; // Starting Y position within schema (accounting for schema padding and label) + let currentRow = 0; + let currentCol = 0; + + schemaLayout.schema.tables.forEach((table, tableIndex) => { + const tableId = `table-${table.id || `${database.slug}-${schemaLayout.schema.sch}-${table.name}`}`; + + // Calculate table position within schema (relative to schema, not absolute) + const tableX = tableStartX + (currentCol * tableSpacing); + const tableY = tableStartY + (currentRow * tableRowSpacing); + + // Debug table positioning + if (tableIndex === 0) { + console.log(`Schema "${schemaLayout.schema.name || schemaLayout.schema.sch}" dimensions: ${schemaLayout.width}x${schemaLayout.height}`); + console.log(`Tables per row: ${schemaLayout.tablesPerRow}, Total rows: ${schemaLayout.rows}`); + console.log(`Table count: ${schemaLayout.tableCount}`); + } + + // Ensure table position is within schema bounds + const maxTableX = schemaLayout.width - 280; // Table width + some margin + const maxTableY = schemaLayout.height - 300; // Table height + some margin + + if (tableX > maxTableX || tableY > maxTableY) { + console.warn(`Table "${table.name}" position (${tableX}, ${tableY}) exceeds schema bounds (${schemaLayout.width}x${schemaLayout.height})`); + } + + // Add primary key and foreign key indicators to columns + const enhancedColumns = (table.columns || []).map((col, colIndex) => ({ + ...col, + is_primary_key: colIndex === 0, // First column as primary key + is_foreign_key: relationships.some(rel => + rel.destination_column_set.some(dest => + dest.column_name === col.name && dest.table_name === table.name + ) + ) + })); + + newNodes.push({ + id: tableId, + type: 'erTable', + position: { x: tableX, y: tableY }, + data: { + ...table, + columns: enhancedColumns, + schema: schemaLayout.schema.sch, + database: database.name + }, + draggable: true, + parentNode: schemaGroupId, + extent: 'parent' + }); + + // Move to next position + currentCol++; + if (currentCol >= schemaLayout.tablesPerRow) { + currentCol = 0; + currentRow++; + } + }); + } else { + // Schema has no tables - show empty state message + console.log(`Schema "${schemaLayout.schema.name || schemaLayout.schema.sch}" (${schemaLayout.schema.sch}) has no tables`); + } + + schemaXOffset += schemaLayout.width + 150; // Move to next schema position + }); + } + + // Create edges for relationships + relationships.forEach((rel, index) => { + const sourceTableId = `table-${rel.source_column_set[0]?.table_id || `${rel.source_column_set[0]?.schema_name}-${rel.source_column_set[0]?.table_name}`}`; + const targetTableId = `table-${rel.destination_column_set[0]?.table_id || `${rel.destination_column_set[0]?.schema_name}-${rel.destination_column_set[0]?.table_name}`}`; + + // Check if both nodes exist + const sourceExists = newNodes.some(node => node.id === sourceTableId); + const targetExists = newNodes.some(node => node.id === targetTableId); + + if (sourceExists && targetExists) { + newEdges.push({ + id: `relationship-${index}`, + type: 'erRelationship', + source: sourceTableId, + target: targetTableId, + data: { + relationship_type: rel.relationship_type, + source_column: rel.source_column_set[0]?.column_name, + target_column: rel.destination_column_set[0]?.column_name + }, + style: { + stroke: '#8a2be2', + strokeWidth: 2 + }, + animated: false + }); + } + }); + + setNodes(newNodes); + setEdges(newEdges); + + // Update available schemas and existing tables for the modal + if (database && database.schemas) { + setAvailableSchemas(database.schemas); + + // Collect all existing tables from all schemas + const allTables = []; + database.schemas.forEach(schema => { + if (schema.tables) { + schema.tables.forEach(table => { + allTables.push({ + ...table, + schema: schema.sch, + schemaName: schema.name || schema.sch + }); + }); + } + }); + setExistingTables(allTables); + } + + // Auto-fit view after a short delay to ensure nodes are rendered + setTimeout(() => { + if (newNodes.length > 0) { + fitView({ + padding: 0.1, + includeHiddenNodes: false, + minZoom: 0.15, + maxZoom: 0.8, + duration: 800 // Smooth animation + }); + } + }, 300); + }; + + const onConnect = useCallback( + (params) => setEdges((eds) => addEdge({ + ...params, + type: 'erRelationship', + data: { + relationship_type: '1:N', + source_column: 'id', + target_column: 'foreign_key_id' + }, + style: { + stroke: '#8a2be2', + strokeWidth: 2 + }, + animated: true + }, eds)), + [setEdges] + ); + + const handleAddClick = () => { + setShowAddMenu(!showAddMenu); + }; + + const handleDataSourceSelect = (dataSource) => { + setSelectedDataSource(dataSource); + setDataSourceMenuAnchor(null); + }; + + const handleCloseMenus = () => { + setDataSourceMenuAnchor(null); + }; + + if (isLoading) { + return ( +
+ {/* Breadcrumb Header */} +
+ +
+ + {/* Loading Content */} +
+
+ +

Loading ER Diagram...

+

Fetching schema for {selectedService?.name} - {selectedDataSource?.name}

+
+
+
+ ); + } + + return ( +
+