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 (
+
+ );
+};
+
+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 */}
+
+
+ {/* Data Sources Menu */}
+ {selectedService && (
+
+ )}
+ >
+ );
+};
+
+// 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 (
+
+
+
+ {/* Breadcrumb Header */}
+
+
+
+
+ {/* ReactFlow Container */}
+
+
+
+ {
+ if (node.type === 'erTable') {
+ switch(node.data.table_type) {
+ case 'stage': return '#00a99d';
+ case 'fact': return '#fa8c16';
+ case 'dimension': return '#52c41a';
+ default: return '#8a2be2';
+ }
+ }
+ return '#666';
+ }}
+ />
+
+
+
+
+
+
+ Entity Relationship Diagram
+
+
+
+ Tables: {nodes.filter(n => n.type === 'erTable').length} •
+ Relationships: {edges.length} •
+ Schemas: {nodes.filter(n => n.type === 'erSchemaGroup').length}
+
+ {isUsingMockData && (
+
+
+ Using mock data - {apiError}
+
+ )}
+
+
+
+
+ {/* Relationship Legend Panel */}
+
+
+
+
+ Relationship Types
+
+
+
+ →
+ 1:N - One to Many
+
+
+ →
+ 1:1 - One to One
+
+
+ ↔
+ N:M - Many to Many
+
+
+ • Arrows show relationship direction
+ • Numbers show cardinality
+ • Click edges to select them
+
+
+
+
+
+
+
+ {/* Data Sources Menu */}
+ {selectedService && (
+
+ )}
+
+ {/* Add Table Button */}
+
+
+ {/* Add Table Modal */}
+
setIsAddTableModalOpen(false)}
+ onAddTable={handleAddTable}
+ schemas={availableSchemas}
+ existingTables={existingTables}
+ />
+
+
+ );
+};
+
+// Wrapper component with ReactFlowProvider
+const ERDiagramCanvas = () => {
+ return (
+
+
+
+ );
+};
+
+export default ERDiagramCanvas;
\ No newline at end of file
diff --git a/src/components/InfiniteCanvas.jsx b/src/components/InfiniteCanvas.jsx
index 72e47e3..e106d89 100644
--- a/src/components/InfiniteCanvas.jsx
+++ b/src/components/InfiniteCanvas.jsx
@@ -18,9 +18,10 @@ import ReactFlow, {
} from 'reactflow';
import 'reactflow/dist/style.css';
import axios from 'axios';
+import planPlusLogo from '../assets/img/planPlusLogo.png';
// Import icons from react-icons
-import { FaDatabase, FaTable, FaFlask, FaArrowRight, FaPlus, FaTimes } from 'react-icons/fa';
+import { FaDatabase, FaTable, FaFlask, FaArrowRight, FaPlus, FaTimes, FaChevronUp, FaChevronDown } from 'react-icons/fa';
import { BiSolidData } from 'react-icons/bi';
import { AiFillFolder } from 'react-icons/ai';
import { BsFileEarmarkSpreadsheet } from 'react-icons/bs';
@@ -944,7 +945,197 @@ const CustomEdge = ({ id, source, target, sourceX, sourceY, targetX, targetY, so
);
};
+// Create a unique ID for edges that should be animated
+const SERVICE_EDGE_CLASS = 'service-db-connection';
+
+// Add the CSS for the animated connections directly to the stylesheet
+// This avoids potential issues with React's rendering cycle
+const style = document.createElement('style');
+style.textContent = `
+ @keyframes dashdraw {
+ from {
+ stroke-dashoffset: 10;
+ }
+ to {
+ stroke-dashoffset: 0;
+ }
+ }
+ .${SERVICE_EDGE_CLASS} .react-flow__edge-path {
+ animation: dashdraw 0.5s linear infinite;
+ }
+`;
+// Add the style element only once
+if (!document.getElementById('react-flow-animation')) {
+ style.id = 'react-flow-animation';
+ document.head.appendChild(style);
+}
+
+// Hierarchical Edge for connecting service to databases
+const HierarchicalEdge = ({ id, source, target, sourceX, sourceY, targetX, targetY, style = {} }) => {
+ // Calculate the path points for a stepped edge with orthogonal lines
+ const [path] = getSmoothStepPath({
+ sourceX,
+ sourceY,
+ sourcePosition: Position.Bottom, // Always start from bottom
+ targetX,
+ targetY,
+ targetPosition: Position.Top, // Always connect to top
+ borderRadius: 20, // Smoother corners for longer connections
+ stepSize: 30, // Increased step size for longer connections
+ });
+
+ // Enhanced style for hierarchical connections with animated dotted line
+ const hierarchyStyle = {
+ strokeWidth: 2,
+ stroke: '#00a99d',
+ strokeDasharray: '5, 5', // Create dotted/dashed line effect
+ ...style,
+ };
+
+ return (
+
+
+
+ );
+};
+
// Custom node types
+const ServiceNode = ({ data = {} }) => {
+ // Safety check for undefined data
+ if (!data) {
+ console.error('ServiceNode received undefined data');
+ data = {}; // Provide a default empty object
+ }
+
+ return (
+
+
+ {/* Logo and text container */}
+
+

+
+ Plan+
+
+
+
+ {/* Connected databases count */}
+
+ Connected Databases: {data.databases || 0}
+
+
+
+
+
+
+
+ {/* Handles for connections - explicit id for targeting */}
+
+
+ );
+};
+
const DatabaseNode = ({ data = {} }) => {
// Safety check for undefined data
if (!data) {
@@ -952,6 +1143,8 @@ const DatabaseNode = ({ data = {} }) => {
data = {}; // Provide a default empty object
}
+ // Enhanced styles for handles used in hierarchy connections
+
// Debug: Log the data being received by the component
// console.log('DatabaseNode data:', data);
@@ -1878,6 +2071,7 @@ const InfiniteCanvas = () => {
// Define node types (memoized to prevent unnecessary re-renders)
const nodeTypes = useMemo(() => ({
+ service: ServiceNode,
database: DatabaseNode,
schema: SchemaNode,
table: TableNode,
@@ -1886,6 +2080,7 @@ const InfiniteCanvas = () => {
// Define edge types
const edgeTypes = useMemo(() => ({
custom: CustomEdge,
+ hierarchical: HierarchicalEdge,
}), []);
// Function to handle redirection to DataFlow view
@@ -1968,8 +2163,30 @@ const InfiniteCanvas = () => {
// No alert needed for MVP - the view will change automatically
};
- // Initialize with database nodes from state instead of mockData
- const initialNodes = databases
+ // Create a service node for "Plan Plus"
+ const serviceNode = {
+ id: 'service-plan-plus',
+ type: 'service',
+ data: {
+ id: 'service-plan-plus',
+ // label: 'Qubit Service: Plan+',
+ databases: databases.length,
+ expanded: true,
+ onToggle: (id) => {
+ // Toggle expansion of the service node
+ setNodes(nodes => nodes.map(node => {
+ if (node.id === id) {
+ return { ...node, data: { ...node.data, expanded: !node.data.expanded } };
+ }
+ return node;
+ }));
+ }
+ },
+ position: { x: 400, y: 70 } // Positioned even higher on the canvas for maximum spacing
+ };
+
+ // Initialize with database nodes as children of the service
+ const databaseNodes = databases
.filter(db => db && db.id) // Filter out any invalid database objects
.map((db, index) => {
// Create a safe ID for the database
@@ -1988,15 +2205,36 @@ const InfiniteCanvas = () => {
onToggle: (id) => toggleDatabaseExpansion(id),
onViewDetails: handleViewDataFlow // Add the function to handle redirection
},
- // Position databases in a grid layout for better visibility
+ // Position databases in a grid layout with significantly increased vertical spacing
position: {
x: 250 + (index % 2) * 400, // 2 columns
- y: 100 + Math.floor(index / 2) * 250 // rows based on index
+ y: 400 + Math.floor(index / 2) * 250 // further increased vertical spacing (400px) from service node
},
};
});
+
+ // Combine the service node with database nodes
+ const initialNodes = [
+ serviceNode,
+ ...databaseNodes
+ ];
- const initialEdges = [];
+ // Create edges from service node to each database with custom hierarchical connections
+ const initialEdges = databases
+ .filter(db => db && db.id)
+ .map((db) => ({
+ id: `edge-service-to-${db.id}`,
+ source: 'service-plan-plus',
+ target: db.id,
+ type: 'hierarchical', // Using our custom hierarchical edge
+ animated: true,
+ style: {
+ stroke: '#00a99d',
+ strokeWidth: 2,
+ strokeDasharray: '5, 5', // Create dotted/dashed line effect
+ },
+ // No need for handles as they're built into the edge type
+ }));
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
@@ -2005,7 +2243,30 @@ const InfiniteCanvas = () => {
useEffect(() => {
console.log('Databases updated, updating nodes:', databases);
- const updatedNodes = databases
+ // Create a parent service node for "Plan Plus"
+ const serviceNode = {
+ id: 'service-plan-plus',
+ type: 'service',
+ data: {
+ id: 'service-plan-plus',
+ // label: 'Qubit Service: Plan+',
+ databases: databases.length,
+ expanded: true,
+ onToggle: (id) => {
+ // Toggle expansion of the service node
+ setNodes(nodes => nodes.map(node => {
+ if (node.id === id) {
+ return { ...node, data: { ...node.data, expanded: !node.data.expanded } };
+ }
+ return node;
+ }));
+ }
+ },
+ position: { x: 400, y: 70 } // Positioned even higher on the canvas for maximum spacing
+ };
+
+ // Map database nodes as children of the service node
+ const databaseNodes = databases
.filter(db => db && db.id)
.map((db, index) => {
const existingNode = nodes.find(node => node.id === db.id);
@@ -2020,6 +2281,11 @@ const InfiniteCanvas = () => {
name: db.name, // Make sure name is set from the API response
schemas: db.schemas || 0,
tables: db.tables || 0,
+ },
+ // Update position with significantly increased vertical spacing
+ position: {
+ x: 250 + (index % 2) * 400, // 2 columns
+ y: 400 + Math.floor(index / 2) * 250 // further increased vertical spacing (400px) from service node
}
};
}
@@ -2038,16 +2304,41 @@ const InfiniteCanvas = () => {
onToggle: (id) => toggleDatabaseExpansion(id),
onViewDetails: handleViewDataFlow
},
+ // Position databases in a grid layout with significantly increased vertical spacing
position: {
x: 250 + (index % 2) * 400,
- y: 100 + Math.floor(index / 2) * 250
+ y: 400 + Math.floor(index / 2) * 250 // further increased vertical spacing (400px) from service node
},
};
});
+ // Create edges from service node to each database with custom hierarchical connections
+ const serviceEdges = databases
+ .filter(db => db && db.id)
+ .map((db) => ({
+ id: `edge-service-to-${db.id}`,
+ source: 'service-plan-plus',
+ target: db.id,
+ type: 'hierarchical', // Using our custom hierarchical edge
+ animated: true,
+ style: {
+ stroke: '#00a99d',
+ strokeWidth: 2,
+ strokeDasharray: '5, 5', // Create dotted/dashed line effect
+ },
+ // No need for handles as they're built into the edge type
+ }));
+
+ // Combine service node with database nodes
+ const updatedNodes = [
+ serviceNode,
+ ...databaseNodes
+ ];
+
// Only update if we have nodes to show
if (updatedNodes.length > 0) {
setNodes(updatedNodes);
+ setEdges(serviceEdges);
}
}, [databases]);