Compare commits

...

15 Commits

Author SHA1 Message Date
johnnyfish
ba46643cd0 feat(import): optimize large diagrams with filtering and smart reordering 2025-08-13 12:17:48 +03:00
Guy Ben-Aharon
4520f8b1f7 update index.html (#850) 2025-08-13 11:34:08 +03:00
Guy Ben-Aharon
712bdf5b95 fix: filter to default schema on load new diagram (#849) 2025-08-12 18:07:19 +03:00
Jonathan Fishner
d7c9536272 fix: reorder with areas (#846) 2025-08-12 16:25:56 +03:00
Guy Ben-Aharon
815a52f192 update index.html (#848) 2025-08-12 14:31:41 +03:00
Guy Ben-Aharon
f1a4298362 fix: remove unnecessary space (#845) 2025-08-12 11:08:37 +03:00
Guy Ben-Aharon
b8f2141bd2 fix(sidebar): add titles to sidebar (#844)
* update shadcn

* menu v1

* menu v2

* resize menu items

* fix

* fix
2025-08-11 17:38:40 +03:00
Guy Ben-Aharon
eaebe34768 fix(menu): clear file menu (#843) 2025-08-11 11:46:33 +03:00
Jonathan Fishner
0d623a86b1 feat(postgres): add support hash index types (#812)
* feat(postgres): add support for hash index type with single column constraint

* some fixes

* some fixes

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-08-10 20:24:34 +03:00
Guy Ben-Aharon
19fd94c6bd fix(area filter): fix dragging tables over filtered areas (#842) 2025-08-10 15:26:47 +03:00
Guy Ben-Aharon
0da3caeeac fix(table colors): switch to default table color (#841) 2025-08-10 14:42:22 +03:00
Guy Ben-Aharon
cb2ba66233 fix(select-box): fix select box issue in dialog (#840) 2025-08-10 14:06:09 +03:00
Guy Ben-Aharon
8a2267281b alignment of node converters (#839) 2025-08-10 13:36:55 +03:00
Guy Ben-Aharon
41ba251377 fix: update filter on adding table (#838) 2025-08-10 11:04:52 +03:00
Jonathan Fishner
e9c5442d9d feat(filter): filter tables by areas (#836)
* feat: auto-hide/show areas based on table visibility in canvas filter

* fix build

* fix

* fix

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-08-10 10:13:28 +03:00
64 changed files with 2412 additions and 862 deletions

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="max-image-preview:large" />
<meta name="robots" content="noindex, max-image-preview:large" />
<title>ChartDB - Create & Visualize Database Schema Diagrams</title>
<link rel="canonical" href="https://chartdb.io" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
@@ -16,14 +16,19 @@
<script src="/config.js"></script>
<script>
// Load analytics only if not disabled
(function() {
const disableAnalytics = (window.env && window.env.DISABLE_ANALYTICS === 'true') ||
(typeof process !== 'undefined' && process.env && process.env.VITE_DISABLE_ANALYTICS === 'true');
(function () {
const disableAnalytics =
(window.env && window.env.DISABLE_ANALYTICS === 'true') ||
(typeof process !== 'undefined' &&
process.env &&
process.env.VITE_DISABLE_ANALYTICS === 'true');
if (!disableAnalytics) {
const script = document.createElement('script');
script.src = 'https://cdn.usefathom.com/script.js';
script.setAttribute('data-site', 'PRHIVBNN');
script.setAttribute('data-canonical', 'false');
script.setAttribute('data-spa', 'auto');
script.defer = true;
document.head.appendChild(script);
}

588
package-lock.json generated
View File

@@ -18,22 +18,22 @@
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-context-menu": "^2.2.1",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-menubar": "^1.1.1",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-scroll-area": "1.2.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.8",
"@radix-ui/react-tooltip": "^1.2.7",
"@uidotdev/usehooks": "^2.4.1",
"@xyflow/react": "^12.8.2",
"ahooks": "^3.8.1",
@@ -2121,23 +2121,23 @@
}
},
"node_modules/@radix-ui/react-dialog": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz",
"integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==",
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz",
"integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.5",
"@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.2",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-portal": "1.1.4",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-slot": "1.1.2",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.10",
"@radix-ui/react-focus-guards": "1.1.2",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
@@ -2156,17 +2156,53 @@
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
"integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
"integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-escape-keydown": "1.1.0"
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-escape-keydown": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -2183,15 +2219,30 @@
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
"integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz",
"integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==",
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0"
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -2208,14 +2259,56 @@
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-layout-effect": "1.1.0"
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
"integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -2233,12 +2326,12 @@
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.2"
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -2255,13 +2348,29 @@
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -2273,6 +2382,39 @@
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
@@ -2941,12 +3083,12 @@
}
},
"node_modules/@radix-ui/react-separator": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz",
"integrity": "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==",
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
"integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2"
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -2964,12 +3106,12 @@
}
},
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.2"
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -2986,24 +3128,6 @@
}
}
},
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
@@ -3156,23 +3280,23 @@
}
},
"node_modules/@radix-ui/react-tooltip": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz",
"integrity": "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==",
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz",
"integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.5",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-popper": "1.2.2",
"@radix-ui/react-portal": "1.1.4",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-slot": "1.1.2",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-visually-hidden": "1.1.2"
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.10",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.7",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-visually-hidden": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -3189,13 +3313,19 @@
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": {
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/primitive": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz",
"integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2"
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -3212,17 +3342,47 @@
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
"integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
"integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-escape-keydown": "1.1.0"
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-escape-keydown": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -3239,22 +3399,40 @@
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz",
"integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==",
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
"integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0",
"@radix-ui/react-use-rect": "1.1.0",
"@radix-ui/react-use-size": "1.1.0",
"@radix-ui/rect": "1.1.0"
"@radix-ui/react-arrow": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.1",
"@radix-ui/react-use-rect": "1.1.1",
"@radix-ui/react-use-size": "1.1.1",
"@radix-ui/rect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -3272,13 +3450,37 @@
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-use-layout-effect": "1.1.0"
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
"integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -3296,12 +3498,12 @@
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.2"
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -3318,13 +3520,98 @@
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-rect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
"integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
"license": "MIT",
"dependencies": {
"@radix-ui/rect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-size": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -3337,12 +3624,12 @@
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz",
"integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==",
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2"
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -3359,6 +3646,12 @@
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/rect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT"
},
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
@@ -3392,6 +3685,39 @@
}
}
},
"node_modules/@radix-ui/react-use-effect-event": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",

View File

@@ -26,22 +26,22 @@
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-context-menu": "^2.2.1",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-menubar": "^1.1.1",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-scroll-area": "1.2.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.8",
"@radix-ui/react-tooltip": "^1.2.7",
"@uidotdev/usehooks": "^2.4.1",
"@xyflow/react": "^12.8.2",
"ahooks": "^3.8.1",

View File

@@ -1,4 +1,4 @@
User-agent: *
Allow: /
Disallow: /
Sitemap: https://app.chartdb.io/sitemap.xml

View File

@@ -1,7 +1,7 @@
import { cva } from 'class-variance-authority';
export const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {

View File

@@ -4,6 +4,7 @@ import { Cross2Icon } from '@radix-ui/react-icons';
import { cn } from '@/lib/utils';
import { ScrollArea } from '../scroll-area/scroll-area';
import { ChevronLeft } from 'lucide-react';
const Dialog = DialogPrimitive.Root;
@@ -32,28 +33,75 @@ const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
showClose?: boolean;
showBack?: boolean;
backButtonClassName?: string;
blurBackground?: boolean;
forceOverlay?: boolean;
onBackClick?: () => void;
}
>(({ className, children, showClose, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className
)}
{...props}
>
{children}
{showClose && (
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="size-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
));
>(
(
{
className,
children,
showClose,
showBack,
onBackClick,
backButtonClassName,
blurBackground,
forceOverlay,
...props
},
ref
) => (
<DialogPortal>
{forceOverlay ? (
<div
className={cn(
'fixed inset-0 z-50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
{
'bg-black/80': !blurBackground,
'bg-black/30 backdrop-blur-sm': blurBackground,
}
)}
data-state="open"
/>
) : null}
<DialogOverlay
className={cn({
'bg-black/30 backdrop-blur-sm': blurBackground,
})}
/>
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
className
)}
{...props}
>
{children}
{showBack && (
<button
onClick={() => onBackClick?.()}
className={cn(
'absolute left-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground',
backButtonClassName
)}
>
<ChevronLeft className="size-4" />
</button>
)}
{showClose && (
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="size-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
);
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({

View File

@@ -2,16 +2,13 @@ import React from 'react';
import { cn } from '@/lib/utils';
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className
)}
ref={ref}

View File

@@ -94,6 +94,10 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
setOpen?.(isOpen);
setIsOpen(isOpen);
if (isOpen) {
setSearchTerm('');
}
setTimeout(() => (document.body.style.pointerEvents = ''), 500);
},
[setOpen]

View File

@@ -29,6 +29,7 @@ const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = '16rem';
const SIDEBAR_WIDTH_MOBILE = '18rem';
const SIDEBAR_WIDTH_ICON = '3rem';
const SIDEBAR_WIDTH_ICON_EXTENDED = '4rem';
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
type SidebarContext = {
@@ -142,6 +143,8 @@ const SidebarProvider = React.forwardRef<
{
'--sidebar-width': SIDEBAR_WIDTH,
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
'--sidebar-width-icon-extended':
SIDEBAR_WIDTH_ICON_EXTENDED,
...style,
} as React.CSSProperties
}
@@ -166,7 +169,7 @@ const Sidebar = React.forwardRef<
React.ComponentProps<'div'> & {
side?: 'left' | 'right';
variant?: 'sidebar' | 'floating' | 'inset';
collapsible?: 'offcanvas' | 'icon' | 'none';
collapsible?: 'offcanvas' | 'icon' | 'icon-extended' | 'none';
}
>(
(
@@ -245,8 +248,8 @@ const Sidebar = React.forwardRef<
'group-data-[collapsible=offcanvas]:w-0',
'group-data-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset'
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]'
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))] group-data-[collapsible=icon-extended]:w-[calc(var(--sidebar-width-icon-extended)_+_theme(spacing.4))]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[collapsible=icon-extended]:w-[--sidebar-width-icon-extended]'
)}
/>
<div
@@ -257,8 +260,8 @@ const Sidebar = React.forwardRef<
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
// Adjust the padding for floating and inset variants.
variant === 'floating' || variant === 'inset'
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l',
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)] group-data-[collapsible=icon-extended]:w-[calc(var(--sidebar-width-icon-extended)_+_theme(spacing.4)_+2px)]'
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[collapsible=icon-extended]:w-[--sidebar-width-icon-extended] group-data-[side=left]:border-r group-data-[side=right]:border-l',
className
)}
{...props}
@@ -421,7 +424,7 @@ const SidebarContent = React.forwardRef<
ref={ref}
data-sidebar="content"
className={cn(
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden group-data-[collapsible=icon-extended]:overflow-hidden',
className
)}
{...props}
@@ -461,6 +464,7 @@ const SidebarGroupLabel = React.forwardRef<
className={cn(
'flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
'group-data-[collapsible=icon-extended]:-mt-8 group-data-[collapsible=icon-extended]:opacity-0',
className
)}
{...props}
@@ -483,7 +487,7 @@ const SidebarGroupAction = React.forwardRef<
'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
// Increases the hit area of the button on mobile.
'after:absolute after:-inset-2 after:md:hidden',
'group-data-[collapsible=icon]:hidden',
'group-data-[collapsible=icon]:hidden group-data-[collapsible=icon-extended]:hidden',
className
)}
{...props}
@@ -532,7 +536,7 @@ const SidebarMenuItem = React.forwardRef<
SidebarMenuItem.displayName = 'SidebarMenuItem';
const sidebarMenuButtonVariants = cva(
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon-extended]:h-auto group-data-[collapsible=icon-extended]:flex-col group-data-[collapsible=icon-extended]:gap-1 group-data-[collapsible=icon-extended]:p-2 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate group-data-[collapsible=icon-extended]:[&>span]:w-full group-data-[collapsible=icon-extended]:[&>span]:text-center group-data-[collapsible=icon-extended]:[&>span]:text-[10px] group-data-[collapsible=icon-extended]:[&>span]:leading-tight [&>svg]:size-4 [&>svg]:shrink-0',
{
variants: {
variant: {
@@ -636,7 +640,7 @@ const SidebarMenuAction = React.forwardRef<
'peer-data-[size=sm]/menu-button:top-1',
'peer-data-[size=default]/menu-button:top-1.5',
'peer-data-[size=lg]/menu-button:top-2.5',
'group-data-[collapsible=icon]:hidden',
'group-data-[collapsible=icon]:hidden group-data-[collapsible=icon-extended]:hidden',
showOnHover &&
'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0',
className
@@ -753,7 +757,7 @@ const SidebarMenuSubButton = React.forwardRef<
'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
size === 'sm' && 'text-xs',
size === 'md' && 'text-sm',
'group-data-[collapsible=icon]:hidden',
'group-data-[collapsible=icon]:hidden group-data-[collapsible=icon-extended]:hidden',
className
)}
{...props}

View File

@@ -13,15 +13,17 @@ const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
// <TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]',
className
)}
{...props}
/>
// </TooltipPrimitive.Portal>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;

View File

@@ -15,7 +15,7 @@ interface CanvasProviderProps {
}
export const CanvasProvider = ({ children }: CanvasProviderProps) => {
const { tables, relationships, updateTablesState, databaseType } =
const { tables, relationships, updateTablesState, databaseType, areas } =
useChartDB();
const { filter } = useDiagramFilter();
const { fitView } = useReactFlow();
@@ -44,6 +44,7 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
},
})
),
areas,
mode: 'all',
});
@@ -86,6 +87,7 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
updateTablesState,
fitView,
databaseType,
areas,
]
);

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useMemo, useState } from 'react';
import type { DBTable } from '@/lib/domain/db-table';
import { deepCopy, generateId } from '@/lib/utils';
import { randomColor } from '@/lib/colors';
import { defaultTableColor, defaultAreaColor } from '@/lib/colors';
import type { ChartDBContext, ChartDBEvent } from './chartdb-context';
import { chartDBContext } from './chartdb-context';
import { DatabaseType } from '@/lib/domain/database-type';
@@ -337,7 +337,7 @@ export const ChartDBProvider: React.FC<
},
],
indexes: [],
color: randomColor(),
color: defaultTableColor,
createdAt: Date.now(),
isView: false,
order: tables.length,
@@ -1412,7 +1412,7 @@ export const ChartDBProvider: React.FC<
y: 0,
width: 300,
height: 200,
color: randomColor(),
color: defaultAreaColor,
...attributes,
};

View File

