fix: data modeling rendering
This commit is contained in:
Generated
+191
@@ -11,10 +11,13 @@
|
||||
"@base-ui/react": "^1.3.0",
|
||||
"@fontsource-variable/geist": "^5.2.8",
|
||||
"@tailwindcss/postcss": "^4.2.1",
|
||||
"@types/dagre": "^0.7.54",
|
||||
"@types/react-grid-layout": "^1.3.6",
|
||||
"@xyflow/react": "^12.10.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"dagre": "^0.8.5",
|
||||
"lucide-react": "^0.577.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
@@ -2964,6 +2967,15 @@
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"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": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||
@@ -2994,6 +3006,12 @@
|
||||
"@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": {
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.8.tgz",
|
||||
@@ -3015,6 +3033,31 @@
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||
"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": {
|
||||
"version": "4.1.12",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz",
|
||||
@@ -3934,6 +4037,12 @@
|
||||
"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": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
||||
@@ -4280,6 +4389,19 @@
|
||||
"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": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-3.0.1.tgz",
|
||||
@@ -4468,6 +4590,15 @@
|
||||
"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": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||
@@ -4513,6 +4644,51 @@
|
||||
"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": {
|
||||
"version": "4.0.1",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmmirror.com/graphql/-/graphql-16.13.1.tgz",
|
||||
@@ -6740,6 +6925,12 @@
|
||||
"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": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
|
||||
@@ -13,10 +13,13 @@
|
||||
"@base-ui/react": "^1.3.0",
|
||||
"@fontsource-variable/geist": "^5.2.8",
|
||||
"@tailwindcss/postcss": "^4.2.1",
|
||||
"@types/dagre": "^0.7.54",
|
||||
"@types/react-grid-layout": "^1.3.6",
|
||||
"@xyflow/react": "^12.10.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"dagre": "^0.8.5",
|
||||
"lucide-react": "^0.577.0",
|
||||
"react": "^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 { 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 { Button } from "../components/ui/button";
|
||||
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 { Dialog, DialogContent, DialogHeader, DialogTitle } from "../components/ui/dialog";
|
||||
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 {
|
||||
[table: string]: { name: string; type: string }[];
|
||||
@@ -66,6 +70,61 @@ interface ModelDetailResponse {
|
||||
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() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
@@ -80,10 +139,104 @@ export function Modeling() {
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
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(() => {
|
||||
fetchInitialData();
|
||||
}, [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 tableNames = Object.keys(schemaRes);
|
||||
const columnsMap: Record<string, string[]> = {};
|
||||
@@ -245,6 +398,9 @@ export function Modeling() {
|
||||
</div>
|
||||
{step === "view" && (
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleAutoLayout}>
|
||||
Auto Layout
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleReselectTables}>
|
||||
Reselect Tables
|
||||
</Button>
|
||||
@@ -371,56 +527,23 @@ export function Modeling() {
|
||||
</ScrollArea>
|
||||
</Card>
|
||||
|
||||
{/* Canvas Area (Simulated) */}
|
||||
<div className="flex-1 overflow-auto bg-slate-100 rounded-lg border p-8 relative">
|
||||
<div className="absolute inset-0 pointer-events-none"
|
||||
style={{
|
||||
backgroundImage: 'radial-gradient(#cbd5e1 1px, transparent 1px)',
|
||||
backgroundSize: '20px 20px'
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 relative z-10">
|
||||
{mdl?.models.map((model) => (
|
||||
<Card key={model.name} className="shadow-md border-t-4 border-t-blue-500 min-w-[240px] cursor-pointer" onClick={() => openModelDetail(model.name)}>
|
||||
<CardHeader className="py-3 px-4 bg-gray-50 border-b flex flex-row items-center justify-between">
|
||||
<div className="font-semibold text-sm flex items-center gap-2">
|
||||
<TableIcon className="w-4 h-4 text-blue-500" />
|
||||
{model.name}
|
||||
</div>
|
||||
</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>
|
||||
{/* Canvas Area (ReactFlow) */}
|
||||
<div className="flex-1 overflow-hidden bg-slate-50 rounded-lg border relative">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
minZoom={0.1}
|
||||
maxZoom={1.5}
|
||||
attributionPosition="bottom-right"
|
||||
>
|
||||
<Background color="#cbd5e1" gap={20} size={1} />
|
||||
<Controls />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user