changed to controlled react state and added edit feature

pull/33/head
Khwaja 3 months ago
parent a463b8eb76
commit acd3b469bf

@ -287,53 +287,50 @@ const onAddCenterOutput = (centerName: string, productName: string) => {
}); });
}; };
const onRenamePlant = (prevName: string, newName: string) => { const onRenamePlant = (uniqueId: string, newName: string) => {
setCircularData(prev => { setCircularData(prev => {
const oldPlant = prev.plants[prevName]; const plant = prev.plants[uniqueId];
if (!oldPlant) return prev; if (!plant) return prev;
const next = {
const updatedData: CircularData = {
...prev, ...prev,
plants: { plants: {
...prev.plants, ...prev.plants,
[prevName]: {...oldPlant, name: newName} [uniqueId]: { ...plant, name: newName},
} },
}; };
return next;
}); });
}; };
const onRenameProduct = (prevName: string, newName: string) => { const onRenameProduct = (uniqueId: string, newName: string) => {
setCircularData(prev => { setCircularData(prev => {
const oldProduct = prev.products[prevName]; const product = prev.products[uniqueId];
if (!oldProduct) return prev; if (!product) return prev;
const next = {
const updatedData: CircularData = {
...prev, ...prev,
products: { products: {
...prev.products, ...prev.products,
[newName]: oldProduct, [uniqueId]: { ...product, name: newName},
} },
}; };
delete updatedData.products[prevName]; return next;
return updatedData;
}); });
}; };
const onRenameCenter = (prevName: string, newName: string) => { const onRenameCenter = (uniqueId: string, newName: string) => {
setCircularData(prev => { setCircularData(prev => {
const oldCenter = prev.centers[prevName]; const center = prev.centers[uniqueId];
if (!oldCenter) return prev; if (!center) return prev;
const next = {
const updatedData: CircularData = {
...prev, ...prev,
centers: { centers: {
...prev.centers, ...prev.centers,
[newName]: oldCenter, [uniqueId]: { ...center, name: newName},
} },
}; };
delete updatedData.centers[prevName]; return next;
return updatedData;
}); });
}; };

