mirror of https://github.com/ANL-CEEESA/RELOG.git
parent
b095e4bbbe
commit
6262047671
@ -1,242 +1,233 @@
|
||||
import { CircularPlant, CircularProduct, CircularCenter } from "./CircularData";
|
||||
import { Node, Edge } from "@xyflow/react";
|
||||
import styles from "./PipelineBlock.module.css";
|
||||
import { ReactFlow, Background, Controls, MarkerType } from '@xyflow/react';
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import dagre from 'dagre';
|
||||
import {
|
||||
ReactFlow,
|
||||
Background,
|
||||
Controls,
|
||||
Node,
|
||||
Edge,
|
||||
Connection,
|
||||
MarkerType
|
||||
} from '@xyflow/react';
|
||||
import { CircularPlant, CircularProduct, CircularCenter } from './CircularData';
|
||||
import CustomNode, { CustomNodeData } from './NodesAndEdges';
|
||||
import Section from '../Common/Section';
|
||||
import Card from '../Common/Card';
|
||||
import { useEffect, useCallback } from "react";
|
||||
import { Connection } from '@xyflow/react';
|
||||
import CustomNode, { CustomNodeData }from "./NodesAndEdges";
|
||||
|
||||
import styles from './PipelineBlock.module.css';
|
||||
|
||||
|
||||
interface PipelineBlockProps {
|
||||
onAddPlant: () => void;
|
||||
onAddProduct: () => void;
|
||||
onAddCenter: () => void;
|
||||
onMovePlant: (name: string , x: number, y: number) => void;
|
||||
onMovePlant: (name: string, x: number, y: number) => void;
|
||||
onMoveProduct: (name: string, x: number, y: number) => void;
|
||||
onMoveCenter: (name: string, x: number, y: number) => void;
|
||||
onSetPlantInput: (plantName:string, productName: string) => void;
|
||||
onSetPlantInput: (plantName: string, productName: string) => void;
|
||||
onAddPlantOutput: (plantName: string, productName: string) => void;
|
||||
onAddCenterInput: (plantName: string, productName: string) => void;
|
||||
onAddCenterOutput: (plantName: string, productName: string) => void;
|
||||
onRemovePlant: (id:string) => void;
|
||||
onAddCenterInput: (centerName: string, productName: string) => void;
|
||||
onAddCenterOutput: (centerName: string, productName: string) => void;
|
||||
onRemovePlant: (id: string) => void;
|
||||
onRemoveProduct: (id: string) => void;
|
||||
onRemoveCenter: (id: string) => void;
|
||||
|
||||
products: Record<string, CircularProduct>;
|
||||
plants: Record<string, CircularPlant>;
|
||||
centers: Record<string, CircularCenter>;
|
||||
}
|
||||
const onNodeDoubleClick = () => {};
|
||||
|
||||
const handleNodesDelete = () => {};
|
||||
const handleEdgesDelete = () => {};
|
||||
const onLayout = () => {};
|
||||
|
||||
const PipelineBlock: React.FC<PipelineBlockProps> = (props) => {
|
||||
const nodes: Node[] = [];
|
||||
const edges: Edge[] = [];
|
||||
|
||||
let mapNameToType: Record<string,string> = {};
|
||||
let hasNullPositions: boolean = false;
|
||||
|
||||
const onConnect = (params: Connection) => {
|
||||
const { source, target } = params;
|
||||
if (!source || ! target) return;
|
||||
|
||||
const sourceType = mapNameToType[source];
|
||||
const targetType = mapNameToType[target];
|
||||
|
||||
if (sourceType === "product" && targetType === "plant") {
|
||||
props.onSetPlantInput(target,source);
|
||||
} else if (sourceType === "plant" && targetType === "product") {
|
||||
props.onAddPlantOutput(source, target);
|
||||
}
|
||||
|
||||
else if (sourceType === "product" && targetType === "center") {
|
||||
props.onAddCenterInput(target, source);
|
||||
}
|
||||
else if (sourceType === "center" && targetType === "product") {
|
||||
props.onAddCenterOutput(source, target);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const onNodeDragStop =(_:any, node: Node) => {
|
||||
const { id, position, data} = node;
|
||||
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 === "center") {
|
||||
props.onMoveCenter(id, position.x, position.y);
|
||||
}
|
||||
|
||||
};
|
||||
const handleNodesDelete = useCallback((deleted: Node[]) => {
|
||||
deleted.forEach((n) => {
|
||||
const type = mapNameToType[n.id];
|
||||
if (type === "plant") {
|
||||
props.onRemovePlant(n.id);
|
||||
} else if (type === "product") {
|
||||
props.onRemoveProduct(n.id);
|
||||
} else if (type === "center") {
|
||||
props.onRemoveCenter!(n.id);
|
||||
function getLayoutedNodesAndEdges(
|
||||
nodes: Node<CustomNodeData>[],
|
||||
edges: Edge[]
|
||||
): { nodes: Node<CustomNodeData>[]; edges: Edge[] } {
|
||||
const NODE_WIDTH = 125;
|
||||
const NODE_HEIGHT = 45;
|
||||
const g = new dagre.graphlib.Graph();
|
||||
g.setDefaultEdgeLabel(() => ({}));
|
||||
g.setGraph({ rankdir: 'LR' });
|
||||
nodes.forEach(n => g.setNode(n.id, { width: NODE_WIDTH, height: NODE_HEIGHT }));
|
||||
edges.forEach(e => g.setEdge(e.source, e.target));
|
||||
dagre.layout(g);
|
||||
const layouted = nodes.map(n => {
|
||||
const d = g.node(n.id)!;
|
||||
return {
|
||||
...n,
|
||||
position: {
|
||||
x: d.x - NODE_WIDTH / 2,
|
||||
y: d.y - NODE_HEIGHT / 2
|
||||
}
|
||||
};
|
||||
});
|
||||
}, [props, mapNameToType]);
|
||||
return { nodes: layouted, edges };
|
||||
}
|
||||
|
||||
|
||||
for (const [productName, product] of Object.entries(props.products) as [string, CircularProduct][]) {
|
||||
if(!product.x || !product.y) hasNullPositions = true;
|
||||
mapNameToType[productName] = "product";
|
||||
const PipelineBlock: React.FC<PipelineBlockProps> = props => {
|
||||
const nodes: Node<CustomNodeData>[] = [];
|
||||
const edges: Edge[] = [];
|
||||
const mapNameToType: Record<string, 'plant' | 'product' | 'center'> = {};
|
||||
let hasNullPositions = false;
|
||||
Object.entries(props.products).forEach(([key, product]) => {
|
||||
if (!product.x || !product.y) hasNullPositions = true;
|
||||
mapNameToType[key] = 'product';
|
||||
nodes.push({
|
||||
id: product.uid,
|
||||
type: "default",
|
||||
data: {label: product.name, type: 'product'},
|
||||
position: { x:product.x, y:product.y},
|
||||
type: 'default',
|
||||
data: { label: product.name, type: 'product' },
|
||||
position: { x: product.x, y: product.y },
|
||||
className: 'ProductNode'
|
||||
});
|
||||
}
|
||||
for (const [plantName, plant] of Object.entries(props.plants) as [string, CircularPlant][]) {
|
||||
if(!plant.x || !plant.y) hasNullPositions = true;
|
||||
mapNameToType[plantName] = "plant";
|
||||
});
|
||||
Object.entries(props.plants).forEach(([key, plant]) => {
|
||||
if (!plant.x || !plant.y) hasNullPositions = true;
|
||||
mapNameToType[key] = 'plant';
|
||||
nodes.push({
|
||||
id: plant.uid,
|
||||
type: "default",
|
||||
data: {label: plant.name, type: 'plant'},
|
||||
position: { x:plant.x, y:plant.y},
|
||||
type: 'default',
|
||||
data: { label: plant.name, type: 'plant' },
|
||||
position: { x: plant.x, y: plant.y },
|
||||
className: 'PlantNode'
|
||||
});
|
||||
|
||||
if (plant) {
|
||||
for (const inputProduct of plant.inputs){
|
||||
plant.inputs.forEach(input => {
|
||||
edges.push({
|
||||
id: `${inputProduct}-${plantName}`,
|
||||
source: inputProduct,
|
||||
target: plantName,
|
||||
id: `${input}-${key}-in`,
|
||||
source: input,
|
||||
target: key,
|
||||
animated: true,
|
||||
style: { stroke: "black" },
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
},
|
||||
|
||||
style: { stroke: 'black' },
|
||||
markerEnd: { type: MarkerType.ArrowClosed }
|
||||
});
|
||||
}
|
||||
for (const outputProduct of plant.outputs ?? []) {
|
||||
});
|
||||
plant.outputs.forEach(output => {
|
||||
edges.push({
|
||||
id: `${plantName}-${outputProduct}`,
|
||||
source: plantName,
|
||||
target: outputProduct,
|
||||
id: `${key}-${output}-out`,
|
||||
source: key,
|
||||
target: output,
|
||||
animated: true,
|
||||
style: { stroke: 'black' },
|
||||
markerEnd: { type: MarkerType.ArrowClosed },
|
||||
markerEnd: { type: MarkerType.ArrowClosed }
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
for (const [centerName, center] of Object.entries(props.centers)) {
|
||||
mapNameToType[centerName] = "center";
|
||||
});
|
||||
});
|
||||
Object.entries(props.centers).forEach(([key, center]) => {
|
||||
if (!center.x || !center.y) hasNullPositions = true;
|
||||
mapNameToType[key] = 'center';
|
||||
nodes.push({
|
||||
id: center.uid,
|
||||
type: "default",
|
||||
data: { label: center.name, type: "center"},
|
||||
position: {x: center.x, y: center.y},
|
||||
type: 'default',
|
||||
data: { label: center.name, type: 'center' },
|
||||
position: { x: center.x, y: center.y },
|
||||
className: 'CenterNode'
|
||||
});
|
||||
if (center.input) {
|
||||
edges.push({
|
||||
id: `${center.input}-${centerName}`,
|
||||
id: `${center.input}-${key}-in`,
|
||||
source: center.input,
|
||||
target:centerName,
|
||||
target: key,
|
||||
animated: true,
|
||||
style: { stroke: "black"},
|
||||
markerEnd: { type: MarkerType.ArrowClosed},
|
||||
style: { stroke: 'black' },
|
||||
markerEnd: { type: MarkerType.ArrowClosed }
|
||||
});
|
||||
}
|
||||
for (const out of center.output) {
|
||||
center.output.forEach(out => {
|
||||
edges.push({
|
||||
id: `${centerName}-${out}`,
|
||||
source: centerName,
|
||||
target:out,
|
||||
id: `${key}-${out}-out`,
|
||||
source: key,
|
||||
target: out,
|
||||
animated: true,
|
||||
style: { stroke: "black"},
|
||||
markerEnd: { type: MarkerType.ArrowClosed},
|
||||
style: { stroke: 'black' },
|
||||
markerEnd: { type: MarkerType.ArrowClosed }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
const onConnect = (params: Connection) => {
|
||||
const { source, target } = params;
|
||||
if (!source || !target) return;
|
||||
const sourceType = mapNameToType[source];
|
||||
const targetType = mapNameToType[target];
|
||||
if (sourceType === 'product' && targetType === 'plant') props.onSetPlantInput(target, source);
|
||||
else if (sourceType === 'plant' && targetType === 'product') props.onAddPlantOutput(source, target);
|
||||
else if (sourceType === 'product' && targetType === 'center') props.onAddCenterInput(target, source);
|
||||
else if (sourceType === 'center' && targetType === 'product') props.onAddCenterOutput(source, target);
|
||||
};
|
||||
|
||||
const onNodeDragStop = (_: any, node: Node<CustomNodeData>) => {
|
||||
const { id, position, data } = node;
|
||||
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 === 'center') props.onMoveCenter(id, position.x, position.y);
|
||||
};
|
||||
|
||||
const handleNodesDelete = useCallback(
|
||||
(deleted: Node<CustomNodeData>[]) => {
|
||||
deleted.forEach(n => {
|
||||
const type = mapNameToType[n.id];
|
||||
if (type === 'plant') props.onRemovePlant(n.id);
|
||||
else if (type === 'product') props.onRemoveProduct(n.id);
|
||||
else if (type === 'center') props.onRemoveCenter(n.id);
|
||||
});
|
||||
},
|
||||
[props]
|
||||
);
|
||||
|
||||
const onLayout = () => {
|
||||
const { nodes: ln, edges: le } = getLayoutedNodesAndEdges(nodes, edges);
|
||||
ln.forEach(n => {
|
||||
const { id, position, data } = n;
|
||||
if (data.type === 'plant') props.onMovePlant(id, position.x, position.y);
|
||||
else if (data.type === 'product') props.onMoveProduct(id, position.x, position.y);
|
||||
else props.onMoveCenter(id, position.x, position.y);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (hasNullPositions) onLayout();
|
||||
|
||||
}, [hasNullPositions]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section title="Pipeline" />
|
||||
<Card>
|
||||
<div className={styles.PipelineBlock}>
|
||||
<ReactFlow
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section title="Pipeline" />
|
||||
<Card>
|
||||
<div className={styles.PipelineBlock}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodeDoubleClick={onNodeDoubleClick}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
onConnect={onConnect}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
onNodesDelete={handleNodesDelete}
|
||||
deleteKeyCode="Delete"
|
||||
deleteKeyCode="delete"
|
||||
maxZoom={1.25}
|
||||
minZoom={0.5}
|
||||
snapToGrid={true}
|
||||
preventScrolling={false}
|
||||
snapToGrid
|
||||
preventScrolling
|
||||
nodeTypes={{ default: CustomNode }}
|
||||
>
|
||||
<Background />
|
||||
<Controls showInteractive={false} />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
<div style={{ textAlign: "center", marginTop: "1rem" }}>
|
||||
<button
|
||||
style={{ margin: "0 8px" }}
|
||||
onClick={props.onAddProduct}
|
||||
>
|
||||
>
|
||||
<Background />
|
||||
<Controls showInteractive={false} />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', marginTop: '1rem' }}>
|
||||
<button style={{ margin: '0 8px' }} onClick={props.onAddProduct}>
|
||||
Add product
|
||||
</button>
|
||||
<button
|
||||
style={{ margin: "0 8px" }}
|
||||
onClick={props.onAddPlant}
|
||||
>
|
||||
</button>
|
||||
<button style={{ margin: '0 8px' }} onClick={props.onAddPlant}>
|
||||
Add plant
|
||||
</button>
|
||||
<button
|
||||
style={{ margin: "0 8px" }}
|
||||
onClick={props.onAddCenter}
|
||||
>
|
||||
</button>
|
||||
<button style={{ margin: '0 8px' }} onClick={props.onAddCenter}>
|
||||
Add center
|
||||
</button>
|
||||
<button
|
||||
style={{ margin: "0 8px" }}
|
||||
onClick={onLayout}
|
||||
>
|
||||
</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 between products, plants, and centers. Double click to rename an element. Click an element to select and move it. Press the [Delete] key to remove it."
|
||||
>
|
||||
</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>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default PipelineBlock;
|
||||
|
Loading…
Reference in new issue