mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 08:06:00 +00:00
admin ui: store data canvas positions
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 +1,2 @@
|
||||
export { appShellStore } from "./appshell";
|
||||
export { dataCanvasStore, type CanvasPosition } from "./datacanvas";
|
||||
|
||||
Reference in New Issue
Block a user