@ -1,21 +1,14 @@
import React, { useEffect, useCallback } from 'react'; import React, { useEffect, useCallback, useRef } from 'react';
import dagre from 'dagre'; import dagre from 'dagre';
import { import {
ReactFlow, ReactFlow, useNodesState, useEdgesState, Background,
Background, Controls, Node, Edge, Connection, MarkerType } from '@xyflow/react';
Controls,
Node,
Edge,
Connection,
MarkerType
} from '@xyflow/react';
import { CircularPlant, CircularProduct, CircularCenter } from './CircularData'; import { CircularPlant, CircularProduct, CircularCenter } from './CircularData';
import CustomNode, { CustomNodeData } from './NodesAndEdges'; import CustomNode, { CustomNodeData } from './NodesAndEdges';
import Section from '../Common/Section'; import Section from '../Common/Section';
import Card from '../Common/Card'; import Card from '../Common/Card';
import styles from './PipelineBlock.module.css'; import styles from './PipelineBlock.module.css';
interface PipelineBlockProps { interface PipelineBlockProps {
onAddPlant: () => void; onAddPlant: () => void;
onAddProduct: () => void; onAddProduct: () => void;
@ -30,68 +23,65 @@ interface PipelineBlockProps {
onRemovePlant: (id: string) => void; onRemovePlant: (id: string) => void;
onRemoveProduct: (id: string) => void; onRemoveProduct: (id: string) => void;
onRemoveCenter: (id: string) => void; onRemoveCenter: (id: string) => void;
onRenameProduct: (prevName: string, newName: string) => void; onRenameProduct: (uid: string, newName: string) => void;
onRenamePlant: (prevName: string, newName: string) => void; onRenamePlant: (uid: string, newName: string) => void;
onRenameCenter: (prevName: string, newName: string) => void; onRenameCenter: (uid: string, newName: string) => void;
products: Record<string, CircularProduct>; products: Record<string, CircularProduct>;
plants: Record<string, CircularPlant>; plants: Record<string, CircularPlant>;
centers: Record<string, CircularCenter>; centers: Record<string, CircularCenter>;
} }
function getLayoutedNodesAndEdges( function getLayouted(
nodes: Node<CustomNodeData>[], nodes: Node<CustomNodeData>[],
edges: Edge[] edges: Edge[]
): { nodes: Node<CustomNodeData>[]; edges: Edge[] } { ): { nodes: Node<CustomNodeData>[]; edges: Edge[] } {
const NODE_WIDTH = 125; const W = 125, H = 45;
const NODE_HEIGHT = 45;
const g = new dagre.graphlib.Graph(); const g = new dagre.graphlib.Graph();
g.setDefaultEdgeLabel(() => ({})); g.setDefaultEdgeLabel(() => ({}));
g.setGraph({ rankdir: 'LR' }); g.setGraph({ rankdir: 'LR' });
nodes.forEach(n => g.setNode(n.id, { width: NODE_WIDTH, height: NODE_HEIGHT })); nodes.forEach(n => g.setNode(n.id, { width: W, height: H }));
edges.forEach(e => g.setEdge(e.source, e.target)); edges.forEach(e => g.setEdge(e.source, e.target));
dagre.layout(g); dagre.layout(g);
const layouted = nodes.map(n => {
const d = g.node(n.id)!;
return { return {
...n, nodes: nodes.map(n => {
position: { const d = g.node(n.id)!;
x: d.x - NODE_WIDTH / 2, return { ...n, position: { x: d.x - W/2, y: d.y - H/2 } };
y: d.y - NODE_HEIGHT / 2 }),
} edges
}; };
});
return { nodes: layouted, edges };
} }
const PipelineBlock: React.FC<PipelineBlockProps> = props => { const PipelineBlock: React.FC<PipelineBlockProps> = props => {
const nodes: Node<CustomNodeData>[] = []; const mapRef = useRef<Record<string, 'plant'|'product'|'center'>>({});
const edges: Edge[] = []; const [nodes, setNodes, onNodesChange] = useNodesState([]);
const mapNameToType: Record<string, 'plant' | 'product' | 'center'> = {}; const [edges, setEdges, onEdgesChange] = useEdgesState([]);
let hasNullPositions = false;
Object.entries(props.products).forEach(([key, product]) => { const rebuild = useCallback(() => {
if (!product.x || !product.y) hasNullPositions = true; const m: Record<string, 'plant'|'product'|'center'> = {};
mapNameToType[key] = 'product'; const newNodes: Node<CustomNodeData>[] = [];
nodes.push({ const newEdges: Edge[] = [];
id: product.uid, Object.entries(props.products).forEach(([key, p]) => {
m[key] = 'product';
newNodes.push({
id: p.uid,
type: 'default', type: 'default',
data: { label: product.name, type: 'product' }, data: { label: p.name, type: 'product' },
position: { x: product.x, y: product.y }, position: { x: p.x, y: p.y },
className: 'ProductNode' className: 'ProductNode'
}); });
}); });
Object.entries(props.plants).forEach(([key, plant]) => {
if (!plant.x || !plant.y) hasNullPositions = true; Object.entries(props.plants).forEach(([key, pl]) => {
mapNameToType[key] = 'plant'; m[key] = 'plant';
nodes.push({ newNodes.push({
id: plant.uid, id: pl.uid,
type: 'default', type: 'default',
data: { label: plant.name, type: 'plant' }, data: { label: pl.name, type: 'plant' },
position: { x: plant.x, y: plant.y }, position: { x: pl.x, y: pl.y },
className: 'PlantNode' className: 'PlantNode'
}); });
plant.inputs.forEach(input => {
edges.push({ pl.inputs.forEach(input => {
newEdges.push({
id: `${input}-${key}-in`, id: `${input}-${key}-in`,
source: input, source: input,
target: key, target: key,
@ -100,8 +90,9 @@ const PipelineBlock: React.FC<PipelineBlockProps> = props => {
markerEnd: { type: MarkerType.ArrowClosed } markerEnd: { type: MarkerType.ArrowClosed }
}); });
}); });
plant.outputs.forEach(output => {
edges.push({ pl.outputs.forEach(output => {
newEdges.push({
id: `${key}-${output}-out`, id: `${key}-${output}-out`,
source: key, source: key,
target: output, target: output,
@ -111,84 +102,92 @@ const PipelineBlock: React.FC<PipelineBlockProps> = props => {
}); });
}); });
}); });
Object.entries(props.centers).forEach(([key, center]) => {
if (!center.x || !center.y) hasNullPositions = true; Object.entries(props.centers).forEach(([key, c]) => {
mapNameToType[key] = 'center'; m[key] = 'center';
nodes.push({ newNodes.push({
id: center.uid, id: c.uid,
type: 'default', type: 'default',
data: { label: center.name, type: 'center' }, data: { label: c.name, type: 'center' },
position: { x: center.x, y: center.y }, position: { x: c.x, y: c.y },
className: 'CenterNode' className: 'CenterNode'
}); });
if (center.input) {
edges.push({ if (c.input) {
id: `${center.input}-${key}-in`, newEdges.push({
source: center.input, id: `${c.input}-${key}-in`,
source: c.input,
target: key, target: key,
animated: true, animated: true,
style: { stroke: 'black' }, style: { stroke: 'black' },
markerEnd: { type: MarkerType.ArrowClosed } markerEnd: { type: MarkerType.ArrowClosed }
}); });
} }
center.output.forEach(out => {
edges.push({ c.output.forEach(o => {
id: `${key}-${out}-out`, newEdges.push({
id: `${key}-${o}-out`,
source: key, source: key,
target: out, target: o,
animated: true, animated: true,
style: { stroke: 'black' }, style: { stroke: 'black' },
markerEnd: { type: MarkerType.ArrowClosed } markerEnd: { type: MarkerType.ArrowClosed }
}); });
}); });
}); });
const onConnect = (params: Connection) => { mapRef.current = m;
const { source, target } = params; setNodes(newNodes);
if (!source || !target) return; setEdges(newEdges);
const sourceType = mapNameToType[source]; }, [
const targetType = mapNameToType[target];
if (sourceType === 'product' && targetType === 'plant') props.onSetPlantInput(target, source); props.products,
else if (sourceType === 'plant' && targetType === 'product') props.onAddPlantOutput(source, target); props.plants,
else if (sourceType === 'product' && targetType === 'center') props.onAddCenterInput(target, source); props.centers,
else if (sourceType === 'center' && targetType === 'product') props.onAddCenterOutput(source, target); setNodes,
setEdges
]);
useEffect(() => { rebuild(); }, [rebuild]);
const onConnect = (c: Connection) => {
const s = c.source!, t = c.target!;
const st = mapRef.current[s], tt = mapRef.current[t];
if (st==='product' && tt==='plant') props.onSetPlantInput(t, s);
else if (st==='plant' && tt==='product') props.onAddPlantOutput(s, t);
else if (st==='product' && tt==='center') props.onAddCenterInput(t, s);
else if (st==='center' && tt==='product') props.onAddCenterOutput(s, t);
}; };
const onNodeDragStop = (_: any, node: Node<CustomNodeData>) => { const onNodeDragStop = (_: any, n: Node<CustomNodeData>) => {
const { id, position, data } = node; const { id, position, data } = n;
if (data.type==='plant') props.onMovePlant(id, position.x, position.y); if (data.type==='plant') props.onMovePlant(id, position.x, position.y);
if (data.type==='product') props.onMoveProduct(id, position.x, position.y); if (data.type==='product') props.onMoveProduct(id, position.x, position.y);
if (data.type==='center') props.onMoveCenter(id, position.x, position.y); if (data.type==='center') props.onMoveCenter(id, position.x, position.y);
}; };
const handleNodesDelete = useCallback( const handleNodesDelete = useCallback((deleted: Node<CustomNodeData>[]) => {
(deleted: Node<CustomNodeData>[]) => {
deleted.forEach(n => { deleted.forEach(n => {
const type = mapNameToType[n.id]; const t = mapRef.current[n.id];
if (type === 'plant') props.onRemovePlant(n.id); if (t==='plant') props.onRemovePlant(n.id);
else if (type === 'product') props.onRemoveProduct(n.id); if (t==='product') props.onRemoveProduct(n.id);
else if (type === 'center') props.onRemoveCenter(n.id); if (t==='center') props.onRemoveCenter(n.id);
}); });
},
[props]
);
const onNodeDoubleClick = (ev: React.MouseEvent, node: Node<CustomNodeData>) => { }, [props]);
const oldName = node.data.label;
const newName = window.prompt("Enter new name", oldName); const onNodeDoubleClick = (_: React.MouseEvent, n: Node<CustomNodeData>) => {
if (!newName|| newName.trim().length === 0) return; const oldName = n.data.label;
if (newName in mapNameToType) return; const newName = window.prompt('Enter new name', oldName);
if (node.data.type === "plant") { console.log('after rename', newName);
props.onRenamePlant(oldName, newName); const uniqueId = n.id;
} else if (node.data.type === "product") { if (!newName || newName===oldName) return;
props.onRenameProduct(oldName, newName); if (n.data.type==='plant') props.onRenamePlant(uniqueId, newName);
} if (n.data.type==='product') props.onRenameProduct(uniqueId, newName);
else if (node.data.type === "center") { if (n.data.type==='center') props.onRenameCenter(uniqueId, newName);
props.onRenameCenter(oldName, newName);
}
}; };
const onLayout = () => { const onLayout = () => {
const { nodes: ln, edges: le } = getLayoutedNodesAndEdges(nodes, edges); const { nodes: ln, edges: le } = getLayouted(nodes, edges);
ln.forEach(n => { ln.forEach(n => {
const { id, position, data } = n; const { id, position, data } = n;
if (data.type==='plant') props.onMovePlant(id, position.x, position.y); if (data.type==='plant') props.onMovePlant(id, position.x, position.y);
@ -197,10 +196,6 @@ const PipelineBlock: React.FC<PipelineBlockProps> = props => {
}); });
}; };
useEffect(() => {
if (hasNullPositions) onLayout();
}, [hasNullPositions]);
return ( return (
<> <>
<Section title="Pipeline" /> <Section title="Pipeline" />
@ -209,11 +204,13 @@ const PipelineBlock: React.FC<PipelineBlockProps> = props => {
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
edges={edges} edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect} onConnect={onConnect}
onNodeDoubleClick={onNodeDoubleClick} onNodeDoubleClick={onNodeDoubleClick}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
onNodesDelete={handleNodesDelete} onNodesDelete={handleNodesDelete}
deleteKeyCode="delete" deleteKeyCode="Delete"
maxZoom={1.25} maxZoom={1.25}
minZoom={0.5} minZoom={0.5}
snapToGrid snapToGrid
@ -225,28 +222,18 @@ const PipelineBlock: React.FC<PipelineBlockProps> = props => {
</ReactFlow> </ReactFlow>
</div> </div>
<div style={{ textAlign: 'center', marginTop: '1rem' }}> <div style={{ textAlign: 'center', marginTop: '1rem' }}>
<button style={{ margin: '0 8px' }} onClick={props.onAddProduct}> <button style={{ margin: '0 8px' }} onClick={props.onAddProduct}>Add product</button>
Add product <button style={{ margin: '0 8px' }} onClick={props.onAddPlant}>Add plant</button>
</button> <button style={{ margin: '0 8px' }} onClick={props.onAddCenter}>Add center</button>
<button style={{ margin: '0 8px' }} onClick={props.onAddPlant}> <button style={{ margin: '0 8px' }} onClick={onLayout}>Auto Layout</button>
Add plant <button title="Drag & connect. Double-click to rename. Delete to remove." style={{ margin: '0 8px' }}>?</button>
</button>
<button style={{ margin: '0 8px' }} onClick={props.onAddCenter}>
Add center
</button>
<button style={{ margin: '0 8px' }} onClick={onLayout}>
Auto Layout
</button>
<button
style={{ margin: '0 8px' }}
title="Drag from one connector to another to create links. Double click to rename. Click to move. Press Delete to remove."
>
?
</button>
</div> </div>
</Card> </Card>
</> </>
); );
}; };
export default PipelineBlock; export default PipelineBlock;
Loading…
Cancel
Save