@ -1,13 +1,19 @@
import { CircularPlant , CircularProduct , CircularCenter } from "./CircularData" ;
import React , { useEffect , useCallback } from 'react' ;
import { Node , Edge } from "@xyflow/react" ;
import dagre from 'dagre' ;
import styles from "./PipelineBlock.module.css" ;
import {
import { ReactFlow , Background , Controls , MarkerType } from '@xyflow/react' ;
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 Section from '../Common/Section' ;
import Card from '../Common/Card' ;
import Card from '../Common/Card' ;
import { useEffect , useCallback } from "react" ;
import styles from './PipelineBlock.module.css' ;
import { Connection } from '@xyflow/react' ;
import CustomNode , { CustomNodeData } from "./NodesAndEdges" ;
interface PipelineBlockProps {
interface PipelineBlockProps {
@ -19,218 +25,202 @@ interface PipelineBlockProps {
onMoveCenter : ( 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 ;
onAddPlantOutput : ( plantName : string , productName : string ) = > void ;
onAddCenterInput : ( plant Name: string , productName : string ) = > void ;
onAddCenterInput : ( center Name: string , productName : string ) = > void ;
onAddCenterOutput : ( plant Name: string , productName : string ) = > void ;
onAddCenterOutput : ( center Name: string , productName : string ) = > void ;
onRemovePlant : ( id : string ) = > void ;
onRemovePlant : ( id : string ) = > void ;
onRemoveProduct : ( id : string ) = > void ;
onRemoveProduct : ( id : string ) = > void ;
onRemoveCenter : ( id : string ) = > void ;
onRemoveCenter : ( id : 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 > ;
}
}
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" ) {
function getLayoutedNodesAndEdges (
props . onAddCenterInput ( target , source ) ;
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
}
}
else if ( sourceType === "center" && targetType === "product" ) {
props . onAddCenterOutput ( source , target ) ;
}
} ;
} ;
} ) ;
const onNodeDragStop = ( _ :any , node : Node ) = > {
return { nodes : layouted , edges } ;
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 ) ;
}
} ) ;
} , [ props , mapNameToType ] ) ;
for ( const [ productName , product ] of Object . entries ( props . products ) as [ string , CircularProduct ] [ ] ) {
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 ;
if ( ! product . x || ! product . y ) hasNullPositions = true ;
mapNameToType [ productName ] = "product" ;
mapNameToType [ key ] = 'product' ;
nodes . push ( {
nodes . push ( {
id : product.uid ,
id : product.uid ,
type : "default" ,
type : 'default' ,
data : { label : product.name , type : 'product' } ,
data : { label : product.name , type : 'product' } ,
position : { x : product.x , y : product.y } ,
position : { x : product.x , y : product.y } ,
className : 'ProductNode'
className : 'ProductNode'
} ) ;
} ) ;
}
} ) ;
for ( const [ plantName , plant ] of Object . entries ( props . plants ) as [ string , CircularPlant ] [ ] ) {
Object . entries ( props . plants ) . forEach ( ( [ key , plant ] ) = > {
if ( ! plant . x || ! plant . y ) hasNullPositions = true ;
if ( ! plant . x || ! plant . y ) hasNullPositions = true ;
mapNameToType [ plantName ] = "plant" ;
mapNameToType [ key ] = 'plant' ;
nodes . push ( {
nodes . push ( {
id : plant.uid ,
id : plant.uid ,
type : "default" ,
type : 'default' ,
data : { label : plant.name , type : 'plant' } ,
data : { label : plant.name , type : 'plant' } ,
position : { x : plant.x , y : plant.y } ,
position : { x : plant.x , y : plant.y } ,
className : 'PlantNode'
className : 'PlantNode'
} ) ;
} ) ;
plant . inputs . forEach ( input = > {
if ( plant ) {
for ( const inputProduct of plant . inputs ) {
edges . push ( {
edges . push ( {
id : ` ${ input Product} - ${ plantName } ` ,
id : ` ${ input } - ${ key } -in ` ,
source : inpu tProduc t,
source : inpu t,
target : plantName ,
target : key ,
animated : true ,
animated : true ,
style : { stroke : "black" } ,
style : { stroke : 'black' } ,
markerEnd : {
markerEnd : { type : MarkerType . ArrowClosed }
type : MarkerType . ArrowClosed ,
} ,
} ) ;
} ) ;
}
} ) ;
for ( const outputProduct of plant . outputs ? ? [ ] ) {
plant . outputs . forEach ( output = > {
edges . push ( {
edges . push ( {
id : ` ${ plantName } - ${ outputProduct } ` ,
id : ` ${ key } - ${ output } -out ` ,
source : plantName ,
source : key ,
target : outpu tProduc t,
target : outpu t,
animated : true ,
animated : true ,
style : { stroke : 'black' } ,
style : { stroke : 'black' } ,
markerEnd : { type : MarkerType . ArrowClosed } ,
markerEnd : { type : MarkerType . ArrowClosed }
} ) ;
} ) ;
}
} ) ;
} ) ;
}
Object . entries ( props . centers ) . forEach ( ( [ key , center ] ) = > {
if ( ! center . x || ! center . y ) hasNullPositions = true ;
}
mapNameToType [ key ] = 'center' ;
for ( const [ centerName , center ] of Object . entries ( props . centers ) ) {
mapNameToType [ centerName ] = "center" ;
nodes . push ( {
nodes . push ( {
id : center.uid ,
id : center.uid ,
type : "default" ,
type : 'default' ,
data : { label : center.name , type : "center" } ,
data : { label : center.name , type : 'center' } ,
position : { x : center.x , y : center.y } ,
position : { x : center.x , y : center.y } ,
className : 'CenterNode'
className : 'CenterNode'
} ) ;
} ) ;
if ( center . input ) {
if ( center . input ) {
edges . push ( {
edges . push ( {
id : ` ${ center . input } - ${ centerName} ` ,
id : ` ${ center . input } - ${ key} -in ` ,
source : center.input ,
source : center.input ,
target :centerName ,
target : key ,
animated : true ,
animated : true ,
style : { stroke : "black" } ,
style : { stroke : 'black' } ,
markerEnd : { type : MarkerType . ArrowClosed } ,
markerEnd : { type : MarkerType . ArrowClosed }
} ) ;
} ) ;
}
}
for ( const out of center . output ) {
center . output . forEach ( out = > {
edges . push ( {
edges . push ( {
id : ` ${ centerName} - ${ out } ` ,
id : ` ${ key} - ${ out } -out ` ,
source : centerName ,
source : key ,
target : out ,
target : out ,
animated : true ,
animated : true ,
style : { stroke : "black" } ,
style : { stroke : 'black' } ,
markerEnd : { type : MarkerType . ArrowClosed } ,
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 ( ( ) = > {
useEffect ( ( ) = > {
if ( hasNullPositions ) onLayout ( ) ;
if ( hasNullPositions ) onLayout ( ) ;
} , [ hasNullPositions ] ) ;
} , [ hasNullPositions ] ) ;
return (
return (
< >
< >
< Section title = "Pipeline" / >
< Section title = "Pipeline" / >
< Card >
< Card >
< div className = { styles . PipelineBlock } >
< div className = { styles . PipelineBlock } >
< ReactFlow
< ReactFlow
nodes = { nodes }
nodes = { nodes }
edges = { edges }
edges = { edges }
onNodeDoubleClick = { onNodeDoubleClick }
onNodeDragStop = { onNodeDragStop }
onConnect = { onConnect }
onConnect = { onConnect }
onNodeDragStop = { onNodeDragStop }
onNodesDelete = { handleNodesDelete }
onNodesDelete = { handleNodesDelete }
deleteKeyCode = "D elete"
deleteKeyCode = "d elete"
maxZoom = { 1.25 }
maxZoom = { 1.25 }
minZoom = { 0.5 }
minZoom = { 0.5 }
snapToGrid = { true }
snapToGrid
preventScrolling = { false }
preventScrolling
nodeTypes = { { default : CustomNode } }
nodeTypes = { { default : CustomNode } }
>
>
< Background / >
< Background / >
< Controls showInteractive = { false } / >
< Controls showInteractive = { false } / >
< / ReactFlow >
< / ReactFlow >
< / div >
< / div >
< div style = { { textAlign : "center" , marginTop : "1rem" } } >
< div style = { { textAlign : 'center' , marginTop : '1rem' } } >
< button
< button style = { { margin : '0 8px' } } onClick = { props . onAddProduct } >
style = { { margin : "0 8px" } }
onClick = { props . onAddProduct }
>
Add product
Add product
< / button >
< / button >
< button
< button style = { { margin : '0 8px' } } onClick = { props . onAddPlant } >
style = { { margin : "0 8px" } }
onClick = { props . onAddPlant }
>
Add plant
Add plant
< / button >
< / button >
< button
< button style = { { margin : '0 8px' } } onClick = { props . onAddCenter } >
style = { { margin : "0 8px" } }
onClick = { props . onAddCenter }
>
Add center
Add center
< / button >
< / button >
< button
< button style = { { margin : '0 8px' } } onClick = { onLayout } >
style = { { margin : "0 8px" } }
onClick = { onLayout }
>
Auto Layout
Auto Layout
< / button >
< / button >
< button
< button
style = { { margin : "0 8px" } }
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 ."
title = "Drag from one connector to another to create links . Double click to rename. Click to move. Press Delete to remove."
>
>
?
?
< / button >
< / button >
@ -240,3 +230,4 @@ return (
) ;
) ;
} ;
} ;
export default PipelineBlock ;
export default PipelineBlock ;