@@ -1,48 +1,50 @@
import type { DBSchema } from '@/lib/domain';
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
import type {
DiagramFilter,
FilterTableInfo,
} from '@/lib/domain/diagram-filter/diagram-filter';
import { emptyFn } from '@/lib/utils';
import { createContext } from 'react';
export interface DiagramFilterContext {
filter?: DiagramFilter;
loading: boolean;
hasActiveFilter: boolean;
schemasDisplayed: DBSchema[];
// schemas
schemaIdsFilter?: string[];
addSchemaIdsFilter: (...ids: string[]) => void;
removeSchemaIdsFilter: (...ids: string[]) => void;
clearSchemaIdsFilter: () => void;
// tables
tableIdsFilter?: string[];
addTableIdsFilter: (...ids: string[]) => void;
removeTableIdsFilter: (...ids: string[]) => void;
clearTableIdsFilter: () => void;
setTableIdsFilterEmpty: () => void;
// reset
resetFilter: () => void;
// smart filters
toggleSchemaFilter: (schemaId: string) => void;
toggleTableFilter: (tableId: string) => void;
addSchemaIfFiltered: (schemaId: string) => void;
addSchemaToFilter: (schemaId: string) => void;
addTablesToFilter: (attrs: {
tableIds?: string[];
filterCallback?: (table: FilterTableInfo) => boolean;
}) => void;
removeTablesFromFilter: (attrs: {
tableIds?: string[];
filterCallback?: (table: FilterTableInfo) => boolean;
}) => void;
}
export const diagramFilterContext = createContext<DiagramFilterContext>({
hasActiveFilter: false,
addSchemaIdsFilter: emptyFn,
addTableIdsFilter: emptyFn,
clearSchemaIdsFilter: emptyFn,
clearTableIdsFilter: emptyFn,
setTableIdsFilterEmpty: emptyFn,
removeSchemaIdsFilter: emptyFn,
removeTableIdsFilter: emptyFn,
resetFilter: emptyFn,
toggleSchemaFilter: emptyFn,
toggleTableFilter: emptyFn,
addSchemaIfFiltered: emptyFn,
addSchemaToFilter: emptyFn,
schemasDisplayed: [],
addTablesToFilter: emptyFn,
removeTablesFromFilter: emptyFn,
loading: false,
});

View File

