admin ui: store data canvas positions

This commit is contained in:
dswbx
2025-03-18 12:39:23 +01:00
parent f6996b1953
commit 3180037b67
6 changed files with 117 additions and 44 deletions
+5 -3
View File
@@ -22,7 +22,8 @@ type CanvasProps = ReactFlowProps & {
onDropNewEdge?: (base: any) => any;
};
export function Canvas({
export function Canvas(
{
nodes: _nodes,
edges: _edges,
externalProvider,
@@ -32,7 +33,9 @@ export function Canvas({
onDropNewNode,
onDropNewEdge,
...props
}: CanvasProps) {
}: CanvasProps,
ref?: any,
) {
const [nodes, setNodes, onNodesChange] = useNodesState(_nodes ?? []);
const [edges, setEdges, onEdgesChange] = useEdgesState(_edges ?? []);
const { screenToFlowPosition } = useReactFlow();
@@ -176,7 +179,6 @@ export function Canvas({
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodesConnectable={false}
/*panOnDrag={isSpacePressed}*/
panOnDrag={true}
zoomOnScroll={isCommandPressed}
panOnScroll={!isCommandPressed}
@@ -44,8 +44,10 @@ export const layoutWithDagre = ({ nodes, edges, graph }: LayoutProps) => {
const position = dagreGraph.node(node.id);
return {
...node,
position: {
x: position.x - (node.width ?? 0) / 2,
y: position.y - (node.height ?? 0) / 2,
},
};
}),
edges,
@@ -35,7 +35,6 @@ export function Panels({ children, ...props }: PanelsProps) {
)}
<Panel unstyled position="bottom-right">
{props.zoom && (
<>
<Panel.Wrapper className="px-1.5">
<Panel.IconButton Icon={TbPlus} round onClick={handleZoomIn} />
<Panel.Text className="px-2" mono onClick={handleZoomReset}>
@@ -44,7 +43,6 @@ export function Panels({ children, ...props }: PanelsProps) {
<Panel.IconButton Icon={TbMinus} round onClick={handleZoomOut} />
<Panel.IconButton Icon={TbMaximize} round onClick={handleZoomReset} />
</Panel.Wrapper>
</>
)}
{props.minimap && (
<>
@@ -6,6 +6,10 @@ import { layoutWithDagre } from "ui/components/canvas/layouts";
import { Panels } from "ui/components/canvas/panels";
import { EntityTableNode } from "./EntityTableNode";
import { useTheme } from "ui/client/use-theme";
import { Panel } from "ui/components/canvas/panels/Panel";
import { TbLayout } from "react-icons/tb";
import { useState } from "react";
import { type CanvasPosition, dataCanvasStore } from "ui/store";
function entitiesToNodes(entities: AppDataConfig["entities"]): Node<TAppDataEntity>[] {
return Object.entries(entities ?? {}).map(([name, entity]) => {
@@ -65,12 +69,45 @@ const nodeTypes = {
entity: EntityTableNode.Component,
} as const;
function getNodeAutoLayout(nodes: Node<TAppDataEntity>[], edges: any[]): CanvasPosition[] {
const nodeLayout = layoutWithDagre({
nodes: nodes.map((n) => ({
id: n.id,
...EntityTableNode.getSize(n.data),
})),
edges,
graph: {
rankdir: "LR",
marginx: 50,
marginy: 50,
},
});
return nodeLayout.nodes.map((n) => ({
id: n.id,
...n.position,
}));
}
function setNodesLayout(nodes: Node<TAppDataEntity>[], layout: CanvasPosition[]) {
return nodes.map((node) => {
const pos = layout.find((l) => l.id === node.id);
if (pos) {
return {
...node,
position: { x: pos.x, y: pos.y },
};
}
return node;
});
}
export function DataSchemaCanvas() {
const {
config: { data },
} = useBknd();
const { theme } = useTheme();
const nodes = entitiesToNodes(data.entities);
const edges = relationsToEdges(data.relations).map((e) => ({
...e,
style: {
@@ -85,31 +122,35 @@ export function DataSchemaCanvas() {
},
}));
const nodeLayout = layoutWithDagre({
nodes: nodes.map((n) => ({
id: n.id,
...EntityTableNode.getSize(n.data),
})),
edges,
graph: {
rankdir: "LR",
marginx: 50,
marginy: 50,
},
});
const entityNodes = entitiesToNodes(data.entities);
const positions = dataCanvasStore((state) => state.positions);
const setPositions = dataCanvasStore((state) => state.setPositions);
const resetPositions = dataCanvasStore((state) => state.reset);
nodeLayout.nodes.forEach((node) => {
const n = nodes.find((n) => n.id === node.id);
if (n) {
n.position = { x: node.x, y: node.y };
const layout = positions ? positions : getNodeAutoLayout(entityNodes, edges);
const [nodes, setNodes] = useState<Node<TAppDataEntity>[]>(setNodesLayout(entityNodes, layout));
function setLayout(positions: CanvasPosition[] = getNodeAutoLayout(entityNodes, edges)) {
setNodes(setNodesLayout(entityNodes, positions));
}
function resetLayout() {
resetPositions();
setLayout();
}
});
return (
<ReactFlowProvider>
<Canvas
nodes={nodes}
edges={edges}
onNodeDragStop={(e, node) => {
const positions = nodes
.map((n) => (n.id === node.id ? node : n))
.map((n) => ({ id: n.id, ...n.position }));
setPositions(positions);
setLayout(positions);
}}
nodeTypes={nodeTypes}
minZoom={0.1}
maxZoom={2}
@@ -118,7 +159,11 @@ export function DataSchemaCanvas() {
maxZoom: 0.8,
}}
>
<Panels zoom minimap />
<Panels zoom minimap>
<Panel position="bottom-left">
<Panel.IconButton round Icon={TbLayout} onClick={resetLayout} />
</Panel>
</Panels>
</Canvas>
</ReactFlowProvider>
);
+25
View File
@@ -0,0 +1,25 @@
import { create } from "zustand";
import { combine, persist } from "zustand/middleware";
export type CanvasPosition = {
id: string;
x: number;
y: number;
};
export const dataCanvasStore = create(
persist(
combine(
{
positions: null as CanvasPosition[] | null,
},
(set) => ({
setPositions: (positions: CanvasPosition[]) => set(() => ({ positions })),
reset: () => set(() => ({ positions: null })),
}),
),
{
name: "datacanvas",
},
),
);
+1
View File
@@ -1 +1,2 @@
export { appShellStore } from "./appshell";
export { dataCanvasStore, type CanvasPosition } from "./datacanvas";