fix: data modeling rendering
This commit is contained in:
Generated
+191
@@ -11,10 +11,13 @@
|
|||||||
"@base-ui/react": "^1.3.0",
|
"@base-ui/react": "^1.3.0",
|
||||||
"@fontsource-variable/geist": "^5.2.8",
|
"@fontsource-variable/geist": "^5.2.8",
|
||||||
"@tailwindcss/postcss": "^4.2.1",
|
"@tailwindcss/postcss": "^4.2.1",
|
||||||
|
"@types/dagre": "^0.7.54",
|
||||||
"@types/react-grid-layout": "^1.3.6",
|
"@types/react-grid-layout": "^1.3.6",
|
||||||
|
"@xyflow/react": "^12.10.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
"dagre": "^0.8.5",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
@@ -2964,6 +2967,15 @@
|
|||||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-drag": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/d3-ease": {
|
"node_modules/@types/d3-ease": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||||
@@ -2994,6 +3006,12 @@
|
|||||||
"@types/d3-time": "*"
|
"@types/d3-time": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-selection": {
|
||||||
|
"version": "3.0.11",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
||||||
|
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/d3-shape": {
|
"node_modules/@types/d3-shape": {
|
||||||
"version": "3.1.8",
|
"version": "3.1.8",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.8.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.8.tgz",
|
||||||
@@ -3015,6 +3033,31 @@
|
|||||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-transition": {
|
||||||
|
"version": "3.0.9",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
||||||
|
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-zoom": {
|
||||||
|
"version": "3.0.8",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
|
||||||
|
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-interpolate": "*",
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/dagre": {
|
||||||
|
"version": "0.7.54",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/dagre/-/dagre-0.7.54.tgz",
|
||||||
|
"integrity": "sha512-QjcRY+adGbYvBFS7cwv5txhVIwX1XXIUswWl+kSQTbI6NjgZydrZkEKX/etzVd7i+bCsCb40Z/xlBY5eoFuvWQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/debug": {
|
"node_modules/@types/debug": {
|
||||||
"version": "4.1.12",
|
"version": "4.1.12",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.12.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.12.tgz",
|
||||||
@@ -3460,6 +3503,66 @@
|
|||||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@xyflow/react": {
|
||||||
|
"version": "12.10.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@xyflow/react/-/react-12.10.1.tgz",
|
||||||
|
"integrity": "sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@xyflow/system": "0.0.75",
|
||||||
|
"classcat": "^5.0.3",
|
||||||
|
"zustand": "^4.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=17",
|
||||||
|
"react-dom": ">=17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xyflow/react/node_modules/zustand": {
|
||||||
|
"version": "4.5.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.7.tgz",
|
||||||
|
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"use-sync-external-store": "^1.2.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.7.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=16.8",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=16.8"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xyflow/system": {
|
||||||
|
"version": "0.0.75",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@xyflow/system/-/system-0.0.75.tgz",
|
||||||
|
"integrity": "sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-drag": "^3.0.7",
|
||||||
|
"@types/d3-interpolate": "^3.0.4",
|
||||||
|
"@types/d3-selection": "^3.0.10",
|
||||||
|
"@types/d3-transition": "^3.0.8",
|
||||||
|
"@types/d3-zoom": "^3.0.8",
|
||||||
|
"d3-drag": "^3.0.0",
|
||||||
|
"d3-interpolate": "^3.0.1",
|
||||||
|
"d3-selection": "^3.0.0",
|
||||||
|
"d3-zoom": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz",
|
||||||
@@ -3934,6 +4037,12 @@
|
|||||||
"url": "https://polar.sh/cva"
|
"url": "https://polar.sh/cva"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/classcat": {
|
||||||
|
"version": "5.0.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/classcat/-/classcat-5.0.5.tgz",
|
||||||
|
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cli-cursor": {
|
"node_modules/cli-cursor": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
||||||
@@ -4280,6 +4389,19 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-drag": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-selection": "3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/d3-dsv": {
|
"node_modules/d3-dsv": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-3.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-3.0.1.tgz",
|
||||||
@@ -4468,6 +4590,15 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-selection": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/d3-shape": {
|
"node_modules/d3-shape": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||||
@@ -4513,6 +4644,51 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-transition": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/d3-transition/-/d3-transition-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3",
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-ease": "1 - 3",
|
||||||
|
"d3-interpolate": "1 - 3",
|
||||||
|
"d3-timer": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"d3-selection": "2 - 3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-zoom": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-drag": "2 - 3",
|
||||||
|
"d3-interpolate": "1 - 3",
|
||||||
|
"d3-selection": "2 - 3",
|
||||||
|
"d3-transition": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dagre": {
|
||||||
|
"version": "0.8.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/dagre/-/dagre-0.8.5.tgz",
|
||||||
|
"integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graphlib": "^2.1.8",
|
||||||
|
"lodash": "^4.17.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/data-uri-to-buffer": {
|
"node_modules/data-uri-to-buffer": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||||
@@ -5715,6 +5891,15 @@
|
|||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/graphlib": {
|
||||||
|
"version": "2.1.8",
|
||||||
|
"resolved": "https://registry.npmmirror.com/graphlib/-/graphlib-2.1.8.tgz",
|
||||||
|
"integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": "^4.17.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/graphql": {
|
"node_modules/graphql": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmmirror.com/graphql/-/graphql-16.13.1.tgz",
|
"resolved": "https://registry.npmmirror.com/graphql/-/graphql-16.13.1.tgz",
|
||||||
@@ -6740,6 +6925,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.23",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.23.tgz",
|
||||||
|
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
|
|||||||
@@ -13,10 +13,13 @@
|
|||||||
"@base-ui/react": "^1.3.0",
|
"@base-ui/react": "^1.3.0",
|
||||||
"@fontsource-variable/geist": "^5.2.8",
|
"@fontsource-variable/geist": "^5.2.8",
|
||||||
"@tailwindcss/postcss": "^4.2.1",
|
"@tailwindcss/postcss": "^4.2.1",
|
||||||
|
"@types/dagre": "^0.7.54",
|
||||||
"@types/react-grid-layout": "^1.3.6",
|
"@types/react-grid-layout": "^1.3.6",
|
||||||
|
"@xyflow/react": "^12.10.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
"dagre": "^0.8.5",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { memo } from "react";
|
||||||
|
import { Handle, Position } from "@xyflow/react";
|
||||||
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||||
|
import { Table as TableIcon } from "lucide-react";
|
||||||
|
|
||||||
|
interface Column {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableNodeData {
|
||||||
|
name: string;
|
||||||
|
columns: Column[];
|
||||||
|
onDetailClick: (name: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TableNode = memo(({ data }: { data: TableNodeData }) => {
|
||||||
|
return (
|
||||||
|
<Card className="min-w-[180px] max-w-[240px] shadow-md border-t-4 border-t-blue-500 text-xs">
|
||||||
|
<Handle type="target" position={Position.Top} className="!bg-blue-500" />
|
||||||
|
|
||||||
|
<CardHeader
|
||||||
|
className="py-2 px-3 bg-gray-50 border-b flex flex-row items-center justify-between cursor-pointer hover:bg-gray-100"
|
||||||
|
onClick={() => data.onDetailClick(data.name)}
|
||||||
|
>
|
||||||
|
<div className="font-semibold flex items-center gap-2 truncate" title={data.name}>
|
||||||
|
<TableIcon className="w-3 h-3 text-blue-500 shrink-0" />
|
||||||
|
<span className="truncate">{data.name}</span>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="max-h-[200px] overflow-y-auto">
|
||||||
|
{data.columns.map((col) => (
|
||||||
|
<div
|
||||||
|
key={col.name}
|
||||||
|
className="py-1.5 px-3 border-b last:border-0 hover:bg-gray-50 flex items-center"
|
||||||
|
title={`${col.name} (${col.type})`}
|
||||||
|
>
|
||||||
|
<span className="font-medium truncate">{col.name}</span>
|
||||||
|
{/* 类型列已被隐藏 */}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<Handle type="source" position={Position.Bottom} className="!bg-blue-500" />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
});
|
||||||
+175
-52
@@ -1,5 +1,8 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo, useCallback } from "react";
|
||||||
import { useParams, useNavigate } from "react-router-dom";
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
|
import { ReactFlow, Background, Controls, useNodesState, useEdgesState, MarkerType, type Node, type Edge, ConnectionLineType } from "@xyflow/react";
|
||||||
|
import "@xyflow/react/dist/style.css";
|
||||||
|
import dagre from "dagre";
|
||||||
import { api } from "../lib/api";
|
import { api } from "../lib/api";
|
||||||
import { Button } from "../components/ui/button";
|
import { Button } from "../components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
|
||||||
@@ -7,7 +10,8 @@ import { Label } from "../components/ui/label";
|
|||||||
import { ScrollArea } from "../components/ui/scroll-area";
|
import { ScrollArea } from "../components/ui/scroll-area";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../components/ui/dialog";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
||||||
import { ArrowLeft, Table as TableIcon, Network } from "lucide-react";
|
import { ArrowLeft, Table as TableIcon } from "lucide-react";
|
||||||
|
import { TableNode } from "../components/modeling/TableNode";
|
||||||
|
|
||||||
interface RawSchema {
|
interface RawSchema {
|
||||||
[table: string]: { name: string; type: string }[];
|
[table: string]: { name: string; type: string }[];
|
||||||
@@ -66,6 +70,61 @@ interface ModelDetailResponse {
|
|||||||
preview_rows: Record<string, unknown>[];
|
preview_rows: Record<string, unknown>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dagreGraph = new dagre.graphlib.Graph();
|
||||||
|
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
||||||
|
|
||||||
|
const getLayoutedElements = (nodes: Node[], edges: Edge[]) => {
|
||||||
|
// If there are few or no edges, use grid layout to spread out nodes
|
||||||
|
if (edges.length === 0 || edges.length < nodes.length * 0.3) {
|
||||||
|
const COLUMNS = 4;
|
||||||
|
const ROW_HEIGHT = 400; // Height per row including spacing
|
||||||
|
const COL_WIDTH = 300; // Width per column including spacing
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodes: nodes.map((node, index) => {
|
||||||
|
const col = index % COLUMNS;
|
||||||
|
const row = Math.floor(index / COLUMNS);
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
position: {
|
||||||
|
x: col * COL_WIDTH,
|
||||||
|
y: row * ROW_HEIGHT,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
edges,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise use Dagre for connected graphs
|
||||||
|
dagreGraph.setGraph({ rankdir: 'TB', nodesep: 100, ranksep: 120 });
|
||||||
|
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
// Estimating height based on column count
|
||||||
|
const height = 50 + (node.data.columns as Column[]).length * 28;
|
||||||
|
dagreGraph.setNode(node.id, { width: 240, height });
|
||||||
|
});
|
||||||
|
|
||||||
|
edges.forEach((edge) => {
|
||||||
|
dagreGraph.setEdge(edge.source, edge.target);
|
||||||
|
});
|
||||||
|
|
||||||
|
dagre.layout(dagreGraph);
|
||||||
|
|
||||||
|
const layoutedNodes = nodes.map((node) => {
|
||||||
|
const nodeWithPosition = dagreGraph.node(node.id);
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
position: {
|
||||||
|
x: nodeWithPosition.x - 120, // center offset (width/2)
|
||||||
|
y: nodeWithPosition.y - (nodeWithPosition.height / 2),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { nodes: layoutedNodes, edges };
|
||||||
|
};
|
||||||
|
|
||||||
export function Modeling() {
|
export function Modeling() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -80,10 +139,104 @@ export function Modeling() {
|
|||||||
const [detailLoading, setDetailLoading] = useState(false);
|
const [detailLoading, setDetailLoading] = useState(false);
|
||||||
const [modelDetail, setModelDetail] = useState<ModelDetailResponse | null>(null);
|
const [modelDetail, setModelDetail] = useState<ModelDetailResponse | null>(null);
|
||||||
|
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
|
||||||
|
|
||||||
|
const nodeTypes = useMemo(() => ({ table: TableNode }), []);
|
||||||
|
|
||||||
|
// Save layout to localStorage when nodes change (dragged)
|
||||||
|
const onNodeDragStop = useCallback(() => {
|
||||||
|
if (nodes.length > 0) {
|
||||||
|
const layoutData = nodes.map(n => ({ id: n.id, position: n.position }));
|
||||||
|
localStorage.setItem(`er-layout-${id}`, JSON.stringify(layoutData));
|
||||||
|
}
|
||||||
|
}, [nodes, id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchInitialData();
|
fetchInitialData();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (step === 'view' && mdl) {
|
||||||
|
// Try to load saved layout
|
||||||
|
const savedLayoutStr = localStorage.getItem(`er-layout-${id}`);
|
||||||
|
let savedPositions: Record<string, {x: number, y: number}> = {};
|
||||||
|
|
||||||
|
if (savedLayoutStr) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(savedLayoutStr);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
parsed.forEach((item: any) => {
|
||||||
|
if (item.id && item.position) {
|
||||||
|
savedPositions[item.id] = item.position;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse saved layout", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialNodes: Node[] = mdl.models.map((model) => ({
|
||||||
|
id: model.name,
|
||||||
|
type: 'table',
|
||||||
|
position: savedPositions[model.name] || { x: 0, y: 0 },
|
||||||
|
data: {
|
||||||
|
name: model.name,
|
||||||
|
columns: model.columns,
|
||||||
|
onDetailClick: openModelDetail
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const initialEdges: Edge[] = mdl.relationships.map((rel, index) => {
|
||||||
|
// Assuming rel.models has at least 2 elements
|
||||||
|
if (rel.models.length < 2) return null;
|
||||||
|
return {
|
||||||
|
id: `e-${index}`,
|
||||||
|
source: rel.models[0],
|
||||||
|
target: rel.models[1],
|
||||||
|
type: ConnectionLineType.SmoothStep,
|
||||||
|
animated: false,
|
||||||
|
label: rel.joinType,
|
||||||
|
style: { stroke: '#94a3b8' },
|
||||||
|
labelStyle: { fill: '#64748b', fontSize: 11 },
|
||||||
|
markerEnd: {
|
||||||
|
type: MarkerType.ArrowClosed,
|
||||||
|
color: '#94a3b8',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}).filter(Boolean) as Edge[];
|
||||||
|
|
||||||
|
// Only run auto-layout if we don't have saved positions for most nodes
|
||||||
|
// or if user explicitly requests it (future feature)
|
||||||
|
const hasSavedLayout = Object.keys(savedPositions).length >= initialNodes.length * 0.5;
|
||||||
|
|
||||||
|
if (hasSavedLayout) {
|
||||||
|
setNodes(initialNodes);
|
||||||
|
setEdges(initialEdges);
|
||||||
|
} else {
|
||||||
|
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
|
||||||
|
initialNodes,
|
||||||
|
initialEdges
|
||||||
|
);
|
||||||
|
setNodes(layoutedNodes);
|
||||||
|
setEdges(layoutedEdges);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [step, mdl, id]);
|
||||||
|
|
||||||
|
const handleAutoLayout = () => {
|
||||||
|
if (!mdl) return;
|
||||||
|
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
|
||||||
|
nodes,
|
||||||
|
edges
|
||||||
|
);
|
||||||
|
setNodes([...layoutedNodes]);
|
||||||
|
setEdges([...layoutedEdges]);
|
||||||
|
// Clear saved layout to prefer auto layout
|
||||||
|
localStorage.removeItem(`er-layout-${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
const initSelectionFromSchema = (schemaRes: RawSchema) => {
|
const initSelectionFromSchema = (schemaRes: RawSchema) => {
|
||||||
const tableNames = Object.keys(schemaRes);
|
const tableNames = Object.keys(schemaRes);
|
||||||
const columnsMap: Record<string, string[]> = {};
|
const columnsMap: Record<string, string[]> = {};
|
||||||
@@ -245,6 +398,9 @@ export function Modeling() {
|
|||||||
</div>
|
</div>
|
||||||
{step === "view" && (
|
{step === "view" && (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" onClick={handleAutoLayout}>
|
||||||
|
Auto Layout
|
||||||
|
</Button>
|
||||||
<Button variant="outline" onClick={handleReselectTables}>
|
<Button variant="outline" onClick={handleReselectTables}>
|
||||||
Reselect Tables
|
Reselect Tables
|
||||||
</Button>
|
</Button>
|
||||||
@@ -371,56 +527,23 @@ export function Modeling() {
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Canvas Area (Simulated) */}
|
{/* Canvas Area (ReactFlow) */}
|
||||||
<div className="flex-1 overflow-auto bg-slate-100 rounded-lg border p-8 relative">
|
<div className="flex-1 overflow-hidden bg-slate-50 rounded-lg border relative">
|
||||||
<div className="absolute inset-0 pointer-events-none"
|
<ReactFlow
|
||||||
style={{
|
nodes={nodes}
|
||||||
backgroundImage: 'radial-gradient(#cbd5e1 1px, transparent 1px)',
|
edges={edges}
|
||||||
backgroundSize: '20px 20px'
|
onNodesChange={onNodesChange}
|
||||||
}}
|
onEdgesChange={onEdgesChange}
|
||||||
/>
|
onNodeDragStop={onNodeDragStop}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 relative z-10">
|
fitView
|
||||||
{mdl?.models.map((model) => (
|
minZoom={0.1}
|
||||||
<Card key={model.name} className="shadow-md border-t-4 border-t-blue-500 min-w-[240px] cursor-pointer" onClick={() => openModelDetail(model.name)}>
|
maxZoom={1.5}
|
||||||
<CardHeader className="py-3 px-4 bg-gray-50 border-b flex flex-row items-center justify-between">
|
attributionPosition="bottom-right"
|
||||||
<div className="font-semibold text-sm flex items-center gap-2">
|
>
|
||||||
<TableIcon className="w-4 h-4 text-blue-500" />
|
<Background color="#cbd5e1" gap={20} size={1} />
|
||||||
{model.name}
|
<Controls />
|
||||||
</div>
|
</ReactFlow>
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-0">
|
|
||||||
<div className="max-h-[300px] overflow-y-auto text-xs">
|
|
||||||
{model.columns.map((col) => (
|
|
||||||
<div key={col.name} className="flex justify-between py-2 px-4 border-b last:border-0 hover:bg-gray-50">
|
|
||||||
<span className="font-medium">{col.name}</span>
|
|
||||||
<span className="text-muted-foreground font-mono text-[10px]">{col.type}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{/* Show Relationships if any */}
|
|
||||||
{mdl.relationships.filter(r => r.models.includes(model.name)).length > 0 && (
|
|
||||||
<div className="bg-orange-50 p-2 border-t text-xs">
|
|
||||||
<div className="font-semibold text-orange-700 mb-1 flex items-center gap-1">
|
|
||||||
<Network className="w-3 h-3" /> Relationships
|
|
||||||
</div>
|
|
||||||
{mdl.relationships
|
|
||||||
.filter(r => r.models.includes(model.name))
|
|
||||||
.map(r => {
|
|
||||||
const other = r.models.find(m => m !== model.name);
|
|
||||||
return (
|
|
||||||
<div key={r.name} className="text-orange-600 truncate" title={`${r.joinType} with ${other}`}>
|
|
||||||
⟷ {other}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user