@@ -7,33 +7,45 @@ import React, {
} from 'react';
import type { DiagramFilterContext } from './diagram-filter-context';
import { diagramFilterContext } from './diagram-filter-context';
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
import { reduceFilter } from '@/lib/domain/diagram-filter/diagram-filter';
import type {
DiagramFilter,
FilterTableInfo,
} from '@/lib/domain/diagram-filter/diagram-filter';
import {
reduceFilter,
spreadFilterTables,
} from '@/lib/domain/diagram-filter/diagram-filter';
import { useStorage } from '@/hooks/use-storage';
import { useChartDB } from '@/hooks/use-chartdb';
import { filterSchema, filterTable } from '@/lib/domain/diagram-filter/filter';
import { schemaNameToSchemaId } from '@/lib/domain';
import { filterTable } from '@/lib/domain/diagram-filter/filter';
import { databasesWithSchemas, schemaNameToSchemaId } from '@/lib/domain';
import { defaultSchemas } from '@/lib/data/default-schemas';
import type { ChartDBEvent } from '../chartdb-context/chartdb-context';
export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const { diagramId, tables, schemas, databaseType } = useChartDB();
const { diagramId, tables, schemas, databaseType, events } = useChartDB();
const { getDiagramFilter, updateDiagramFilter } = useStorage();
const [filter, setFilter] = useState<DiagramFilter>({});
const [loading, setLoading] = useState<boolean>(true);
const allSchemasIds = useMemo(() => {
return schemas.map((schema) => schema.id);
}, [schemas]);
const allTables = useMemo(() => {
return tables.map((table) => ({
id: table.id,
schemaId: table.schema
? schemaNameToSchemaId(table.schema)
: defaultSchemas[databaseType],
schema: table.schema,
}));
const allTables: FilterTableInfo[] = useMemo(() => {
return tables.map(
(table) =>
({
id: table.id,
schemaId: table.schema
? schemaNameToSchemaId(table.schema)
: defaultSchemas[databaseType],
schema: table.schema ?? defaultSchemas[databaseType],
areaId: table.parentAreaId ?? undefined,
}) satisfies FilterTableInfo
);
}, [tables, databaseType]);
const diagramIdOfLoadedFilter = useRef<string | null>(null);
@@ -51,11 +63,26 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
return;
}
setLoading(true);
const loadFilterFromStorage = async (diagramId: string) => {
if (diagramId) {
const storedFilter = await getDiagramFilter(diagramId);
setFilter(storedFilter ?? {});
let filterToSet = storedFilter;
if (!filterToSet) {
// If no filter is stored, set default based on database type
filterToSet =
schemas.length > 0
? { schemaIds: [schemas[0].id] }
: {};
}
setFilter(filterToSet);
}
setLoading(false);
};
setFilter({});
@@ -64,34 +91,7 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
loadFilterFromStorage(diagramId);
diagramIdOfLoadedFilter.current = diagramId;
}
}, [diagramId, getDiagramFilter]);
// Schema methods
const addSchemaIds: DiagramFilterContext['addSchemaIdsFilter'] =
useCallback((...ids: string[]) => {
setFilter(
(prev) =>
({
...prev,
schemaIds: [
...new Set([...(prev.schemaIds || []), ...ids]),
],
}) satisfies DiagramFilter
);
}, []);
const removeSchemaIds: DiagramFilterContext['removeSchemaIdsFilter'] =
useCallback((...ids: string[]) => {
setFilter(
(prev) =>
({
...prev,
schemaIds: prev.schemaIds?.filter(
(id) => !ids.includes(id)
),
}) satisfies DiagramFilter
);
}, []);
}, [diagramId, getDiagramFilter, schemas]);
const clearSchemaIds: DiagramFilterContext['clearSchemaIdsFilter'] =
useCallback(() => {
@@ -104,35 +104,6 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
);
}, []);
// Table methods
const addTableIds: DiagramFilterContext['addTableIdsFilter'] = useCallback(
(...ids: string[]) => {
setFilter(
(prev) =>
({
...prev,
tableIds: [
...new Set([...(prev.tableIds || []), ...ids]),
],
}) satisfies DiagramFilter
);
},
[]
);
const removeTableIds: DiagramFilterContext['removeTableIdsFilter'] =
useCallback((...ids: string[]) => {
setFilter(
(prev) =>
({
...prev,
tableIds: prev.tableIds?.filter(
(id) => !ids.includes(id)
),
}) satisfies DiagramFilter
);
}, []);
const clearTableIds: DiagramFilterContext['clearTableIdsFilter'] =
useCallback(() => {
setFilter(
@@ -167,10 +138,20 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
const currentSchemaIds = prev.schemaIds;
// Check if schema is currently visible
const isSchemaVisible = filterSchema({
schemaId,
schemaIdsFilter: currentSchemaIds,
});
const isSchemaVisible = !allTables.some(
(table) =>
table.schemaId === schemaId &&
filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter: prev,
options: {
defaultSchema: defaultSchemas[databaseType],
},
}) === false
);
let newSchemaIds: string[] | undefined;
let newTableIds: string[] | undefined = prev.tableIds;
@@ -224,11 +205,11 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
schemaIds: newSchemaIds,
tableIds: newTableIds,
},
allTables
allTables satisfies FilterTableInfo[]
);
});
},
[allSchemasIds, allTables]
[allSchemasIds, allTables, databaseType]
);
const toggleTableFilterForNoSchema = useCallback(
@@ -271,7 +252,7 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
schemaIds: undefined,
tableIds: newTableIds,
},
allTables
allTables satisfies FilterTableInfo[]
);
});
},
@@ -281,7 +262,7 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
const toggleTableFilter: DiagramFilterContext['toggleTableFilter'] =
useCallback(
(tableId: string) => {
if (!defaultSchemas[databaseType]) {
if (!databasesWithSchemas.includes(databaseType)) {
// No schemas, toggle table filter without schema context
toggleTableFilterForNoSchema(tableId);
return;
@@ -359,14 +340,14 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
schemaIds: newSchemaIds,
tableIds: newTableIds,
},
allTables
allTables satisfies FilterTableInfo[]
);
});
},
[allTables, databaseType, toggleTableFilterForNoSchema]
);
const addSchemaIfFiltered: DiagramFilterContext['addSchemaIfFiltered'] =
const addSchemaToFilter: DiagramFilterContext['addSchemaToFilter'] =
useCallback(
(schemaId: string) => {
setFilter((prev) => {
@@ -406,32 +387,148 @@ export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
const schemasDisplayed: DiagramFilterContext['schemasDisplayed'] =
useMemo(() => {
if (!filter.schemaIds) {
if (!hasActiveFilter) {
return schemas;
}
const displayedSchemaIds = new Set<string>();
for (const table of allTables) {
if (
filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
})
) {
if (table.schemaId) {
displayedSchemaIds.add(table.schemaId);
}
}
}
return schemas.filter((schema) =>
filter.schemaIds?.includes(schema.id)
displayedSchemaIds.has(schema.id)
);
}, [filter.schemaIds, schemas]);
}, [hasActiveFilter, schemas, allTables, filter, databaseType]);
const addTablesToFilter: DiagramFilterContext['addTablesToFilter'] =
useCallback(
({ tableIds, filterCallback }) => {
setFilter((prev) => {
let tableIdsToAdd: string[];
if (tableIds) {
// If tableIds are provided, use them directly
tableIdsToAdd = tableIds;
} else if (filterCallback) {
// If filterCallback is provided, filter tables based on it
tableIdsToAdd = allTables
.filter(filterCallback)
.map((table) => table.id);
} else {
// If neither is provided, do nothing
return prev;
}
const filterByTableIds = spreadFilterTables(
prev,
allTables satisfies FilterTableInfo[]
);
const currentTableIds = filterByTableIds.tableIds || [];
const newTableIds = [
...new Set([...currentTableIds, ...tableIdsToAdd]),
];
return reduceFilter(
{
...filterByTableIds,
tableIds: newTableIds,
},
allTables satisfies FilterTableInfo[]
);
});
},
[allTables]
);
const removeTablesFromFilter: DiagramFilterContext['removeTablesFromFilter'] =
useCallback(
({ tableIds, filterCallback }) => {
setFilter((prev) => {
let tableIdsToRemovoe: string[];
if (tableIds) {
// If tableIds are provided, use them directly
tableIdsToRemovoe = tableIds;
} else if (filterCallback) {
// If filterCallback is provided, filter tables based on it
tableIdsToRemovoe = allTables
.filter(filterCallback)
.map((table) => table.id);
} else {
// If neither is provided, do nothing
return prev;
}
const filterByTableIds = spreadFilterTables(
prev,
allTables satisfies FilterTableInfo[]
);
const currentTableIds = filterByTableIds.tableIds || [];
const newTableIds = currentTableIds.filter(
(id) => !tableIdsToRemovoe.includes(id)
);
return reduceFilter(
{
...filterByTableIds,
tableIds: newTableIds,
},
allTables satisfies FilterTableInfo[]
);
});
},
[allTables]
);
const eventConsumer = useCallback(
(event: ChartDBEvent) => {
if (!hasActiveFilter) {
return;
}
if (event.action === 'add_tables') {
addTablesToFilter({
tableIds: event.data.tables.map((table) => table.id),
});
}
},
[hasActiveFilter, addTablesToFilter]
);
events.useSubscription(eventConsumer);
const value: DiagramFilterContext = {
loading,
filter,
schemaIdsFilter: filter.schemaIds,
addSchemaIdsFilter: addSchemaIds,
removeSchemaIdsFilter: removeSchemaIds,
clearSchemaIdsFilter: clearSchemaIds,
setTableIdsFilterEmpty: setTableIdsEmpty,
tableIdsFilter: filter.tableIds,
addTableIdsFilter: addTableIds,
removeTableIdsFilter: removeTableIds,
clearTableIdsFilter: clearTableIds,
resetFilter,
toggleSchemaFilter,
toggleTableFilter,
addSchemaIfFiltered,
addSchemaToFilter,
hasActiveFilter,
schemasDisplayed,
addTablesToFilter,
removeTablesFromFilter,
};
return (

View File

@@ -245,7 +245,9 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
const getDiagramFilter: StorageContext['getDiagramFilter'] = useCallback(
async (diagramId: string): Promise<DiagramFilter | undefined> => {
return await db.diagram_filters.get({ diagramId });
const filter = await db.diagram_filters.get({ diagramId });
return filter;
},
[db]
);

View File

@@ -19,6 +19,7 @@ import { SelectTables } from '../common/select-tables/select-tables';
import { useTranslation } from 'react-i18next';
import type { BaseDialogProps } from '../common/base-dialog-props';
import { sqlImportToDiagram } from '@/lib/data/sql-import';
import { getInitialFilterForLargeDiagram } from '@/lib/export-import-utils';
import type { SelectedTable } from '@/lib/data/import-metadata/filter-metadata';
import { filterMetadataByTables } from '@/lib/data/import-metadata/filter-metadata';
import { MAX_TABLES_WITHOUT_SHOWING_FILTER } from '../common/select-tables/constants';
@@ -43,7 +44,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
const [step, setStep] = useState<CreateDiagramDialogStep>(
CreateDiagramDialogStep.SELECT_DATABASE
);
const { listDiagrams, addDiagram } = useStorage();
const { listDiagrams, addDiagram, updateDiagramFilter } = useStorage();
const [diagramNumber, setDiagramNumber] = useState<number>(1);
const navigate = useNavigate();
const [parsedMetadata, setParsedMetadata] = useState<DatabaseMetadata>();
@@ -89,6 +90,12 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
sourceDatabaseType: databaseType,
targetDatabaseType: databaseType,
});
// Check if we need a filter for large SQL imports
const initialFilter = getInitialFilterForLargeDiagram(diagram);
if (initialFilter) {
await updateDiagramFilter(diagram.id, initialFilter);
}
} else {
let metadata: DatabaseMetadata | undefined = databaseMetadata;
@@ -103,7 +110,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
});
}
diagram = await loadFromDatabaseMetadata({
const result = await loadFromDatabaseMetadata({
databaseType,
databaseMetadata: metadata,
diagramNumber,
@@ -112,6 +119,12 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
? undefined
: databaseEdition,
});
diagram = result.diagram;
// Apply filter if needed for large diagrams
if (result.initialFilter) {
await updateDiagramFilter(diagram.id, result.initialFilter);
}
}
await addDiagram({ diagram });
@@ -126,6 +139,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
importMethod,
databaseType,
addDiagram,
updateDiagramFilter,
databaseEdition,
closeCreateDiagramDialog,
navigate,

View File

@@ -218,8 +218,14 @@ export const CreateRelationshipDialog: React.FC<
closeCreateRelationshipDialog();
}
}}
modal={false}
>
<DialogContent className="flex flex-col overflow-y-auto" showClose>
<DialogContent
className="flex flex-col overflow-y-auto"
showClose
forceOverlay
onInteractOutside={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle>
{t('create_relationship_dialog.title')}

View File

@@ -69,7 +69,7 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
const databaseMetadata: DatabaseMetadata =
loadDatabaseMetadata(scriptResult);
diagram = await loadFromDatabaseMetadata({
const result = await loadFromDatabaseMetadata({
databaseType,
databaseMetadata,
databaseEdition:
@@ -77,6 +77,9 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
? undefined
: databaseEdition,
});
diagram = result.diagram;
// Note: For importing into existing diagram, we don't apply the filter
// as it would affect the existing tables too
}
const tableIdsToRemove = tables

View File

@@ -16,7 +16,10 @@ import { useTranslation } from 'react-i18next';
import { FileUploader } from '@/components/file-uploader/file-uploader';
import { useStorage } from '@/hooks/use-storage';
import { useNavigate } from 'react-router-dom';
import { diagramFromJSONInput } from '@/lib/export-import-utils';
import {
diagramFromJSONInput,
getInitialFilterForLargeDiagram,
} from '@/lib/export-import-utils';
import { Alert, AlertDescription, AlertTitle } from '@/components/alert/alert';
import { AlertCircle } from 'lucide-react';
@@ -27,7 +30,7 @@ export const ImportDiagramDialog: React.FC<ImportDiagramDialogProps> = ({
}) => {
const { t } = useTranslation();
const [file, setFile] = useState<File | null>(null);
const { addDiagram } = useStorage();
const { addDiagram, updateDiagramFilter } = useStorage();
const navigate = useNavigate();
const [error, setError] = useState(false);
@@ -58,8 +61,16 @@ export const ImportDiagramDialog: React.FC<ImportDiagramDialogProps> = ({
try {
const diagram = diagramFromJSONInput(json);
// Check if we need to apply a filter for large diagrams
const initialFilter = getInitialFilterForLargeDiagram(diagram);
await addDiagram({ diagram });
// Apply the filter if needed (to hide isolated tables)
if (initialFilter) {
await updateDiagramFilter(diagram.id, initialFilter);
}
closeImportDiagramDialog();
closeCreateDiagramDialog();
@@ -74,6 +85,7 @@ export const ImportDiagramDialog: React.FC<ImportDiagramDialogProps> = ({
}, [
file,
addDiagram,
updateDiagramFilter,
navigate,
closeImportDiagramDialog,
closeCreateDiagramDialog,

View File

@@ -28,7 +28,6 @@ import {
import { useChartDB } from '@/hooks/use-chartdb';
import { defaultSchemas } from '@/lib/data/default-schemas';
import { Label } from '@/components/label/label';
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
export interface TableSchemaDialogProps extends BaseDialogProps {
table?: DBTable;
@@ -46,7 +45,6 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
}) => {
const { t } = useTranslation();
const { databaseType } = useChartDB();
const { addSchemaIfFiltered } = useDiagramFilter();
const [selectedSchemaId, setSelectedSchemaId] = useState<string>(
table?.schema
? schemaNameToSchemaId(table.schema)
@@ -95,7 +93,6 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
const { closeTableSchemaDialog } = useDialog();
const handleConfirm = useCallback(() => {
let createdSchemaId: string;
if (isCreatingNew && newSchemaName.trim()) {
const newSchema: DBSchema = {
id: schemaNameToSchemaId(newSchemaName.trim()),
@@ -103,26 +100,14 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
tableCount: 0,
};
createdSchemaId = newSchema.id;
onConfirm({ schema: newSchema });
} else {
const schema = schemas.find((s) => s.id === selectedSchemaId);
if (!schema) return;
createdSchemaId = schema.id;
onConfirm({ schema });
}
addSchemaIfFiltered(createdSchemaId);
}, [
onConfirm,
selectedSchemaId,
schemas,
isCreatingNew,
newSchemaName,
addSchemaIfFiltered,
]);
}, [onConfirm, selectedSchemaId, schemas, isCreatingNew, newSchemaName]);
const schemaOptions: SelectBoxOption[] = useMemo(
() =>

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ar: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'جديد',
browse: 'تصفح',
tables: 'الجداول',
relationships: 'الروابط',
areas: 'المناطق',
dependencies: 'التبعيات',
custom_types: 'الأنواع المخصصة',
},
menu: {
file: {
file: 'ملف',
new: 'جديد',
open: 'فتح',
databases: {
databases: 'قواعد البيانات',
new: 'مخطط جديد',
browse: 'تصفح...',
save: 'حفظ',
import: 'استيراد قاعدة بيانات',
export_sql: 'SQL تصدير',
export_as: 'تصدير كـ',
delete_diagram: 'حذف الرسم البياني',
exit: 'خروج',
},
edit: {
edit: 'تحرير',
@@ -149,6 +157,7 @@ export const ar: LanguageTranslation = {
title: 'خصائص الفهرس',
name: 'الإسم',
unique: 'فريد',
index_type: 'نوع الفهرس',
delete_index: 'حذف الفهرس',
},
table_actions: {

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const bn: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'নতুন',
browse: 'ব্রাউজ',
tables: 'টেবিল',
relationships: 'সম্পর্ক',
areas: 'এলাকা',
dependencies: 'নির্ভরতা',
custom_types: 'কাস্টম টাইপ',
},
menu: {
file: {
file: 'ফাইল',
new: 'নতুন',
open: 'খুলুন',
databases: {
databases: 'ডাটাবেস',
new: 'নতুন ডায়াগ্রাম',
browse: 'ব্রাউজ করুন...',
save: 'সংরক্ষণ করুন',
import: 'ডাটাবেস আমদানি করুন',
export_sql: 'SQL রপ্তানি করুন',
export_as: 'রূপে রপ্তানি করুন',
delete_diagram: 'ডায়াগ্রাম মুছুন',
exit: 'প্রস্থান করুন',
},
edit: {
edit: 'সম্পাদনা',
@@ -151,6 +159,7 @@ export const bn: LanguageTranslation = {
title: 'ইনডেক্স কর্ম',
name: 'নাম',
unique: 'অদ্বিতীয়',
index_type: 'ইনডেক্স ধরন',
delete_index: 'ইনডেক্স মুছুন',
},
table_actions: {

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const de: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Neu',
browse: 'Durchsuchen',
tables: 'Tabellen',
relationships: 'Beziehungen',
areas: 'Bereiche',
dependencies: 'Abhängigkeiten',
custom_types: 'Benutzerdefinierte Typen',
},
menu: {
file: {
file: 'Datei',
new: 'Neu',
open: 'Öffnen',
databases: {
databases: 'Datenbanken',
new: 'Neues Diagramm',
browse: 'Durchsuchen...',
save: 'Speichern',
import: 'Datenbank importieren',
export_sql: 'SQL exportieren',
export_as: 'Exportieren als',
delete_diagram: 'Diagramm löschen',
exit: 'Beenden',
},
edit: {
edit: 'Bearbeiten',
@@ -152,6 +160,7 @@ export const de: LanguageTranslation = {
title: 'Indexattribute',
name: 'Name',
unique: 'Eindeutig',
index_type: 'Indextyp',
delete_index: 'Index löschen',
},
table_actions: {

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata } from '../types';
export const en = {
translation: {
editor_sidebar: {
new_diagram: 'New',
browse: 'Browse',
tables: 'Tables',
relationships: 'Refs',
areas: 'Areas',
dependencies: 'Dependencies',
custom_types: 'Custom Types',
},
menu: {
file: {
file: 'File',
new: 'New',
open: 'Open',
databases: {
databases: 'Databases',
new: 'New Diagram',
browse: 'Browse...',
save: 'Save',
import: 'Import',
export_sql: 'Export SQL',
export_as: 'Export as',
delete_diagram: 'Delete Diagram',
exit: 'Exit',
},
edit: {
edit: 'Edit',
@@ -145,6 +153,7 @@ export const en = {
title: 'Index Attributes',
name: 'Name',
unique: 'Unique',
index_type: 'Index Type',
delete_index: 'Delete Index',
},
table_actions: {

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const es: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Nuevo',
browse: 'Examinar',
tables: 'Tablas',
relationships: 'Relaciones',
areas: 'Áreas',
dependencies: 'Dependencias',
custom_types: 'Tipos Personalizados',
},
menu: {
file: {
file: 'Archivo',
new: 'Nuevo',
open: 'Abrir',
databases: {
databases: 'Bases de Datos',
new: 'Nuevo Diagrama',
browse: 'Examinar...',
save: 'Guardar',
import: 'Importar Base de Datos',
export_sql: 'Exportar SQL',
export_as: 'Exportar como',
delete_diagram: 'Eliminar Diagrama',
exit: 'Salir',
},
edit: {
edit: 'Editar',
@@ -150,6 +158,7 @@ export const es: LanguageTranslation = {
title: 'Atributos del Índice',
name: 'Nombre',
unique: 'Único',
index_type: 'Tipo de Índice',
delete_index: 'Eliminar Índice',
},
table_actions: {

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const fr: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Nouveau',
browse: 'Parcourir',
tables: 'Tables',
relationships: 'Relations',
areas: 'Zones',
dependencies: 'Dépendances',
custom_types: 'Types Personnalisés',
},
menu: {
file: {
file: 'Fichier',
new: 'Nouveau',
open: 'Ouvrir',
databases: {
databases: 'Bases de Données',
new: 'Nouveau Diagramme',
browse: 'Parcourir...',
save: 'Enregistrer',
import: 'Importer Base de Données',
export_sql: 'Exporter SQL',
export_as: 'Exporter en tant que',
delete_diagram: 'Supprimer le Diagramme',
exit: 'Quitter',
},
edit: {
edit: 'Édition',
@@ -148,6 +156,7 @@ export const fr: LanguageTranslation = {
title: "Attributs de l'Index",
name: 'Nom',
unique: 'Unique',
index_type: "Type d'index",
delete_index: "Supprimer l'Index",
},
table_actions: {

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const gu: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'નવું',
browse: 'બ્રાઉજ',
tables: 'ટેબલો',
relationships: 'સંબંધો',
areas: 'ક્ષેત્રો',
dependencies: 'નિર્ભરતાઓ',
custom_types: 'કસ્ટમ ટાઇપ',
},
menu: {
file: {
file: 'ફાઇલ',
new: 'નવું',
open: 'ખોલો',
databases: {
databases: 'ડેટાબેસેસ',
new: 'નવું ડાયાગ્રામ',
browse: 'બ્રાઉજ કરો...',
save: 'સાચવો',
import: 'ડેટાબેસ આયાત કરો',
export_sql: 'SQL નિકાસ કરો',
export_as: 'રૂપે નિકાસ કરો',
delete_diagram: 'ડાયાગ્રામ કાઢી નાખો',
exit: 'બહાર જાઓ',
},
edit: {
edit: 'ફેરફાર',
@@ -152,6 +160,7 @@ export const gu: LanguageTranslation = {
title: 'ઇન્ડેક્સ લક્ષણો',
name: 'નામ',
unique: 'અદ્વિતીય',
index_type: 'ઇન્ડેક્સ પ્રકાર',
delete_index: 'ઇન્ડેક્સ કાઢી નાખો',
},
table_actions: {

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const hi: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'नया',
browse: 'ब्राउज़',
tables: 'टेबल',
relationships: 'संबंध',
areas: 'क्षेत्र',
dependencies: 'निर्भरताएं',
custom_types: 'कस्टम टाइप',
},
menu: {
file: {
file: 'फ़ाइल',
new: 'नया',
open: 'खोलें',
databases: {
databases: 'डेटाबेस',
new: 'नया आरेख',
browse: 'ब्राउज़ करें...',
save: 'सहेजें',
import: 'डेटाबेस आयात करें',
export_sql: 'SQL निर्यात करें',
export_as: 'के रूप में निर्यात करें',
delete_diagram: 'आरेख हटाएँ',
exit: 'बाहर जाएँ',
},
edit: {
edit: 'संपादित करें',
@@ -151,6 +159,7 @@ export const hi: LanguageTranslation = {
title: 'सूचकांक विशेषताएँ',
name: 'नाम',
unique: 'अद्वितीय',
index_type: 'इंडेक्स प्रकार',
delete_index: 'सूचकांक हटाएँ',
},
table_actions: {

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const hr: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Novi',
browse: 'Pregledaj',
tables: 'Tablice',
relationships: 'Veze',
areas: 'Područja',
dependencies: 'Ovisnosti',
custom_types: 'Prilagođeni Tipovi',
},
menu: {
file: {
file: 'Datoteka',
new: 'Nova',
open: 'Otvori',
databases: {
databases: 'Baze Podataka',
new: 'Novi Dijagram',
browse: 'Pregledaj...',
save: 'Spremi',
import: 'Uvezi',
export_sql: 'Izvezi SQL',
export_as: 'Izvezi kao',
delete_diagram: 'Izbriši dijagram',
exit: 'Izađi',
},
edit: {
edit: 'Uredi',
@@ -146,6 +154,7 @@ export const hr: LanguageTranslation = {
title: 'Atributi indeksa',
name: 'Naziv',
unique: 'Jedinstven',
index_type: 'Vrsta indeksa',
delete_index: 'Izbriši indeks',
},
table_actions: {

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const id_ID: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Baru',
browse: 'Jelajahi',
tables: 'Tabel',
relationships: 'Relasi',
areas: 'Area',
dependencies: 'Ketergantungan',
custom_types: 'Tipe Kustom',
},
menu: {
file: {
file: 'Berkas',
new: 'Buat Baru',
open: 'Buka',
databases: {
databases: 'Basis Data',
new: 'Diagram Baru',
browse: 'Jelajahi...',
save: 'Simpan',
import: 'Impor Database',
export_sql: 'Ekspor SQL',
export_as: 'Ekspor Sebagai',
delete_diagram: 'Hapus Diagram',
exit: 'Keluar',
},
edit: {
edit: 'Ubah',
@@ -150,6 +158,7 @@ export const id_ID: LanguageTranslation = {
title: 'Atribut Indeks',
name: 'Nama',
unique: 'Unik',
index_type: 'Tipe Indeks',
delete_index: 'Hapus Indeks',
},
table_actions: {

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ja: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: '新規',
browse: '参照',
tables: 'テーブル',
relationships: 'リレーション',
areas: 'エリア',
dependencies: '依存関係',
custom_types: 'カスタムタイプ',
},
menu: {
file: {
file: 'ファイル',
new: '新',
open: '開く',
databases: {
databases: 'データベース',
new: '新しいダイアグラム',
browse: '参照...',
save: '保存',
import: 'データベースをインポート',
export_sql: 'SQLをエクスポート',
export_as: '形式を指定してエクスポート',
delete_diagram: 'ダイアグラムを削除',
exit: '終了',
},
edit: {
edit: '編集',
@@ -154,6 +162,7 @@ export const ja: LanguageTranslation = {
title: 'インデックス属性',
name: '名前',
unique: 'ユニーク',
index_type: 'インデックスタイプ',
delete_index: 'インデックスを削除',
},
table_actions: {

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ko_KR: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: '새로 만들기',
browse: '찾아보기',
tables: '테이블',
relationships: '관계',
areas: '영역',
dependencies: '종속성',
custom_types: '사용자 지정 타입',
},
menu: {
file: {
file: '파일',
databases: {
databases: '데이터베이스',
new: '새 다이어그램',
open: '열기',
browse: '찾아보기...',
save: '저장',
import: '데이터베이스 가져오기',
export_sql: 'SQL로 저장',
export_as: '다른 형식으로 저장',
delete_diagram: '다이어그램 삭제',
exit: '종료',
},
edit: {
edit: '편집',
@@ -150,6 +158,7 @@ export const ko_KR: LanguageTranslation = {
title: '인덱스 속성',
name: '인덱스 명',
unique: '유니크 여부',
index_type: '인덱스 타입',
delete_index: '인덱스 삭제',
},
table_actions: {

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const mr: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'नवीन',
browse: 'ब्राउज',
tables: 'टेबल',
relationships: 'संबंध',
areas: 'क्षेत्रे',
dependencies: 'अवलंबने',
custom_types: 'कस्टम प्रकार',
},
menu: {
file: {
file: 'फाइल',
new: 'नवीन',
open: 'उघडा',
databases: {
databases: 'डेटाबेस',
new: 'नवीन आरेख',
browse: 'ब्राउज करा...',
save: 'जतन करा',
import: 'डेटाबेस इम्पोर्ट करा',
export_sql: 'SQL एक्स्पोर्ट करा',
export_as: 'म्हणून एक्स्पोर्ट करा',
delete_diagram: 'आरेख हटवा',
exit: 'बाहेर पडा',
},
edit: {
edit: 'संपादन करा',
@@ -153,6 +161,7 @@ export const mr: LanguageTranslation = {
title: 'इंडेक्स गुणधर्म',
name: 'नाव',
unique: 'युनिक',
index_type: 'इंडेक्स प्रकार',
delete_index: 'इंडेक्स हटवा',
},
table_actions: {

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ne: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'नयाँ',
browse: 'ब्राउज',
tables: 'टेबलहरू',
relationships: 'सम्बन्धहरू',
areas: 'क्षेत्रहरू',
dependencies: 'निर्भरताहरू',
custom_types: 'कस्टम प्रकारहरू',
},
menu: {
file: {
file: 'फाइल',
new: 'नयाँ',
open: 'खोल्नुहोस्',
databases: {
databases: 'डाटाबेसहरू',
new: 'नयाँ डायाग्राम',
browse: 'ब्राउज गर्नुहोस्...',
save: 'सुरक्षित गर्नुहोस्',
import: 'डाटाबेस आयात गर्नुहोस्',
export_sql: 'SQL निर्यात गर्नुहोस्',
export_as: 'निर्यात गर्नुहोस्',
delete_diagram: 'डायाग्राम हटाउनुहोस्',
exit: 'बाहिर निस्कनुहोस्',
},
edit: {
edit: 'सम्पादन',
@@ -151,6 +159,7 @@ export const ne: LanguageTranslation = {
title: 'सूचक विशेषताहरू',
name: 'नाम',
unique: 'अनन्य',
index_type: 'इन्डेक्स प्रकार',
delete_index: 'सूचक हटाउनुहोस्',
},
table_actions: {

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const pt_BR: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Novo',
browse: 'Navegar',
tables: 'Tabelas',
relationships: 'Relacionamentos',
areas: 'Áreas',
dependencies: 'Dependências',
custom_types: 'Tipos Personalizados',
},
menu: {
file: {
file: 'Arquivo',
new: 'Novo',
open: 'Abrir',
databases: {
databases: 'Bancos de Dados',
new: 'Novo Diagrama',
browse: 'Navegar...',
save: 'Salvar',
import: 'Importar Banco de Dados',
export_sql: 'Exportar SQL',
export_as: 'Exportar como',
delete_diagram: 'Excluir Diagrama',
exit: 'Sair',
},
edit: {
edit: 'Editar',
@@ -151,6 +159,7 @@ export const pt_BR: LanguageTranslation = {
title: 'Atributos do Índice',
name: 'Nome',
unique: 'Único',
index_type: 'Tipo de Índice',
delete_index: 'Excluir Índice',
},
table_actions: {

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ru: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Новая',
browse: 'Обзор',
tables: 'Таблицы',
relationships: 'Связи',
areas: 'Области',
dependencies: 'Зависимости',
custom_types: 'Пользовательские типы',
},
menu: {
file: {
file: 'Файл',
new: 'Создать',
open: 'Открыть',
databases: {
databases: 'Базы данных',
new: 'Новая диаграмма',
browse: 'Обзор...',
save: 'Сохранить',
import: 'Импортировать базу данных',
export_sql: 'Экспорт SQL',
export_as: 'Экспортировать как',
delete_diagram: 'Удалить диаграмму',
exit: 'Выход',
},
edit: {
edit: 'Изменение',
@@ -147,6 +155,7 @@ export const ru: LanguageTranslation = {
title: 'Атрибуты индекса',
name: 'Имя',
unique: 'Уникальный',
index_type: 'Тип индекса',
delete_index: 'Удалить индекс',
},
table_actions: {

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const te: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'కొత్తది',
browse: 'బ్రాఉజ్',
tables: 'టేబల్లు',
relationships: 'సంబంధాలు',
areas: 'ప్రదేశాలు',
dependencies: 'ఆధారతలు',
custom_types: 'కస్టమ్ టైప్స్',
},
menu: {
file: {
file: 'ఫైల్',
new: 'కొత్తది',
open: 'తెరవు',
databases: {
databases: 'డేటాబేస్లు',
new: 'కొత్త డైగ్రాం',
browse: 'బ్రాఉజ్ చేయండి...',
save: 'సేవ్',
import: 'డేటాబేస్‌ను దిగుమతి చేసుకోండి',
export_sql: 'SQL ఎగుమతి',
export_as: 'వగా ఎగుమతి చేయండి',
delete_diagram: 'చిత్రాన్ని తొలగించండి',
exit: 'నిష్క్రమించు',
},
edit: {
edit: 'సవరించు',
@@ -151,6 +159,7 @@ export const te: LanguageTranslation = {
title: 'ఇండెక్స్ గుణాలు',
name: 'పేరు',
unique: 'అద్వితీయ',
index_type: 'ఇండెక్స్ రకం',
delete_index: 'ఇండెక్స్ తొలగించు',
},
table_actions: {

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const tr: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Yeni',
browse: 'Gözat',
tables: 'Tablolar',
relationships: 'İlişkiler',
areas: 'Alanlar',
dependencies: 'Bağımlılıklar',
custom_types: 'Özel Tipler',
},
menu: {
file: {
file: 'Dosya',
new: 'Yeni',
open: '',
databases: {
databases: 'Veritabanları',
new: 'Yeni Diyagram',
browse: 'Gözat...',
save: 'Kaydet',
import: 'Veritabanı İçe Aktar',
export_sql: 'SQL Olarak Dışa Aktar',
export_as: 'Olarak Dışa Aktar',
delete_diagram: 'Diyagramı Sil',
exit: ıkış',
},
edit: {
edit: 'Düzenle',
@@ -150,6 +158,7 @@ export const tr: LanguageTranslation = {
title: 'İndeks Özellikleri',
name: 'Ad',
unique: 'Tekil',
index_type: 'İndeks Türü',
delete_index: 'İndeksi Sil',
},
table_actions: {

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const uk: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Нова',
browse: 'Огляд',
tables: 'Таблиці',
relationships: 'Зв’язки',
areas: 'Області',
dependencies: 'Залежності',
custom_types: 'Користувацькі типи',
},
menu: {
file: {
file: 'Файл',
new: 'Новий',
open: 'Відкрити',
databases: {
databases: 'Бази даних',
new: 'Нова діаграма',
browse: 'Огляд...',
save: 'Зберегти',
import: 'Імпорт бази даних',
export_sql: 'Експорт SQL',
export_as: 'Експортувати як',
delete_diagram: 'Видалити діаграму',
exit: 'Вийти',
},
edit: {
edit: 'Редагувати',
@@ -149,6 +157,7 @@ export const uk: LanguageTranslation = {
title: 'Атрибути індексу',
name: 'Назва індекса',
unique: 'Унікальний',
index_type: 'Тип індексу',
delete_index: 'Видалити індекс',
},
table_actions: {

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const vi: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: 'Mới',
browse: 'Duyệt',
tables: 'Bảng',
relationships: 'Mối quan hệ',
areas: 'Khu vực',
dependencies: 'Phụ thuộc',
custom_types: 'Kiểu tùy chỉnh',
},
menu: {
file: {
file: 'Tệp',
new: 'Tạo mới',
open: 'Mở',
databases: {
databases: 'Cơ sở dữ liệu',
new: 'Sơ đồ mới',
browse: 'Duyệt...',
save: 'Lưu',
import: 'Nhập cơ sở dữ liệu',
export_sql: 'Xuất SQL',
export_as: 'Xuất thành',
delete_diagram: 'Xóa sơ đồ',
exit: 'Thoát',
},
edit: {
edit: 'Sửa',
@@ -150,6 +158,7 @@ export const vi: LanguageTranslation = {
title: 'Thuộc tính chỉ mục',
name: 'Tên',
unique: 'Giá trị duy nhất',
index_type: 'Loại chỉ mục',
delete_index: 'Xóa chỉ mục',
},
table_actions: {

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const zh_CN: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: '新建',
browse: '浏览',
tables: '表',
relationships: '关系',
areas: '区域',
dependencies: '依赖关系',
custom_types: '自定义类型',
},
menu: {
file: {
file: '文件',
new: '新建',
open: '打开',
databases: {
databases: '数据库',
new: '新建关系图',
browse: '浏览...',
save: '保存',
import: '导入数据库',
export_sql: '导出 SQL 语句',
export_as: '导出为',
delete_diagram: '删除关系图',
exit: '退出',
},
edit: {
edit: '编辑',
@@ -147,6 +155,7 @@ export const zh_CN: LanguageTranslation = {
title: '索引属性',
name: '名称',
unique: '唯一',
index_type: '索引类型',
delete_index: '删除索引',
},
table_actions: {

View File

@@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types';
export const zh_TW: LanguageTranslation = {
translation: {
editor_sidebar: {
new_diagram: '新建',
browse: '瀏覽',
tables: '表格',
relationships: '關係',
areas: '區域',
dependencies: '相依性',
custom_types: '自定義類型',
},
menu: {
file: {
file: '檔案',
new: '新增',
open: '開啟',
databases: {
databases: '資料庫',
new: '新增圖表',
browse: '瀏覽...',
save: '儲存',
import: '匯入資料庫',
export_sql: '匯出 SQL',
export_as: '匯出為特定格式',
delete_diagram: '刪除圖表',
exit: '退出',
},
edit: {
edit: '編輯',
@@ -147,6 +155,7 @@ export const zh_TW: LanguageTranslation = {
title: '索引屬性',
name: '名稱',
unique: '唯一',
index_type: '索引類型',
delete_index: '刪除索引',
},
table_actions: {

View File

@@ -19,3 +19,5 @@ export const randomColor = () => {
export const viewColor = '#b0b0b0';
export const materializedViewColor = '#7d7d7d';
export const defaultTableColor = '#8eb7ff';
export const defaultAreaColor = '#b067e9';

View File

@@ -405,7 +405,7 @@ export function exportPostgreSQL({
.filter(Boolean);
return indexFieldNames.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFieldNames.join(', ')});`
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName}${index.type && index.type !== 'btree' ? ` USING ${index.type.toUpperCase()}` : ''} (${indexFieldNames.join(', ')});`
: '';
})
.filter(Boolean);

View File

@@ -6,7 +6,7 @@ import type { DBField } from '@/lib/domain/db-field';
import type { DBIndex } from '@/lib/domain/db-index';
import type { DataType } from '@/lib/data/data-types/data-types';
import { genericDataTypes } from '@/lib/data/data-types/generic-data-types';
import { randomColor } from '@/lib/colors';
import { defaultTableColor } from '@/lib/colors';
import { DatabaseType } from '@/lib/domain/database-type';
import type { DBCustomType } from '@/lib/domain/db-custom-type';
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
@@ -727,10 +727,10 @@ export function convertToChartDBDiagram(
indexes,
x: col * tableSpacing,
y: row * tableSpacing,
color: randomColor(),
color: defaultTableColor,
isView: false,
createdAt: Date.now(),
};
} satisfies DBTable;
});
// Process relationships
@@ -786,12 +786,6 @@ export function convertToChartDBDiagram(
);
if (!sourceField || !targetField) {
console.log('Relationship refers to non-existent field:', {
sourceTable: rel.sourceTable,
sourceField: rel.sourceColumn,
targetTable: rel.targetTable,
targetField: rel.targetColumn,
});
return;
}

View File

@@ -203,11 +203,6 @@ export function findTableWithSchemaSupport(
// If still not found with schema, try any match on the table name
if (!table) {
table = tables.find((t) => t.name === tableName);
if (table) {
console.log(
`Found table ${tableName} without schema match, source schema: ${effectiveSchema}, table schema: ${table.schema}`
);
}
}
return table;
@@ -235,11 +230,7 @@ export function getTableIdWithSchemaSupport(
// If still not found with schema, try without schema
if (!tableId) {
tableId = tableMap[tableName];
if (tableId) {
console.log(
`Found table ID for ${tableName} without schema match, source schema: ${effectiveSchema}`
);
} else {
if (!tableId) {
console.warn(
`No table ID found for ${tableName} with schema ${effectiveSchema}`
);

View File

@@ -220,11 +220,45 @@ export async function sqlImportToDiagram({
targetDatabaseType
);
const adjustedTables = adjustTablePositions({
tables: diagram.tables ?? [],
relationships: diagram.relationships ?? [],
mode: 'perSchema',
});
// Apply the same logic as loadFromDatabaseMetadata for large diagrams
const LARGE_DIAGRAM_THRESHOLD = 200;
const tables = diagram.tables ?? [];
const relationships = diagram.relationships ?? [];
let adjustedTables = tables;
if (tables.length > LARGE_DIAGRAM_THRESHOLD) {
// Create a set of table IDs that have relationships
const tablesWithRelationships = new Set<string>();
relationships.forEach((rel) => {
tablesWithRelationships.add(rel.sourceTableId);
tablesWithRelationships.add(rel.targetTableId);
});
// Separate tables into connected and isolated
const connectedTables = tables.filter((table) =>
tablesWithRelationships.has(table.id)
);
const isolatedTables = tables.filter(
(table) => !tablesWithRelationships.has(table.id)
);
// Only reorder connected tables
const reorderedConnectedTables = adjustTablePositions({
tables: connectedTables,
relationships,
mode: 'perSchema',
});
// Combine reordered connected tables with isolated tables
adjustedTables = [...reorderedConnectedTables, ...isolatedTables];
} else {
// For smaller diagrams, reorder all tables as before
adjustedTables = adjustTablePositions({
tables,
relationships,
mode: 'perSchema',
});
}
const sortedTables = adjustedTables.sort((a, b) => {
if (a.isView === b.isView) {

View File

@@ -6,7 +6,7 @@ import type { Cardinality, DBRelationship } from '@/lib/domain/db-relationship';
import type { DBField } from '@/lib/domain/db-field';
import type { DataTypeData } from '@/lib/data/data-types/data-types';
import { findDataTypeDataById } from '@/lib/data/data-types/data-types';
import { randomColor } from '@/lib/colors';
import { defaultTableColor } from '@/lib/colors';
import { DatabaseType } from '@/lib/domain/database-type';
import type Field from '@dbml/core/types/model_structure/field';
import type { DBIndex } from '@/lib/domain';
@@ -507,11 +507,11 @@ export const importDBMLToDiagram = async (
indexes,
x: col * tableSpacing,
y: row * tableSpacing,
color: randomColor(),
color: defaultTableColor,
isView: false,
createdAt: Date.now(),
comments: tableComment,
} as DBTable;
} satisfies DBTable;
});
// Create relationships using the refs

View File

@@ -2,6 +2,25 @@ import { z } from 'zod';
import type { AggregatedIndexInfo } from '../data/import-metadata/metadata-types/index-info';
import { generateId } from '../utils';
import type { DBField } from './db-field';
import { DatabaseType } from './database-type';
export const INDEX_TYPES = [
'btree',
'hash',
'gist',
'gin',
'spgist',
'brin',
// sql server
'nonclustered',
'clustered',
'xml',
'fulltext',
'spatial',
'hash',
'index',
] as const;
export type IndexType = (typeof INDEX_TYPES)[number];
export interface DBIndex {
id: string;
@@ -9,6 +28,7 @@ export interface DBIndex {
unique: boolean;
fieldIds: string[];
createdAt: number;
type?: IndexType | null;
}
export const dbIndexSchema: z.ZodType<DBIndex> = z.object({
@@ -17,6 +37,7 @@ export const dbIndexSchema: z.ZodType<DBIndex> = z.object({
unique: z.boolean(),
fieldIds: z.array(z.string()),
createdAt: z.number(),
type: z.enum(INDEX_TYPES).optional(),
});
export const createIndexesFromMetadata = ({
@@ -36,5 +57,10 @@ export const createIndexesFromMetadata = ({
.map((c) => fields.find((f) => f.name === c.name)?.id)
.filter((id): id is string => id !== undefined),
createdAt: Date.now(),
type: idx.index_type?.toLowerCase() as IndexType,
})
);
export const databaseIndexTypes: { [key in DatabaseType]?: IndexType[] } = {
[DatabaseType.POSTGRESQL]: ['btree', 'hash'],
};

View File

@@ -10,7 +10,11 @@ import {
} from './db-field';
import type { TableInfo } from '../data/import-metadata/metadata-types/table-info';
import { createAggregatedIndexes } from '../data/import-metadata/metadata-types/index-info';
import { materializedViewColor, viewColor, randomColor } from '@/lib/colors';
import {
materializedViewColor,
viewColor,
defaultTableColor,
} from '@/lib/colors';
import type { DBRelationship } from './db-relationship';
import {
decodeBase64ToUtf16LE,
@@ -22,6 +26,7 @@ import { schemaNameToDomainSchemaName } from './db-schema';
import { DatabaseType } from './database-type';
import type { DatabaseMetadata } from '../data/import-metadata/metadata-types/database-metadata';
import { z } from 'zod';
import type { Area } from './area';
export const MAX_TABLE_SIZE = 450;
export const MID_TABLE_SIZE = 337;
@@ -224,7 +229,7 @@ export const createTablesFromMetadata = ({
? materializedViewColor
: isView
? viewColor
: randomColor(),
: defaultTableColor,
isView: isView,
isMaterializedView: isMaterializedView,
createdAt: Date.now(),
@@ -235,89 +240,170 @@ export const createTablesFromMetadata = ({
return result;
};
// Simple grid-based positioning for large databases
const adjustTablePositionsSimple = (
tables: DBTable[],
mode: 'all' | 'perSchema' = 'all'
): DBTable[] => {
const TABLES_PER_ROW = 20;
const TABLE_WIDTH = 250;
const TABLE_HEIGHT = 350;
const GAP_X = 50;
const GAP_Y = 50;
const START_X = 100;
const START_Y = 100;
if (mode === 'perSchema') {
// Group tables by schema for better organization
const tablesBySchema = new Map<string, DBTable[]>();
tables.forEach((table) => {
const schema = table.schema || 'default';
if (!tablesBySchema.has(schema)) {
tablesBySchema.set(schema, []);
}
tablesBySchema.get(schema)!.push(table);
});
const result: DBTable[] = [];
let currentSchemaOffset = 0;
// Position each schema's tables in its own section
tablesBySchema.forEach((schemaTables) => {
schemaTables.forEach((table, index) => {
const row = Math.floor(index / TABLES_PER_ROW);
const col = index % TABLES_PER_ROW;
result.push({
...table,
x: START_X + col * (TABLE_WIDTH + GAP_X),
y:
START_Y +
currentSchemaOffset +
row * (TABLE_HEIGHT + GAP_Y),
});
});
// Add extra spacing between schemas
const schemaRows = Math.ceil(schemaTables.length / TABLES_PER_ROW);
currentSchemaOffset += schemaRows * (TABLE_HEIGHT + GAP_Y) + 200;
});
return result;
}
// Simple mode - just arrange all tables in a grid
return tables.map((table, index) => {
const row = Math.floor(index / TABLES_PER_ROW);
const col = index % TABLES_PER_ROW;
return {
...table,
x: START_X + col * (TABLE_WIDTH + GAP_X),
y: START_Y + row * (TABLE_HEIGHT + GAP_Y),
};
});
};
export const adjustTablePositions = ({
relationships: inputRelationships,
tables: inputTables,
areas: inputAreas = [],
mode = 'all',
}: {
tables: DBTable[];
relationships: DBRelationship[];
areas?: Area[];
mode?: 'all' | 'perSchema';
}): DBTable[] => {
// For large databases, use simple grid layout for better performance
if (inputTables.length > 200) {
const result = adjustTablePositionsSimple(inputTables, mode);
return result;
}
// For smaller databases, use the existing complex algorithm
// Deep copy inputs for manipulation
const tables = deepCopy(inputTables);
const relationships = deepCopy(inputRelationships);
const areas = deepCopy(inputAreas);
// If there are no areas, fall back to the original algorithm
if (areas.length === 0) {
return adjustTablePositionsWithoutAreas(tables, relationships, mode);
}
// Group tables by their parent area
const tablesByArea = new Map<string | null, DBTable[]>();
// Initialize with empty arrays for all areas
areas.forEach((area) => {
tablesByArea.set(area.id, []);
});
// Also create a group for tables without areas
tablesByArea.set(null, []);
// Group tables
tables.forEach((table) => {
const areaId = table.parentAreaId || null;
if (areaId && tablesByArea.has(areaId)) {
tablesByArea.get(areaId)!.push(table);
} else {
// If the area doesn't exist or table has no area, put it in the null group
tablesByArea.get(null)!.push(table);
}
});
// Check and adjust tables within each area
areas.forEach((area) => {
const tablesInArea = tablesByArea.get(area.id) || [];
if (tablesInArea.length === 0) return;
// Only reposition tables that are outside their area bounds
const tablesToReposition = tablesInArea.filter((table) => {
return !isTableInsideArea(table, area);
});
if (tablesToReposition.length > 0) {
// Create a sub-graph of relationships for tables that need repositioning
const areaRelationships = relationships.filter((rel) => {
const sourceNeedsReposition = tablesToReposition.some(
(t) => t.id === rel.sourceTableId
);
const targetNeedsReposition = tablesToReposition.some(
(t) => t.id === rel.targetTableId
);
return sourceNeedsReposition && targetNeedsReposition;
});
// Position only tables that are outside the area bounds
positionTablesWithinArea(
tablesToReposition,
areaRelationships,
area
);
}
// Tables already inside the area keep their positions
});
// Position free tables (those not in any area)
const freeTables = tablesByArea.get(null) || [];
if (freeTables.length > 0) {
// Create a sub-graph of relationships for free tables
const freeRelationships = relationships.filter((rel) => {
const sourceIsFree = freeTables.some(
(t) => t.id === rel.sourceTableId
);
const targetIsFree = freeTables.some(
(t) => t.id === rel.targetTableId
);
return sourceIsFree && targetIsFree;
});
// Use the original algorithm for free tables with area avoidance
adjustTablePositionsWithoutAreas(
freeTables,
freeRelationships,
mode,
areas
);
}
return tables;
};
// Helper function to check if a table is inside an area
function isTableInsideArea(table: DBTable, area: Area): boolean {
const tableDimensions = getTableDimensions(table);
const padding = 20; // Same padding as used in positioning
return (
table.x >= area.x + padding &&
table.x + tableDimensions.width <= area.x + area.width - padding &&
table.y >= area.y + padding &&
table.y + tableDimensions.height <= area.y + area.height - padding
);
}
// Helper function to position tables within an area
function positionTablesWithinArea(
tables: DBTable[],
_relationships: DBRelationship[],
area: Area
) {
if (tables.length === 0) return;
const padding = 20; // Padding from area edges
const gapX = 50;
const gapY = 50;
// Available space within the area
const availableWidth = area.width - 2 * padding;
const availableHeight = area.height - 2 * padding;
// Simple grid layout within the area
const cols = Math.max(1, Math.floor(availableWidth / 250));
const rows = Math.ceil(tables.length / cols);
const cellWidth = availableWidth / cols;
const cellHeight = availableHeight / Math.max(rows, 1);
tables.forEach((table, index) => {
const col = index % cols;
const row = Math.floor(index / cols);
// Position relative to area
table.x = area.x + padding + col * cellWidth + gapX / 2;
table.y = area.y + padding + row * cellHeight + gapY / 2;
// Ensure table stays within area bounds
const tableDimensions = getTableDimensions(table);
const maxX = area.x + area.width - padding - tableDimensions.width;
const maxY = area.y + area.height - padding - tableDimensions.height;
table.x = Math.min(table.x, maxX);
table.y = Math.min(table.y, maxY);
table.x = Math.max(table.x, area.x + padding);
table.y = Math.max(table.y, area.y + padding);
});
}
// Original algorithm with area avoidance
function adjustTablePositionsWithoutAreas(
tables: DBTable[],
relationships: DBRelationship[],
mode: 'all' | 'perSchema',
areas: Area[] = []
): DBTable[] {
const adjustPositionsForTables = (tablesToAdjust: DBTable[]) => {
const defaultTableWidth = 200;
const defaultTableHeight = 300;
@@ -339,8 +425,23 @@ export const adjustTablePositions = ({
tableConnections.get(rel.targetTableId)!.add(rel.sourceTableId);
});
// Sort tables by number of connections
const sortedTables = [...tablesToAdjust].sort(
// Separate tables into connected and isolated
const connectedTables: DBTable[] = [];
const isolatedTables: DBTable[] = [];
tablesToAdjust.forEach((table) => {
if (
tableConnections.has(table.id) &&
tableConnections.get(table.id)!.size > 0
) {
connectedTables.push(table);
} else {
isolatedTables.push(table);
}
});
// Sort connected tables by number of connections (most connected first)
connectedTables.sort(
(a, b) =>
(tableConnections.get(b.id)?.size || 0) -
(tableConnections.get(a.id)?.size || 0)
@@ -368,6 +469,7 @@ export const adjustTablePositions = ({
y: number,
currentTableId: string
): boolean => {
// Check overlap with other tables
for (const [tableId, pos] of tablePositions) {
if (tableId === currentTableId) continue;
@@ -379,6 +481,26 @@ export const adjustTablePositions = ({
return true;
}
}
// Check overlap with areas
const { width: currentWidth, height: currentHeight } =
getTableWidthAndHeight(currentTableId);
const buffer = 50; // Add buffer around areas to keep tables away
for (const area of areas) {
// Check if the table position would overlap with the area (with buffer)
if (
!(
x + currentWidth < area.x - buffer ||
x > area.x + area.width + buffer ||
y + currentHeight < area.y - buffer ||
y > area.y + area.height + buffer
)
) {
return true;
}
}
return false;
};
@@ -462,19 +584,80 @@ export const adjustTablePositions = ({
});
};
// Position tables
sortedTables.forEach((table, index) => {
if (!positionedTables.has(table.id)) {
const row = Math.floor(index / 6);
const col = index % 6;
const { width: tableWidth, height: tableHeight } =
getTableWidthAndHeight(table.id);
// Position connected tables first
if (connectedTables.length < 100) {
// Use relationship-based positioning for small sets of connected tables
connectedTables.forEach((table, index) => {
if (!positionedTables.has(table.id)) {
const row = Math.floor(index / 6);
const col = index % 6;
const { width: tableWidth, height: tableHeight } =
getTableWidthAndHeight(table.id);
const x = startX + col * (tableWidth + gapX * 2);
const y = startY + row * (tableHeight + gapY * 2);
positionTable(table, x, y);
const x = startX + col * (tableWidth + gapX * 2);
const y = startY + row * (tableHeight + gapY * 2);
positionTable(table, x, y);
}
});
} else {
// Use simple grid layout for large sets of connected tables
connectedTables.forEach((table, index) => {
if (!positionedTables.has(table.id)) {
const row = Math.floor(index / 10); // More columns for large sets
const col = index % 10;
const { width: tableWidth, height: tableHeight } =
getTableWidthAndHeight(table.id);
const x = startX + col * (tableWidth + gapX);
const y = startY + row * (tableHeight + gapY);
// Direct positioning without relationship-based clustering
const finalPos = findNonOverlappingPosition(x, y, table.id);
table.x = finalPos.x;
table.y = finalPos.y;
tablePositions.set(table.id, { x: table.x, y: table.y });
positionedTables.add(table.id);
}
});
}
// Find the bottommost position of connected tables for isolated table placement
let maxY = startY;
for (const pos of tablePositions.values()) {
const tableId = [...tablePositions.entries()].find(
([, p]) => p === pos
)?.[0];
if (tableId) {
const { height } = getTableWidthAndHeight(tableId);
maxY = Math.max(maxY, pos.y + height);
}
});
}
// Position isolated tables after connected ones
if (isolatedTables.length > 0) {
const isolatedStartY = maxY + gapY * 2;
const isolatedStartX = startX;
isolatedTables.forEach((table, index) => {
if (!positionedTables.has(table.id)) {
const row = Math.floor(index / 8); // More columns for isolated tables
const col = index % 8;
const { width: tableWidth, height: tableHeight } =
getTableWidthAndHeight(table.id);
// Use a simple grid layout for isolated tables
const x = isolatedStartX + col * (tableWidth + gapX);
const y = isolatedStartY + row * (tableHeight + gapY);
// Find non-overlapping position
const finalPos = findNonOverlappingPosition(x, y, table.id);
table.x = finalPos.x;
table.y = finalPos.y;
tablePositions.set(table.id, { x: table.x, y: table.y });
positionedTables.add(table.id);
}
});
}
// Apply positions to tables
tablesToAdjust.forEach((table) => {
@@ -508,7 +691,7 @@ export const adjustTablePositions = ({
}
return tables;
};
}
export const calcTableHeight = (table?: DBTable): number => {
if (!table) {

View File

@@ -4,9 +4,11 @@ export interface DiagramFilter {
tableIds?: string[];
}
export interface TableInfo {
export interface FilterTableInfo {
id: string;
schemaId?: string;
schema?: string;
areaId?: string;
}
/**
@@ -18,7 +20,7 @@ export interface TableInfo {
*/
export function reduceFilter(
filter: DiagramFilter,
tables: TableInfo[]
tables: FilterTableInfo[]
): DiagramFilter {
let { schemaIds, tableIds } = filter;
@@ -27,6 +29,10 @@ export function reduceFilter(
return { schemaIds: undefined, tableIds: undefined };
}
if (!schemaIds && tableIds && tableIds.length === 0) {
return { schemaIds: undefined, tableIds: [] };
}
// Get all unique schema IDs from tables
const allSchemaIds = [
...new Set(tables.filter((t) => t.schemaId).map((t) => t.schemaId!)),
@@ -145,3 +151,50 @@ export function reduceFilter(
tableIds: reducedTableIds,
};
}
export const spreadFilterTables = (
filter: DiagramFilter,
tables: FilterTableInfo[]
): DiagramFilter => {
const { schemaIds, tableIds } = filter;
// If no filters are defined, everything is visible (return undefined)
if (!schemaIds && !tableIds) {
const allTablesIds = new Set<string>();
tables.forEach((table) => {
allTablesIds.add(table.id);
});
return { tableIds: Array.from(allTablesIds) };
}
// If only tableIds is defined, return it as is
if (!schemaIds && tableIds) {
return { tableIds };
}
// Collect all table IDs that should be visible
const visibleTableIds = new Set<string>();
// Add existing tableIds to the set
if (tableIds) {
tableIds.forEach((id) => visibleTableIds.add(id));
}
// Add all tables from specified schemas
if (schemaIds) {
const schemaSet = new Set(schemaIds);
tables.forEach((table) => {
if (table.schemaId && schemaSet.has(table.schemaId)) {
visibleTableIds.add(table.id);
}
});
}
// If no tables are visible, return empty array
if (visibleTableIds.size === 0) {
return { tableIds: [] };
}
return { tableIds: Array.from(visibleTableIds) };
};

View File

@@ -37,48 +37,6 @@ export const filterTable = ({
return false;
};
export const filterTableBySchema = ({
table,
schemaIdsFilter,
options = { defaultSchema: undefined },
}: {
table: { id: string; schema?: string | null };
schemaIdsFilter?: string[];
options?: {
defaultSchema?: string;
};
}): boolean => {
if (!schemaIdsFilter) {
return true;
}
const tableSchemaId = table.schema ?? options.defaultSchema;
if (tableSchemaId) {
return schemaIdsFilter.includes(schemaNameToSchemaId(tableSchemaId));
}
return false;
};
export const filterSchema = ({
schemaId,
schemaIdsFilter,
}: {
schemaId?: string;
schemaIdsFilter?: string[];
}): boolean => {
if (!schemaIdsFilter) {
return true;
}
if (!schemaId) {
return false;
}
return schemaIdsFilter.includes(schemaId);
};
export const filterRelationship = ({
tableA: { id: tableAId, schema: tableASchema },
tableB: { id: tableBId, schema: tableBSchema },

View File

@@ -64,7 +64,7 @@ export const loadFromDatabaseMetadata = async ({
databaseMetadata: DatabaseMetadata;
diagramNumber?: number;
databaseEdition?: DatabaseEdition;
}): Promise<Diagram> => {
}): Promise<{ diagram: Diagram; initialFilter?: { tableIds: string[] } }> => {
const {
fk_info: foreignKeys,
views: views,
@@ -93,11 +93,51 @@ export const loadFromDatabaseMetadata = async ({
})
: [];
const adjustedTables = adjustTablePositions({
tables,
relationships,
mode: 'perSchema',
});
// For large diagrams, apply special handling
const LARGE_DIAGRAM_THRESHOLD = 200;
let adjustedTables = tables;
let initialFilter: { tableIds: string[] } | undefined;
if (tables.length > LARGE_DIAGRAM_THRESHOLD) {
// Create a set of table IDs that have relationships
const tablesWithRelationships = new Set<string>();
relationships.forEach((rel) => {
tablesWithRelationships.add(rel.sourceTableId);
tablesWithRelationships.add(rel.targetTableId);
});
// Separate tables into connected and isolated
const connectedTables = tables.filter((table) =>
tablesWithRelationships.has(table.id)
);
const isolatedTables = tables.filter(
(table) => !tablesWithRelationships.has(table.id)
);
// Only reorder connected tables
const reorderedConnectedTables = adjustTablePositions({
tables: connectedTables,
relationships,
mode: 'perSchema',
});
// Combine reordered connected tables with isolated tables
adjustedTables = [...reorderedConnectedTables, ...isolatedTables];
// Set up filter to hide isolated tables if there are any
if (isolatedTables.length > 0) {
initialFilter = {
tableIds: connectedTables.map((t) => t.id),
};
}
} else {
// For smaller diagrams, reorder all tables as before
adjustedTables = adjustTablePositions({
tables,
relationships,
mode: 'perSchema',
});
}
const sortedTables = adjustedTables.sort((a, b) => {
if (a.isView === b.isView) {
@@ -125,5 +165,5 @@ export const loadFromDatabaseMetadata = async ({
updatedAt: new Date(),
};
return diagram;
return { diagram, initialFilter };
};

View File

@@ -1,6 +1,7 @@
import { cloneDiagram } from './clone';
import { diagramSchema, type Diagram } from './domain/diagram';
import { generateDiagramId } from './utils';
import { adjustTablePositions } from './domain/db-table';
export const runningIdGenerator = (): (() => string) => {
let id = 0;
@@ -36,5 +37,82 @@ export const diagramFromJSONInput = (json: string): Diagram => {
updatedAt: new Date(),
});
return cloneDiagramWithIds(diagram);
const clonedDiagram = cloneDiagramWithIds(diagram);
// Apply reordering for large diagrams AFTER identifying which tables have relationships
const LARGE_DIAGRAM_THRESHOLD = 200;
if (
clonedDiagram.tables &&
clonedDiagram.tables.length > LARGE_DIAGRAM_THRESHOLD &&
clonedDiagram.relationships
) {
// Create a set of table IDs that have relationships
const tablesWithRelationships = new Set<string>();
clonedDiagram.relationships.forEach((rel) => {
tablesWithRelationships.add(rel.sourceTableId);
tablesWithRelationships.add(rel.targetTableId);
});
// Filter tables to only those with relationships for reordering
const tablesToReorder = clonedDiagram.tables.filter((table) =>
tablesWithRelationships.has(table.id)
);
// Apply reordering only to tables with relationships
const reorderedTables = adjustTablePositions({
tables: tablesToReorder,
relationships: clonedDiagram.relationships || [],
areas: clonedDiagram.areas || [],
mode: 'all',
});
// Update positions for reordered tables
clonedDiagram.tables = clonedDiagram.tables.map((table) => {
const reorderedTable = reorderedTables.find(
(t) => t.id === table.id
);
if (reorderedTable) {
return {
...table,
x: reorderedTable.x,
y: reorderedTable.y,
};
}
return table;
});
}
return clonedDiagram;
};
export const getInitialFilterForLargeDiagram = (
diagram: Diagram
): { tableIds?: string[] } | null => {
const LARGE_DIAGRAM_THRESHOLD = 200;
if (
diagram.tables &&
diagram.tables.length > LARGE_DIAGRAM_THRESHOLD &&
diagram.relationships
) {
// Create a set of table IDs that have relationships
const tablesWithRelationships = new Set<string>();
diagram.relationships.forEach((rel) => {
tablesWithRelationships.add(rel.sourceTableId);
tablesWithRelationships.add(rel.targetTableId);
});
// Return only tables with relationships to be shown (filter will hide the rest)
const tablesToShow = diagram.tables
.filter((table) => tablesWithRelationships.has(table.id))
.map((table) => table.id);
// If there are tables to filter out, return the filter
if (tablesToShow.length < diagram.tables.length) {
return { tableIds: tablesToShow };
}
}
return null;
};

View File

@@ -5,57 +5,49 @@ import React, {
useEffect,
useRef,
} from 'react';
import { X, Search, Eye, EyeOff, Database, Table, Funnel } from 'lucide-react';
import { X, Search, Database, Table, Funnel, Box } from 'lucide-react';
import { useChartDB } from '@/hooks/use-chartdb';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/button/button';
import { Input } from '@/components/input/input';
import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
import { defaultSchemas } from '@/lib/data/default-schemas';
import { useReactFlow } from '@xyflow/react';
import { TreeView } from '@/components/tree-view/tree-view';
import type { TreeNode } from '@/components/tree-view/tree';
import { ScrollArea } from '@/components/scroll-area/scroll-area';
import { filterSchema, filterTable } from '@/lib/domain/diagram-filter/filter';
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
import { ToggleGroup, ToggleGroupItem } from '@/components/toggle/toggle-group';
import type {
GroupingMode,
NodeContext,
NodeType,
RelevantTableData,
TableContext,
} from './types';
import { generateTreeDataByAreas, generateTreeDataBySchemas } from './utils';
import { FilterItemActions } from './filter-item-actions';
import { databasesWithSchemas } from '@/lib/domain';
export interface CanvasFilterProps {
onClose: () => void;
}
type NodeType = 'schema' | 'table';
type SchemaContext = { name: string; visible: boolean };
type TableContext = {
tableSchema?: string | null;
visible: boolean;
};
type NodeContext = {
schema: SchemaContext;
table: TableContext;
};
type RelevantTableData = {
id: string;
name: string;
schema?: string | null;
};
export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
const { t } = useTranslation();
const { tables, databaseType } = useChartDB();
const { tables, databaseType, areas } = useChartDB();
const {
filter,
toggleSchemaFilter,
toggleTableFilter,
clearTableIdsFilter,
setTableIdsFilterEmpty,
addTablesToFilter,
removeTablesFromFilter,
} = useDiagramFilter();
const { fitView, setNodes } = useReactFlow();
const [searchQuery, setSearchQuery] = useState('');
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
const [isFilterVisible, setIsFilterVisible] = useState(false);
const [groupingMode, setGroupingMode] = useState<GroupingMode>('schema');
const searchInputRef = useRef<HTMLInputElement>(null);
// Extract only the properties needed for tree data
@@ -65,107 +57,41 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
id: table.id,
name: table.name,
schema: table.schema,
parentAreaId: table.parentAreaId,
})),
[tables]
);
const databaseWithSchemas = useMemo(
() => !!defaultSchemas[databaseType],
() => databasesWithSchemas.includes(databaseType),
[databaseType]
);
// Convert tables to tree nodes
const treeData = useMemo(() => {
// Group tables by schema
const tablesBySchema = new Map<string, RelevantTableData[]>();
relevantTableData.forEach((table) => {
const schema = !databaseWithSchemas
? 'All Tables'
: (table.schema ?? defaultSchemas[databaseType] ?? 'default');
if (!tablesBySchema.has(schema)) {
tablesBySchema.set(schema, []);
}
tablesBySchema.get(schema)!.push(table);
});
// Sort tables within each schema
tablesBySchema.forEach((tables) => {
tables.sort((a, b) => a.name.localeCompare(b.name));
});
// Convert to tree nodes
const nodes: TreeNode<NodeType, NodeContext>[] = [];
tablesBySchema.forEach((schemaTables, schemaName) => {
let schemaVisible;
if (databaseWithSchemas) {
const schemaId = schemaNameToSchemaId(schemaName);
schemaVisible = filterSchema({
schemaId,
schemaIdsFilter: filter?.schemaIds,
});
} else {
// if at least one table is visible, the schema is considered visible
schemaVisible = schemaTables.some((table) =>
filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
})
);
}
const schemaNode: TreeNode<NodeType, NodeContext> = {
id: `schema-${schemaName}`,
name: `${schemaName} (${schemaTables.length})`,
type: 'schema',
isFolder: true,
icon: Database,
context: { name: schemaName, visible: schemaVisible },
className: !schemaVisible ? 'opacity-50' : '',
children: schemaTables.map(
(table): TreeNode<NodeType, NodeContext> => {
const tableVisible = filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
});
const hidden = !tableVisible;
return {
id: table.id,
name: table.name,
type: 'table',
isFolder: false,
icon: Table,
context: {
tableSchema: table.schema,
visible: tableVisible,
},
className: hidden ? 'opacity-50' : '',
};
}
),
};
nodes.push(schemaNode);
});
return nodes;
}, [relevantTableData, databaseType, filter, databaseWithSchemas]);
if (groupingMode === 'area') {
return generateTreeDataByAreas({
areas,
databaseType,
filter,
relevantTableData,
});
} else {
return generateTreeDataBySchemas({
relevantTableData,
databaseWithSchemas,
databaseType,
filter,
});
}
}, [
relevantTableData,
databaseType,
filter,
databaseWithSchemas,
groupingMode,
areas,
]);
// Initialize expanded state with all schemas expanded
useMemo(() => {
@@ -201,6 +127,31 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
return result;
}, [treeData, searchQuery]);
// Render actions with proper memoization for performance
const renderActions = useCallback(
(node: TreeNode<NodeType, NodeContext>) => (
<FilterItemActions
node={node}
databaseWithSchemas={databaseWithSchemas}
toggleSchemaFilter={toggleSchemaFilter}
toggleTableFilter={toggleTableFilter}
clearTableIdsFilter={clearTableIdsFilter}
setTableIdsFilterEmpty={setTableIdsFilterEmpty}
addTablesToFilter={addTablesToFilter}
removeTablesFromFilter={removeTablesFromFilter}
/>
),
[
databaseWithSchemas,
toggleSchemaFilter,
toggleTableFilter,
clearTableIdsFilter,
setTableIdsFilterEmpty,
addTablesToFilter,
removeTablesFromFilter,
]
);
const focusOnTable = useCallback(
(tableId: string) => {
// Make sure the table is visible
@@ -236,86 +187,14 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
[fitView, setNodes]
);
// Render component that's always visible (eye indicator)
const renderActions = useCallback(
(node: TreeNode<NodeType, NodeContext>) => {
if (node.type === 'schema') {
const schemaContext = node.context as SchemaContext;
const schemaId = schemaNameToSchemaId(schemaContext.name);
const schemaVisible = node.context.visible;
return (
<Button
variant="ghost"
size="sm"
className="size-7 h-fit p-0"
onClick={(e) => {
e.stopPropagation();
if (databaseWithSchemas) {
toggleSchemaFilter(schemaId);
} else {
// Toggle visibility of all tables in this schema
if (node.context.visible) {
setTableIdsFilterEmpty();
} else {
clearTableIdsFilter();
}
}
}}
>
{!schemaVisible ? (
<EyeOff className="size-3.5 text-muted-foreground" />
) : (
<Eye className="size-3.5" />
)}
</Button>
);
}
if (node.type === 'table') {
const tableId = node.id;
const tableContext = node.context as TableContext;
const tableVisible = tableContext.visible;
return (
<Button
variant="ghost"
size="sm"
className="size-7 h-fit p-0"
onClick={(e) => {
e.stopPropagation();
toggleTableFilter(tableId);
}}
>
{!tableVisible ? (
<EyeOff className="size-3.5 text-muted-foreground" />
) : (
<Eye className="size-3.5" />
)}
</Button>
);
}
return null;
},
[
toggleSchemaFilter,
toggleTableFilter,
clearTableIdsFilter,
setTableIdsFilterEmpty,
databaseWithSchemas,
]
);
// Handle node click
const handleNodeClick = useCallback(
(node: TreeNode<NodeType, NodeContext>) => {
if (node.type === 'table') {
const tableContext = node.context as TableContext;
const isTableVisible = tableContext.visible;
const context = node.context as TableContext;
const isTableVisible = context.visible;
// Only focus if neither table is hidden nor filtered by schema
// Only focus if table is visible
if (isTableVisible) {
focusOnTable(node.id);
}
@@ -376,13 +255,34 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
</div>
</div>
{/* Grouping Toggle */}
<div className="border-b p-2">
<ToggleGroup
type="single"
value={groupingMode}
onValueChange={(value) => {
if (value) setGroupingMode(value as GroupingMode);
}}
className="w-full"
>
<ToggleGroupItem value="schema" className="flex-1 text-xs">
<Database className="mr-1.5 size-3.5" />
{t('canvas_filter.group_by_schema', 'Group by Schema')}
</ToggleGroupItem>
<ToggleGroupItem value="area" className="flex-1 text-xs">
<Box className="mr-1.5 size-3.5" />
{t('canvas_filter.group_by_area', 'Group by Area')}
</ToggleGroupItem>
</ToggleGroup>
</div>
{/* Table Tree */}
<ScrollArea className="flex-1 rounded-b-lg" type="auto">
<TreeView
data={filteredTreeData}
onNodeClick={handleNodeClick}
renderActionsComponent={renderActions}
defaultFolderIcon={Database}
defaultFolderIcon={groupingMode === 'area' ? Box : Database}
defaultIcon={Table}
expanded={expanded}
setExpanded={setExpanded}

View File

@@ -0,0 +1,143 @@
import React from 'react';
import { Eye, EyeOff } from 'lucide-react';
import { Button } from '@/components/button/button';
import type { TreeNode } from '@/components/tree-view/tree';
import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
import type {
AreaContext,
NodeContext,
NodeType,
// RelevantTableData,
SchemaContext,
TableContext,
} from './types';
import type { FilterTableInfo } from '@/lib/domain/diagram-filter/diagram-filter';
interface FilterItemActionsProps {
node: TreeNode<NodeType, NodeContext>;
databaseWithSchemas: boolean;
toggleSchemaFilter: (schemaId: string) => void;
toggleTableFilter: (tableId: string) => void;
clearTableIdsFilter: () => void;
setTableIdsFilterEmpty: () => void;
addTablesToFilter: (attrs: {
tableIds?: string[];
filterCallback?: (table: FilterTableInfo) => boolean;
}) => void;
removeTablesFromFilter: (attrs: {
tableIds?: string[];
filterCallback?: (table: FilterTableInfo) => boolean;
}) => void;
}
export const FilterItemActions: React.FC<FilterItemActionsProps> = ({
node,
databaseWithSchemas,
toggleSchemaFilter,
toggleTableFilter,
clearTableIdsFilter,
setTableIdsFilterEmpty,
addTablesToFilter,
removeTablesFromFilter,
}) => {
if (node.type === 'schema') {
const context = node.context as SchemaContext;
const schemaVisible = context.visible;
const schemaName = context.name;
const schemaId = schemaNameToSchemaId(schemaName);
return (
<Button
variant="ghost"
size="sm"
className="size-7 h-fit p-0"
onClick={(e) => {
e.stopPropagation();
if (databaseWithSchemas) {
toggleSchemaFilter(schemaId);
} else {
// Toggle visibility of all tables in this schema
if (schemaVisible) {
setTableIdsFilterEmpty();
} else {
clearTableIdsFilter();
}
}
}}
>
{!schemaVisible ? (
<EyeOff className="size-3.5 text-muted-foreground" />
) : (
<Eye className="size-3.5" />
)}
</Button>
);
}
if (node.type === 'area') {
const context = node.context as AreaContext;
const areaVisible = context.visible;
const isUngrouped = context.isUngrouped;
const areaId = context.id;
return (
<Button
variant="ghost"
size="sm"
className="size-7 h-fit p-0"
onClick={(e) => {
e.stopPropagation();
// Toggle all tables in this area
if (areaVisible) {
// Hide all tables in this area
removeTablesFromFilter({
filterCallback: (table) =>
(isUngrouped && !table.areaId) ||
(!isUngrouped && table.areaId === areaId),
});
} else {
// Show all tables in this area
addTablesToFilter({
filterCallback: (table) =>
(isUngrouped && !table.areaId) ||
(!isUngrouped && table.areaId === areaId),
});
}
}}
>
{!areaVisible ? (
<EyeOff className="size-3.5 text-muted-foreground" />
) : (
<Eye className="size-3.5" />
)}
</Button>
);
}
if (node.type === 'table') {
const tableId = node.id;
const context = node.context as TableContext;
const tableVisible = context.visible;
return (
<Button
variant="ghost"
size="sm"
className="size-7 h-fit p-0"
onClick={(e) => {
e.stopPropagation();
toggleTableFilter(tableId);
}}
>
{!tableVisible ? (
<EyeOff className="size-3.5 text-muted-foreground" />
) : (
<Eye className="size-3.5" />
)}
</Button>
);
}
return null;
};

View File

@@ -0,0 +1,27 @@
export type NodeType = 'schema' | 'area' | 'table';
export type GroupingMode = 'schema' | 'area';
export type SchemaContext = { name: string; visible: boolean };
export type AreaContext = {
id: string;
name: string;
visible: boolean;
isUngrouped: boolean;
};
export type TableContext = {
tableSchema?: string | null;
visible: boolean;
};
export type NodeContext = {
schema: SchemaContext;
area: AreaContext;
table: TableContext;
};
export type RelevantTableData = {
id: string;
name: string;
schema?: string | null;
parentAreaId?: string | null;
};

View File

@@ -0,0 +1,292 @@
import type { Area, DatabaseType } from '@/lib/domain';
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
import type {
AreaContext,
NodeContext,
NodeType,
RelevantTableData,
SchemaContext,
TableContext,
} from './types';
import type { TreeNode } from '@/components/tree-view/tree';
import { Box, Database, Layers, Table } from 'lucide-react';
import { filterTable } from '@/lib/domain/diagram-filter/filter';
import { defaultSchemas } from '@/lib/data/default-schemas';
export const generateTreeDataByAreas = ({
areas,
databaseType,
filter,
relevantTableData,
}: {
areas: Area[];
databaseType: DatabaseType;
filter?: DiagramFilter;
relevantTableData: RelevantTableData[];
}): TreeNode<NodeType, NodeContext>[] => {
const nodes: TreeNode<NodeType, NodeContext>[] = [];
// Group tables by area
const tablesByArea = new Map<string | null, RelevantTableData[]>();
const tablesWithoutArea: RelevantTableData[] = [];
relevantTableData.forEach((table) => {
if (table.parentAreaId) {
if (!tablesByArea.has(table.parentAreaId)) {
tablesByArea.set(table.parentAreaId, []);
}
tablesByArea.get(table.parentAreaId)!.push(table);
} else {
tablesWithoutArea.push(table);
}
});
// Sort tables within each area
tablesByArea.forEach((areaTables) => {
areaTables.sort((a, b) => a.name.localeCompare(b.name));
});
tablesWithoutArea.sort((a, b) => a.name.localeCompare(b.name));
// Create nodes for areas
areas.forEach((area) => {
const areaTables = tablesByArea.get(area.id) || [];
// Check if at least one table in the area is visible
const areaVisible =
// areaTables.length === 0 ||
!areaTables.some(
(table) =>
filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
}) === false
);
const areaNode: TreeNode<NodeType, NodeContext> = {
id: `area-${area.id}`,
name: `${area.name} (${areaTables.length})`,
type: 'area',
isFolder: true,
icon: Box,
context: {
id: area.id,
name: area.name,
visible: areaVisible,
isUngrouped: false,
} satisfies AreaContext,
className: !areaVisible ? 'opacity-50' : '',
children: areaTables.map(
(table): TreeNode<NodeType, NodeContext> => {
const tableVisible = filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
});
return {
id: table.id,
name: table.name,
type: 'table',
isFolder: false,
icon: Table,
context: {
tableSchema: table.schema,
visible: tableVisible,
} satisfies TableContext,
className: !tableVisible ? 'opacity-50' : '',
};
}
),
};
if (areaTables.length > 0) {
nodes.push(areaNode);
}
});
// Add ungrouped tables
if (tablesWithoutArea.length > 0) {
const ungroupedVisible = !tablesWithoutArea.some(
(table) =>
filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
}) == false
);
const ungroupedNode: TreeNode<NodeType, NodeContext> = {
id: 'ungrouped',
name: `Ungrouped (${tablesWithoutArea.length})`,
type: 'area',
isFolder: true,
icon: Layers,
context: {
id: 'ungrouped',
name: 'Ungrouped',
visible: ungroupedVisible,
isUngrouped: true,
} satisfies AreaContext,
className: !ungroupedVisible ? 'opacity-50' : '',
children: tablesWithoutArea.map(
(table): TreeNode<NodeType, NodeContext> => {
const tableVisible = filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
});
return {
id: table.id,
name: table.name,
type: 'table',
isFolder: false,
icon: Table,
context: {
tableSchema: table.schema,
visible: tableVisible,
} satisfies TableContext,
className: !tableVisible ? 'opacity-50' : '',
};
}
),
};
nodes.push(ungroupedNode);
}
return nodes;
};
export const generateTreeDataBySchemas = ({
relevantTableData,
databaseWithSchemas,
databaseType,
filter,
}: {
relevantTableData: RelevantTableData[];
databaseWithSchemas: boolean;
databaseType: DatabaseType;
filter?: DiagramFilter;
}): TreeNode<NodeType, NodeContext>[] => {
const nodes: TreeNode<NodeType, NodeContext>[] = [];
// Group tables by schema (existing logic)
const tablesBySchema = new Map<string, RelevantTableData[]>();
relevantTableData.forEach((table) => {
const schema = !databaseWithSchemas
? 'All Tables'
: (table.schema ?? defaultSchemas[databaseType] ?? 'default');
if (!tablesBySchema.has(schema)) {
tablesBySchema.set(schema, []);
}
tablesBySchema.get(schema)!.push(table);
});
// Sort tables within each schema
tablesBySchema.forEach((tables) => {
tables.sort((a, b) => a.name.localeCompare(b.name));
});
tablesBySchema.forEach((schemaTables, schemaName) => {
let schemaVisible;
if (databaseWithSchemas) {
schemaVisible = !schemaTables.some(
(table) =>
filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
}) === false
);
} else {
// if at least one table is visible, the schema is considered visible
schemaVisible = !schemaTables.some(
(table) =>
filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
}) === false
);
}
const schemaNode: TreeNode<NodeType, NodeContext> = {
id: `schema-${schemaName}`,
name: `${schemaName} (${schemaTables.length})`,
type: 'schema',
isFolder: true,
icon: Database,
context: {
name: schemaName,
visible: schemaVisible,
} satisfies SchemaContext,
className: !schemaVisible ? 'opacity-50' : '',
children: schemaTables.map(
(table): TreeNode<NodeType, NodeContext> => {
const tableVisible = filterTable({
table: {
id: table.id,
schema: table.schema,
},
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
});
const hidden = !tableVisible;
return {
id: table.id,
name: table.name,
type: 'table',
isFolder: false,
icon: Table,
context: {
tableSchema: table.schema,
visible: tableVisible,
} satisfies TableContext,
className: hidden ? 'opacity-50' : '',
};
}
),
};
nodes.push(schemaNode);
});
return nodes;
};

View File

@@ -119,8 +119,15 @@ const initialEdges: EdgeType[] = [];
const tableToTableNode = (
table: DBTable,
filter: DiagramFilter | undefined,
databaseType: DatabaseType
{
filter,
databaseType,
filterLoading,
}: {
filter?: DiagramFilter;
databaseType: DatabaseType;
filterLoading: boolean;
}
): TableNodeType => {
// Always use absolute position for now
const position = { x: table.x, y: table.y };
@@ -134,23 +141,56 @@ const tableToTableNode = (
isOverlapping: false,
},
width: table.width ?? MIN_TABLE_SIZE,
hidden: !filterTable({
table: { id: table.id, schema: table.schema },
filter,
options: { defaultSchema: defaultSchemas[databaseType] },
}),
hidden:
!filterTable({
table: { id: table.id, schema: table.schema },
filter,
options: { defaultSchema: defaultSchemas[databaseType] },
}) || filterLoading,
};
};
const areaToAreaNode = (area: Area): AreaNodeType => ({
id: area.id,
type: 'area',
position: { x: area.x, y: area.y },
data: { area },
width: area.width,
height: area.height,
zIndex: -10,
});
const areaToAreaNode = (
area: Area,
{
tables,
filter,
databaseType,
filterLoading,
}: {
tables: DBTable[];
filter?: DiagramFilter;
databaseType: DatabaseType;
filterLoading: boolean;
}
): AreaNodeType => {
// Get all tables in this area
const tablesInArea = tables.filter((t) => t.parentAreaId === area.id);
// Check if at least one table in the area is visible
const hasVisibleTable =
tablesInArea.length === 0 ||
tablesInArea.some((table) =>
filterTable({
table: { id: table.id, schema: table.schema },
filter,
options: {
defaultSchema: defaultSchemas[databaseType],
},
})
);
return {
id: area.id,
type: 'area',
position: { x: area.x, y: area.y },
data: { area },
width: area.width,
height: area.height,
zIndex: -10,
hidden: !hasVisibleTable || filterLoading,
};
};
export interface CanvasProps {
initialTables: DBTable[];
@@ -200,13 +240,13 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
showFilter,
setShowFilter,
} = useCanvas();
const { filter } = useDiagramFilter();
const { filter, loading: filterLoading } = useDiagramFilter();
const [isInitialLoadingNodes, setIsInitialLoadingNodes] = useState(true);
const [nodes, setNodes, onNodesChange] = useNodesState<NodeType>(
initialTables.map((table) =>
tableToTableNode(table, filter, databaseType)
tableToTableNode(table, { filter, databaseType, filterLoading })
)
);
const [edges, setEdges, onEdgesChange] =
@@ -220,12 +260,12 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
useEffect(() => {
const initialNodes = initialTables.map((table) =>
tableToTableNode(table, filter, databaseType)
tableToTableNode(table, { filter, databaseType, filterLoading })
);
if (equal(initialNodes, nodes)) {
setIsInitialLoadingNodes(false);
}
}, [initialTables, nodes, filter, databaseType]);
}, [initialTables, nodes, filter, databaseType, filterLoading]);
useEffect(() => {
if (!isInitialLoadingNodes) {
@@ -388,7 +428,11 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
...tables.map((table) => {
const isOverlapping =
(overlapGraph.graph.get(table.id) ?? []).length > 0;
const node = tableToTableNode(table, filter, databaseType);
const node = tableToTableNode(table, {
filter,
databaseType,
filterLoading,
});
// Check if table uses the highlighted custom type
let hasHighlightedCustomType = false;
@@ -409,7 +453,14 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
},
};
}),
...areas.map(areaToAreaNode),
...areas.map((area) =>
areaToAreaNode(area, {
tables,
filter,
databaseType,
filterLoading,
})
),
];
// Check if nodes actually changed
@@ -429,6 +480,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
overlapGraph.graph,
highlightOverlappingTables,
highlightedCustomType,
filterLoading,
]);
const prevFilter = useRef<DiagramFilter | undefined>(undefined);
@@ -460,22 +512,26 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
}
}, [filter, fitView, tables, setOverlapGraph, databaseType]);
// Handle parent area updates when tables move
const tablePositions = useMemo(
() => tables.map((t) => ({ id: t.id, x: t.x, y: t.y })),
[tables]
);
useEffect(() => {
const checkParentAreas = debounce(() => {
const updatedTables = updateTablesParentAreas(tables, areas);
const visibleTables = nodes
.filter((node) => node.type === 'table' && !node.hidden)
.map((node) => (node as TableNodeType).data.table);
const visibleAreas = nodes
.filter((node) => node.type === 'area' && !node.hidden)
.map((node) => (node as AreaNodeType).data.area);
const updatedTables = updateTablesParentAreas(
visibleTables,
visibleAreas
);
const needsUpdate: Array<{
id: string;
parentAreaId: string | null;
}> = [];
updatedTables.forEach((newTable, index) => {
const oldTable = tables[index];
const oldTable = visibleTables[index];
if (
oldTable &&
(!!newTable.parentAreaId || !!oldTable.parentAreaId) &&
@@ -509,7 +565,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
}, 300);
checkParentAreas();
}, [tablePositions, areas, updateTablesState, tables]);
}, [nodes, updateTablesState]);
const onConnectHandler = useCallback(
async (params: AddEdgeParams) => {

View File

@@ -10,18 +10,11 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from '@/components/sidebar/sidebar';
import {
Twitter,
BookOpen,
Group,
FileType,
Plus,
FolderOpen,
} from 'lucide-react';
import { BookOpen, Group, FileType, Plus, FolderOpen } from 'lucide-react';
import { SquareStack, Table, Workflow } from 'lucide-react';
import { useLayout } from '@/hooks/use-layout';
import { useTranslation } from 'react-i18next';
import { DiscordLogoIcon } from '@radix-ui/react-icons';
import { DiscordLogoIcon, TwitterLogoIcon } from '@radix-ui/react-icons';
import { useBreakpoint } from '@/hooks/use-breakpoint';
import ChartDBLogo from '@/assets/logo-light.png';
import ChartDBDarkLogo from '@/assets/logo-dark.png';
@@ -53,7 +46,7 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
const diagramItems: SidebarItem[] = useMemo(
() => [
{
title: t('menu.file.new'),
title: t('editor_sidebar.new_diagram'),
icon: Plus,
onClick: () => {
openCreateDiagramDialog();
@@ -61,7 +54,7 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
active: false,
},
{
title: t('menu.file.open'),
title: t('editor_sidebar.browse'),
icon: FolderOpen,
onClick: () => {
openOpenDiagramDialog();
@@ -75,7 +68,7 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
const baseItems: SidebarItem[] = useMemo(
() => [
{
title: t('side_panel.tables_section.tables'),
title: t('editor_sidebar.tables'),
icon: Table,
onClick: () => {
showSidePanel();
@@ -84,7 +77,7 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
active: selectedSidebarSection === 'tables',
},
{
title: t('side_panel.relationships_section.relationships'),
title: t('editor_sidebar.relationships'),
icon: Workflow,
onClick: () => {
showSidePanel();
@@ -93,7 +86,7 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
active: selectedSidebarSection === 'relationships',
},
{
title: t('side_panel.areas_section.areas'),
title: t('editor_sidebar.areas'),
icon: Group,
onClick: () => {
showSidePanel();
@@ -104,9 +97,7 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
...(dependencies && dependencies.length > 0
? [
{
title: t(
'side_panel.dependencies_section.dependencies'
),
title: t('editor_sidebar.dependencies'),
icon: SquareStack,
onClick: () => {
showSidePanel();
@@ -119,9 +110,7 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
...(databaseType === DatabaseType.POSTGRESQL
? [
{
title: t(
'side_panel.custom_types_section.custom_types'
),
title: t('editor_sidebar.custom_types'),
icon: FileType,
onClick: () => {
showSidePanel();
@@ -153,7 +142,7 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
},
{
title: 'Twitter',
icon: Twitter,
icon: TwitterLogoIcon,
onClick: () =>
window.open(
'https://x.com/intent/follow?screen_name=jonathanfishner',
@@ -162,7 +151,7 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
active: false,
},
{
title: 'Documentation',
title: 'Docs',
icon: BookOpen,
onClick: () => window.open('https://docs.chartdb.io', '_blank'),
active: false,
@@ -174,7 +163,7 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
return (
<Sidebar
side="left"
collapsible="icon"
collapsible="icon-extended"
variant="sidebar"
className="relative h-full"
>
@@ -205,14 +194,21 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
{diagramItems.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
className="hover:bg-gray-200 data-[active=true]:bg-gray-100 data-[active=true]:text-pink-600 data-[active=true]:hover:bg-pink-100 dark:hover:bg-gray-800 dark:data-[active=true]:bg-gray-900 dark:data-[active=true]:text-pink-400 dark:data-[active=true]:hover:bg-pink-950"
className="justify-center space-y-0.5 !px-0 hover:bg-gray-200 data-[active=true]:bg-gray-100 data-[active=true]:text-pink-600 data-[active=true]:hover:bg-pink-100 dark:hover:bg-gray-800 dark:data-[active=true]:bg-gray-900 dark:data-[active=true]:text-pink-400 dark:data-[active=true]:hover:bg-pink-950"
isActive={item.active}
asChild
tooltip={item.title}
>
<button onClick={item.onClick}>
<item.icon />
<span>{item.title}</span>
<span>
{item.title
.split(' ')
.map((word, index) => (
<div key={index}>
{word}
</div>
))}
</span>
</button>
</SidebarMenuButton>
</SidebarMenuItem>
@@ -223,14 +219,21 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
{baseItems.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
className="hover:bg-gray-200 data-[active=true]:bg-gray-100 data-[active=true]:text-pink-600 data-[active=true]:hover:bg-pink-100 dark:hover:bg-gray-800 dark:data-[active=true]:bg-gray-900 dark:data-[active=true]:text-pink-400 dark:data-[active=true]:hover:bg-pink-950"
className="justify-center space-y-0.5 !px-0 hover:bg-gray-200 data-[active=true]:bg-gray-100 data-[active=true]:text-pink-600 data-[active=true]:hover:bg-pink-100 dark:hover:bg-gray-800 dark:data-[active=true]:bg-gray-900 dark:data-[active=true]:text-pink-400 dark:data-[active=true]:hover:bg-pink-950"
isActive={item.active}
asChild
tooltip={item.title}
>
<button onClick={item.onClick}>
<item.icon />
<span>{item.title}</span>
<span>
{item.title
.split(' ')
.map((word, index) => (
<div key={index}>
{word}
</div>
))}
</span>
</button>
</SidebarMenuButton>
</SidebarMenuItem>
@@ -250,10 +253,9 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
</span>
)}
<SidebarMenuButton
className="hover:bg-gray-200 data-[active=true]:bg-gray-100 data-[active=true]:text-pink-600 data-[active=true]:hover:bg-pink-100 dark:hover:bg-gray-800 dark:data-[active=true]:bg-gray-900 dark:data-[active=true]:text-pink-400 dark:data-[active=true]:hover:bg-pink-950"
className="justify-center space-y-0.5 !px-0 hover:bg-gray-200 data-[active=true]:bg-gray-100 data-[active=true]:text-pink-600 data-[active=true]:hover:bg-pink-100 dark:hover:bg-gray-800 dark:data-[active=true]:bg-gray-900 dark:data-[active=true]:text-pink-400 dark:data-[active=true]:hover:bg-pink-950"
isActive={item.active}
asChild
tooltip={item.title}
>
<button onClick={item.onClick}>
<item.icon />

View File

@@ -35,6 +35,7 @@ import {
import { Badge } from '@/components/badge/badge';
import { checkIfCustomTypeUsed } from '../utils';
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
import { defaultSchemas } from '@/lib/data/default-schemas';
export interface CustomTypeListItemHeaderProps {
customType: DBCustomType;
@@ -49,6 +50,7 @@ export const CustomTypeListItemHeader: React.FC<
highlightedCustomType,
highlightCustomTypeId,
tables,
databaseType,
} = useChartDB();
const { schemasDisplayed } = useDiagramFilter();
const { t } = useTranslation();
@@ -163,9 +165,9 @@ export const CustomTypeListItemHeader: React.FC<
const schemaToDisplay = useMemo(() => {
if (schemasDisplayed.length > 1) {
return customType.schema;
return customType.schema ?? defaultSchemas[databaseType];
}
}, [customType.schema, schemasDisplayed.length]);
}, [customType.schema, schemasDisplayed.length, databaseType]);
return (
<div className="group flex h-11 flex-1 items-center justify-between gap-1 overflow-hidden">

View File

@@ -1,7 +1,11 @@
import React from 'react';
import React, { useCallback, useMemo } from 'react';
import { Ellipsis, Trash2 } from 'lucide-react';
import { Button } from '@/components/button/button';
import type { DBIndex } from '@/lib/domain/db-index';
import {
databaseIndexTypes,
type DBIndex,
type IndexType,
} from '@/lib/domain/db-index';
import type { DBField } from '@/lib/domain/db-field';
import {
Popover,
@@ -20,6 +24,7 @@ import {
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import { useChartDB } from '@/hooks/use-chartdb';
export interface TableIndexProps {
index: DBIndex;
@@ -28,6 +33,11 @@ export interface TableIndexProps {
fields: DBField[];
}
const allIndexTypeOptions: { label: string; value: IndexType }[] = [
{ label: 'B-tree (default)', value: 'btree' },
{ label: 'Hash', value: 'hash' },
];
export const TableIndex: React.FC<TableIndexProps> = ({
fields,
index,
@@ -35,14 +45,51 @@ export const TableIndex: React.FC<TableIndexProps> = ({
removeIndex,
}) => {
const { t } = useTranslation();
const { databaseType } = useChartDB();
const fieldOptions = fields.map((field) => ({
label: field.name,
value: field.id,
}));
const updateIndexFields = (fieldIds: string | string[]) => {
const ids = Array.isArray(fieldIds) ? fieldIds : [fieldIds];
updateIndex({ fieldIds: ids });
};
const updateIndexFields = useCallback(
(fieldIds: string | string[]) => {
const ids = Array.isArray(fieldIds) ? fieldIds : [fieldIds];
// For hash indexes, only keep the last selected field
if (index.type === 'hash' && ids.length > 0) {
updateIndex({ fieldIds: [ids[ids.length - 1]] });
} else {
updateIndex({ fieldIds: ids });
}
},
[index.type, updateIndex]
);
const indexTypeOptions = useMemo(
() =>
allIndexTypeOptions.filter((option) =>
databaseIndexTypes[databaseType]?.includes(option.value)
),
[databaseType]
);
const updateIndexType = useCallback(
(value: string | string[]) => {
{
const newType = value as IndexType;
// If switching to hash and multiple fields are selected, keep only the first
if (newType === 'hash' && index.fieldIds.length > 1) {
updateIndex({
type: newType,
fieldIds: [index.fieldIds[0]],
});
} else {
updateIndex({ type: newType });
}
}
},
[updateIndex, index.fieldIds]
);
return (
<div className="flex flex-1 flex-row justify-between gap-2 p-1">
<SelectBox
@@ -135,6 +182,23 @@ export const TableIndex: React.FC<TableIndexProps> = ({
}
/>
</div>
{indexTypeOptions.length > 0 ? (
<div className="mt-2 flex flex-col gap-2">
<Label
htmlFor="indexType"
className="text-subtitle"
>
{t(
'side_panel.tables_section.table.index_actions.index_type'
)}
</Label>
<SelectBox
options={indexTypeOptions}
value={index.type || 'btree'}
onChange={updateIndexType}
/>
</div>
) : null}
<Separator orientation="horizontal" />
<Button
variant="outline"

View File

@@ -268,9 +268,9 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
const schemaToDisplay = useMemo(() => {
if (schemasDisplayed.length > 1) {
return table.schema;
return table.schema ?? defaultSchemas[databaseType];
}
}, [table.schema, schemasDisplayed.length]);
}, [table.schema, schemasDisplayed.length, databaseType]);
useEffect(() => {
if (table.name.trim()) {

View File

@@ -156,13 +156,13 @@ export const Menu: React.FC<MenuProps> = () => {
return (
<Menubar className="h-8 border-none py-2 shadow-none md:h-10 md:py-0">
<MenubarMenu>
<MenubarTrigger>{t('menu.file.file')}</MenubarTrigger>
<MenubarTrigger>{t('menu.databases.databases')}</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={createNewDiagram}>
{t('menu.file.new')}
{t('menu.databases.new')}
</MenubarItem>
<MenubarItem onClick={openDiagram}>
{t('menu.file.open')}
{t('menu.databases.browse')}
<MenubarShortcut>
{
keyboardShortcutsForOS[
@@ -172,7 +172,7 @@ export const Menu: React.FC<MenuProps> = () => {
</MenubarShortcut>
</MenubarItem>
<MenubarItem onClick={updateDiagramUpdatedAt}>
{t('menu.file.save')}
{t('menu.databases.save')}
<MenubarShortcut>
{
keyboardShortcutsForOS[
@@ -184,7 +184,7 @@ export const Menu: React.FC<MenuProps> = () => {
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
{t('menu.file.import')}
{t('menu.databases.import')}
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem onClick={openImportDiagramDialog}>
@@ -253,7 +253,7 @@ export const Menu: React.FC<MenuProps> = () => {
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
{t('menu.file.export_sql')}
{t('menu.databases.export_sql')}
</MenubarSubTrigger>
<MenubarSubContent>
{databaseType === DatabaseType.GENERIC ? (
@@ -336,7 +336,7 @@ export const Menu: React.FC<MenuProps> = () => {
</MenubarSub>
<MenubarSub>
<MenubarSubTrigger>
{t('menu.file.export_as')}
{t('menu.databases.export_as')}
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem onClick={exportPNG}>PNG</MenubarItem>
@@ -362,10 +362,8 @@ export const Menu: React.FC<MenuProps> = () => {
})
}
>
{t('menu.file.delete_diagram')}
{t('menu.databases.delete_diagram')}
</MenubarItem>
<MenubarSeparator />
<MenubarItem>{t('menu.file.exit')}</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>