224 lines
5.8 KiB
JavaScript
224 lines
5.8 KiB
JavaScript
import { useEffect, useState, useRef } from "react";
|
|
import { Cardinality } from "../data/constants";
|
|
import { calcPath } from "../utils/calcPath";
|
|
|
|
function Table({ table, grab }) {
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
const [hoveredField, setHoveredField] = useState(-1);
|
|
const height = table.fields.length * 36 + 50 + 7;
|
|
return (
|
|
<foreignObject
|
|
key={table.name}
|
|
x={table.x}
|
|
y={table.y}
|
|
width={200}
|
|
height={height}
|
|
className="drop-shadow-lg rounded-md cursor-move"
|
|
onMouseDown={grab}
|
|
onMouseEnter={() => setIsHovered(true)}
|
|
onMouseLeave={() => setIsHovered(false)}
|
|
>
|
|
<div
|
|
className={`border-2 ${
|
|
isHovered ? "border-dashed border-blue-500" : "border-zinc-300"
|
|
} select-none rounded-lg w-full bg-zinc-100 text-zinc-800`}
|
|
>
|
|
<div
|
|
className={`h-[10px] w-full rounded-t-md`}
|
|
style={{ backgroundColor: table.color }}
|
|
/>
|
|
<div className="font-bold h-[40px] flex justify-between items-center border-b border-zinc-400 bg-zinc-200 px-3">
|
|
{table.name}
|
|
</div>
|
|
{table.fields.map((e, i) => (
|
|
<div
|
|
key={i}
|
|
className={`${
|
|
i === table.fields.length - 1 ? "" : "border-b border-gray-400"
|
|
} h-[36px] px-2 py-1 flex justify-between`}
|
|
onMouseEnter={() => setHoveredField(i)}
|
|
onMouseLeave={() => setHoveredField(-1)}
|
|
>
|
|
<div className={hoveredField === i ? "text-zinc-500" : ""}>
|
|
<button
|
|
className={`w-[9px] h-[9px] bg-[#2f68ad] rounded-full me-2`}
|
|
/>
|
|
{e.name}
|
|
</div>
|
|
<div className="text-zinc-400">{e.type}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</foreignObject>
|
|
);
|
|
}
|
|
|
|
function Relationship({ relationship, tables }) {
|
|
const pathRef = useRef();
|
|
let start = { x: 0, y: 0 };
|
|
let end = { x: 0, y: 0 };
|
|
|
|
let cardinalityStart = "1";
|
|
let cardinalityEnd = "1";
|
|
|
|
switch (relationship.cardinality) {
|
|
case Cardinality.MANY_TO_ONE:
|
|
cardinalityStart = "n";
|
|
cardinalityEnd = "1";
|
|
break;
|
|
case Cardinality.ONE_TO_MANY:
|
|
cardinalityStart = "1";
|
|
cardinalityEnd = "n";
|
|
break;
|
|
case Cardinality.ONE_TO_ONE:
|
|
cardinalityStart = "1";
|
|
cardinalityEnd = "1";
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
const length = 32;
|
|
|
|
const [refAquired, setRefAquired] = useState(false);
|
|
useEffect(() => {
|
|
setRefAquired(true);
|
|
}, []);
|
|
|
|
if (refAquired) {
|
|
const pathLength = pathRef.current.getTotalLength();
|
|
const point1 = pathRef.current.getPointAtLength(length);
|
|
start = { x: point1.x, y: point1.y };
|
|
const point2 = pathRef.current.getPointAtLength(pathLength - length);
|
|
end = { x: point2.x, y: point2.y };
|
|
}
|
|
|
|
return (
|
|
<g className="select-none" onClick={() => console.log(pathRef.current)}>
|
|
<path
|
|
ref={pathRef}
|
|
d={calcPath({
|
|
...relationship,
|
|
startTable: {
|
|
x: tables[relationship.startTableId].x,
|
|
y: tables[relationship.startTableId].y,
|
|
},
|
|
endTable: {
|
|
x: tables[relationship.endTableId].x,
|
|
y: tables[relationship.endTableId].y,
|
|
},
|
|
})}
|
|
stroke="gray"
|
|
fill="none"
|
|
strokeWidth={2}
|
|
/>
|
|
{pathRef.current && (
|
|
<>
|
|
<circle cx={start.x} cy={start.y} r="12" fill="grey" />
|
|
<text
|
|
x={start.x}
|
|
y={start.y}
|
|
fill="white"
|
|
strokeWidth="0.5"
|
|
textAnchor="middle"
|
|
alignmentBaseline="middle"
|
|
>
|
|
{cardinalityStart}
|
|
</text>
|
|
<circle cx={end.x} cy={end.y} r="12" fill="grey" />
|
|
<text
|
|
x={end.x}
|
|
y={end.y}
|
|
fill="white"
|
|
strokeWidth="0.5"
|
|
textAnchor="middle"
|
|
alignmentBaseline="middle"
|
|
>
|
|
{cardinalityEnd}
|
|
</text>
|
|
</>
|
|
)}
|
|
</g>
|
|
);
|
|
}
|
|
|
|
export default function SimpleCanvas({ diagram, zoom }) {
|
|
const [tables, setTables] = useState(diagram.tables);
|
|
const [dragging, setDragging] = useState(-1);
|
|
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
|
|
|
const grabTable = (e, id) => {
|
|
setDragging(id);
|
|
setOffset({
|
|
x: e.clientX - tables[id].x,
|
|
y: e.clientY - tables[id].y,
|
|
});
|
|
};
|
|
|
|
const moveTable = (e) => {
|
|
if (dragging !== -1) {
|
|
const dx = e.clientX - offset.x;
|
|
const dy = e.clientY - offset.y;
|
|
setTables((prev) =>
|
|
prev.map((table, i) =>
|
|
i === dragging ? { ...table, x: dx, y: dy } : table,
|
|
),
|
|
);
|
|
}
|
|
};
|
|
|
|
const releaseTable = () => {
|
|
setDragging(-1);
|
|
setOffset({ x: 0, y: 0 });
|
|
};
|
|
|
|
return (
|
|
<svg
|
|
className="w-full h-full cursor-grab"
|
|
onMouseUp={releaseTable}
|
|
onMouseMove={moveTable}
|
|
onMouseLeave={releaseTable}
|
|
>
|
|
<defs>
|
|
<pattern
|
|
id="pattern-circles"
|
|
x="0"
|
|
y="0"
|
|
width="22"
|
|
height="22"
|
|
patternUnits="userSpaceOnUse"
|
|
patternContentUnits="userSpaceOnUse"
|
|
>
|
|
<circle
|
|
id="pattern-circle"
|
|
cx="4"
|
|
cy="4"
|
|
r="0.85"
|
|
fill="rgb(99, 152, 191)"
|
|
></circle>
|
|
</pattern>
|
|
</defs>
|
|
<rect
|
|
x="0"
|
|
y="0"
|
|
width="100%"
|
|
height="100%"
|
|
fill="url(#pattern-circles)"
|
|
></rect>
|
|
<g
|
|
style={{
|
|
transform: `scale(${zoom})`,
|
|
transformOrigin: "top left",
|
|
}}
|
|
>
|
|
{diagram.relationships.map((r, i) => (
|
|
<Relationship key={i} relationship={r} tables={tables} />
|
|
))}
|
|
{tables.map((t, i) => (
|
|
<Table key={i} table={t} grab={(e) => grabTable(e, i)} />
|
|
))}
|
|
</g>
|
|
</svg>
|
|
);
|
|
}
|