|
|
@ -1,17 +1,28 @@
|
|
|
|
import React, { useEffect, useCallback, useRef } from 'react';
|
|
|
|
import React, { useEffect, useCallback, useRef, useState } from "react";
|
|
|
|
import dagre from 'dagre';
|
|
|
|
import dagre from "dagre";
|
|
|
|
import {
|
|
|
|
import {
|
|
|
|
ReactFlow,ReactFlowProvider, useNodesState, useEdgesState, Background,
|
|
|
|
ReactFlow,
|
|
|
|
Controls, Node, Edge, Connection, MarkerType,
|
|
|
|
ReactFlowInstance,
|
|
|
|
|
|
|
|
ReactFlowProvider,
|
|
|
|
|
|
|
|
useNodesState,
|
|
|
|
|
|
|
|
useEdgesState,
|
|
|
|
|
|
|
|
Background,
|
|
|
|
|
|
|
|
Controls,
|
|
|
|
|
|
|
|
Node,
|
|
|
|
|
|
|
|
Edge,
|
|
|
|
|
|
|
|
Connection,
|
|
|
|
|
|
|
|
addEdge,
|
|
|
|
|
|
|
|
MarkerType,
|
|
|
|
getNodesBounds,
|
|
|
|
getNodesBounds,
|
|
|
|
getViewportForBounds,
|
|
|
|
getViewportForBounds,
|
|
|
|
useReactFlow} from '@xyflow/react';
|
|
|
|
} from "@xyflow/react";
|
|
|
|
import { PlantNode, ProductNode, CenterNode } from './CircularData';
|
|
|
|
import { PlantNode, ProductNode, CenterNode } 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";
|
|
|
|
import { toPng } from 'html-to-image';
|
|
|
|
import buttonStyles from "./Button.module.css";
|
|
|
|
|
|
|
|
import HelpButton from "../Common/Buttons/HelpButton.module.css";
|
|
|
|
|
|
|
|
|
|
|
|
interface PipelineBlockProps {
|
|
|
|
interface PipelineBlockProps {
|
|
|
|
onAddPlant: () => void;
|
|
|
|
onAddPlant: () => void;
|
|
|
@ -34,90 +45,94 @@ interface PipelineBlockProps {
|
|
|
|
plants: Record<string, PlantNode>;
|
|
|
|
plants: Record<string, PlantNode>;
|
|
|
|
centers: Record<string, CenterNode>;
|
|
|
|
centers: Record<string, CenterNode>;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getLayouted(
|
|
|
|
function getLayouted(
|
|
|
|
nodes: Node<CustomNodeData>[],
|
|
|
|
nodes: Node<CustomNodeData>[],
|
|
|
|
edges: Edge[]
|
|
|
|
edges: Edge[],
|
|
|
|
): { nodes: Node<CustomNodeData>[]; edges: Edge[] } {
|
|
|
|
): { nodes: Node<CustomNodeData>[]; edges: Edge[] } {
|
|
|
|
const W = 125, H = 45;
|
|
|
|
const W = 125,
|
|
|
|
|
|
|
|
H = 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: W, height: H }));
|
|
|
|
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);
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
nodes: nodes.map(n => {
|
|
|
|
nodes: nodes.map((n) => {
|
|
|
|
const d = g.node(n.id)!;
|
|
|
|
const d = g.node(n.id)!;
|
|
|
|
return { ...n, position: { x: d.x - W/2, y: d.y - H/2 } };
|
|
|
|
return { ...n, position: { x: d.x - W / 2, y: d.y - H / 2 } };
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
edges
|
|
|
|
edges,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const PipelineBlock: React.FC<PipelineBlockProps> = props => {
|
|
|
|
const PipelineBlock: React.FC<PipelineBlockProps> = (props) => {
|
|
|
|
const mapRef = useRef<Record<string, 'plant'|'product'|'center'>>({});
|
|
|
|
const mapRef = useRef<Record<string, "plant" | "product" | "center">>({});
|
|
|
|
|
|
|
|
|
|
|
|
const flowWrapper = useRef<HTMLDivElement>(null);
|
|
|
|
const flowWrapper = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
|
|
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
|
|
|
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
|
|
|
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge<CustomNodeData>>(
|
|
|
|
|
|
|
|
[],
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const rebuild = useCallback(() => {
|
|
|
|
const rebuild = useCallback(() => {
|
|
|
|
const m: Record<string, 'plant'|'product'|'center'> = {};
|
|
|
|
const m: Record<string, "plant" | "product" | "center"> = {};
|
|
|
|
const newNodes: Node<CustomNodeData>[] = [];
|
|
|
|
const newNodes: Node<CustomNodeData>[] = [];
|
|
|
|
const newEdges: Edge[] = [];
|
|
|
|
const newEdges: Edge[] = [];
|
|
|
|
Object.entries(props.products).forEach(([key, p]) => {
|
|
|
|
Object.entries(props.products).forEach(([key, p]) => {
|
|
|
|
m[key] = 'product';
|
|
|
|
m[key] = "product";
|
|
|
|
newNodes.push({
|
|
|
|
newNodes.push({
|
|
|
|
id: p.uid,
|
|
|
|
id: p.uid,
|
|
|
|
type: 'default',
|
|
|
|
type: "default",
|
|
|
|
data: { label: p.name, type: 'product' },
|
|
|
|
data: { label: p.name, type: "product" },
|
|
|
|
position: { x: p.x, y: p.y },
|
|
|
|
position: { x: p.x, y: p.y },
|
|
|
|
className: 'ProductNode'
|
|
|
|
className: "ProductNode",
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Object.entries(props.plants).forEach(([key, pl]) => {
|
|
|
|
Object.entries(props.plants).forEach(([key, pl]) => {
|
|
|
|
m[key] = 'plant';
|
|
|
|
m[key] = "plant";
|
|
|
|
newNodes.push({
|
|
|
|
newNodes.push({
|
|
|
|
id: pl.uid,
|
|
|
|
id: pl.uid,
|
|
|
|
type: 'default',
|
|
|
|
type: "default",
|
|
|
|
data: { label: pl.name, type: 'plant' },
|
|
|
|
data: { label: pl.name, type: "plant" },
|
|
|
|
position: { x: pl.x, y: pl.y },
|
|
|
|
position: { x: pl.x, y: pl.y },
|
|
|
|
className: 'PlantNode'
|
|
|
|
className: "PlantNode",
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
pl.inputs.forEach(input => {
|
|
|
|
pl.inputs.forEach((input) => {
|
|
|
|
newEdges.push({
|
|
|
|
newEdges.push({
|
|
|
|
id: `${input}-${key}-in`,
|
|
|
|
id: `${input}-${key}-in`,
|
|
|
|
source: input,
|
|
|
|
source: input,
|
|
|
|
target: key,
|
|
|
|
target: key,
|
|
|
|
animated: true,
|
|
|
|
animated: true,
|
|
|
|
style: { stroke: 'black' },
|
|
|
|
style: { stroke: "black" },
|
|
|
|
markerEnd: { type: MarkerType.ArrowClosed }
|
|
|
|
markerEnd: { type: MarkerType.ArrowClosed },
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
pl.outputs.forEach(output => {
|
|
|
|
pl.outputs.forEach((output) => {
|
|
|
|
newEdges.push({
|
|
|
|
newEdges.push({
|
|
|
|
id: `${key}-${output}-out`,
|
|
|
|
id: `${key}-${output}-out`,
|
|
|
|
source: key,
|
|
|
|
source: key,
|
|
|
|
target: output,
|
|
|
|
target: output,
|
|
|
|
animated: true,
|
|
|
|
animated: true,
|
|
|
|
style: { stroke: 'black' },
|
|
|
|
style: { stroke: "black" },
|
|
|
|
markerEnd: { type: MarkerType.ArrowClosed }
|
|
|
|
markerEnd: { type: MarkerType.ArrowClosed },
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Object.entries(props.centers).forEach(([key, c]) => {
|
|
|
|
Object.entries(props.centers).forEach(([key, c]) => {
|
|
|
|
m[key] = 'center';
|
|
|
|
m[key] = "center";
|
|
|
|
newNodes.push({
|
|
|
|
newNodes.push({
|
|
|
|
id: c.uid,
|
|
|
|
id: c.uid,
|
|
|
|
type: 'default',
|
|
|
|
type: "default",
|
|
|
|
data: { label: c.name, type: 'center' },
|
|
|
|
data: { label: c.name, type: "center" },
|
|
|
|
position: { x: c.x, y: c.y },
|
|
|
|
position: { x: c.x, y: c.y },
|
|
|
|
className: 'CenterNode'
|
|
|
|
className: "CenterNode",
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (c.input) {
|
|
|
|
if (c.input) {
|
|
|
@ -126,145 +141,185 @@ const PipelineBlock: React.FC<PipelineBlockProps> = props => {
|
|
|
|
source: c.input,
|
|
|
|
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 },
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.output.forEach(o => {
|
|
|
|
c.output.forEach((o) => {
|
|
|
|
newEdges.push({
|
|
|
|
newEdges.push({
|
|
|
|
id: `${key}-${o}-out`,
|
|
|
|
id: `${key}-${o}-out`,
|
|
|
|
source: key,
|
|
|
|
source: key,
|
|
|
|
target: o,
|
|
|
|
target: o,
|
|
|
|
animated: true,
|
|
|
|
animated: true,
|
|
|
|
style: { stroke: 'black' },
|
|
|
|
style: { stroke: "black" },
|
|
|
|
markerEnd: { type: MarkerType.ArrowClosed }
|
|
|
|
markerEnd: { type: MarkerType.ArrowClosed },
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
mapRef.current = m;
|
|
|
|
mapRef.current = m;
|
|
|
|
setNodes(newNodes);
|
|
|
|
setNodes(newNodes);
|
|
|
|
setEdges(newEdges);
|
|
|
|
setEdges(newEdges);
|
|
|
|
}, [
|
|
|
|
}, [props.products, props.plants, props.centers, setNodes, setEdges]);
|
|
|
|
|
|
|
|
|
|
|
|
props.products,
|
|
|
|
useEffect(() => {
|
|
|
|
props.plants,
|
|
|
|
rebuild();
|
|
|
|
props.centers,
|
|
|
|
}, [rebuild]);
|
|
|
|
setNodes,
|
|
|
|
const onConnect = (connection: Connection) => {
|
|
|
|
setEdges
|
|
|
|
const { source: s, target: t } = connection;
|
|
|
|
]);
|
|
|
|
const st = mapRef.current[s!],
|
|
|
|
|
|
|
|
tt = mapRef.current[t!];
|
|
|
|
useEffect(() => { rebuild(); }, [rebuild]);
|
|
|
|
|
|
|
|
const onConnect = (c: Connection) => {
|
|
|
|
if (st === "product" && tt === "plant") props.onSetPlantInput(t!, s!);
|
|
|
|
const s = c.source!, t = c.target!;
|
|
|
|
else if (st === "plant" && tt === "product") props.onAddPlantOutput(s!, t!);
|
|
|
|
const st = mapRef.current[s], tt = mapRef.current[t];
|
|
|
|
else if (st === "product" && tt === "center")
|
|
|
|
if (st==='product' && tt==='plant') props.onSetPlantInput(t, s);
|
|
|
|
props.onAddCenterInput(t!, s!);
|
|
|
|
else if (st==='plant' && tt==='product') props.onAddPlantOutput(s, t);
|
|
|
|
else if (st === "center" && tt === "product")
|
|
|
|
else if (st==='product' && tt==='center') props.onAddCenterInput(t, s);
|
|
|
|
props.onAddCenterOutput(s!, t);
|
|
|
|
else if (st==='center' && tt==='product') props.onAddCenterOutput(s, t);
|
|
|
|
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const onNodeDragStop = (_: any, n: Node<CustomNodeData>) => {
|
|
|
|
const onNodeDragStop = (_: any, n: Node<CustomNodeData>) => {
|
|
|
|
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);
|
|
|
|
if (data.type==='product') props.onMoveProduct(id, position.x, position.y);
|
|
|
|
if (data.type === "product")
|
|
|
|
if (data.type==='center') props.onMoveCenter(id, position.x, position.y);
|
|
|
|
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 t = mapRef.current[n.id];
|
|
|
|
|
|
|
|
if (t==='plant') props.onRemovePlant(n.id);
|
|
|
|
|
|
|
|
if (t==='product') props.onRemoveProduct(n.id);
|
|
|
|
|
|
|
|
if (t==='center') props.onRemoveCenter(n.id);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}, [props]);
|
|
|
|
const handleNodesDelete = useCallback(
|
|
|
|
|
|
|
|
(deleted: Node<CustomNodeData>[]) => {
|
|
|
|
|
|
|
|
deleted.forEach((n) => {
|
|
|
|
|
|
|
|
const t = mapRef.current[n.id];
|
|
|
|
|
|
|
|
if (t === "plant") props.onRemovePlant(n.id);
|
|
|
|
|
|
|
|
if (t === "product") props.onRemoveProduct(n.id);
|
|
|
|
|
|
|
|
if (t === "center") props.onRemoveCenter(n.id);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[props],
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const onNodeDoubleClick = (_: React.MouseEvent, n: Node<CustomNodeData>) => {
|
|
|
|
const onNodeDoubleClick = (_: React.MouseEvent, n: Node<CustomNodeData>) => {
|
|
|
|
const oldName = n.data.label;
|
|
|
|
const oldName = n.data.label;
|
|
|
|
const newName = window.prompt('Enter new name', oldName);
|
|
|
|
const newName = window.prompt("Enter new name", oldName);
|
|
|
|
console.log('after rename', newName);
|
|
|
|
console.log("after rename", newName);
|
|
|
|
const uniqueId = n.id;
|
|
|
|
const uniqueId = n.id;
|
|
|
|
if (!newName || newName===oldName) return;
|
|
|
|
if (!newName || newName === oldName) return;
|
|
|
|
if (n.data.type==='plant') props.onRenamePlant(uniqueId, newName);
|
|
|
|
if (n.data.type === "plant") props.onRenamePlant(uniqueId, newName);
|
|
|
|
if (n.data.type==='product') props.onRenameProduct(uniqueId, newName);
|
|
|
|
if (n.data.type === "product") props.onRenameProduct(uniqueId, newName);
|
|
|
|
if (n.data.type==='center') props.onRenameCenter(uniqueId, newName);
|
|
|
|
if (n.data.type === "center") props.onRenameCenter(uniqueId, newName);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
function DownloadButton() {
|
|
|
|
function DownloadButton() {
|
|
|
|
const onDownload = async () => {
|
|
|
|
const onDownload = async () => {
|
|
|
|
if (!flowWrapper.current) return;
|
|
|
|
if (!rfInstance || !flowWrapper.current) return;
|
|
|
|
const node = flowWrapper.current;
|
|
|
|
|
|
|
|
const { width, height } = node.getBoundingClientRect();
|
|
|
|
const minZoom = 0.1;
|
|
|
|
const dataUrl = await toPng(node, {
|
|
|
|
const maxZoom = 2;
|
|
|
|
backgroundColor: '#fff',
|
|
|
|
const padding = 0.1;
|
|
|
|
width: Math.round(width),
|
|
|
|
|
|
|
|
height: Math.round(height)
|
|
|
|
const nodes = rfInstance.getNodes();
|
|
|
|
});
|
|
|
|
const bounds = getNodesBounds(nodes);
|
|
|
|
const downloadLink = document.createElement('a');
|
|
|
|
|
|
|
|
downloadLink.href = dataUrl;
|
|
|
|
const { clientWidth: width, clientHeight: height } = flowWrapper.current;
|
|
|
|
downloadLink.download = 'pipeline.png';
|
|
|
|
const { x, y, zoom } = getViewportForBounds(
|
|
|
|
downloadLink.click();
|
|
|
|
bounds,
|
|
|
|
|
|
|
|
width,
|
|
|
|
|
|
|
|
height,
|
|
|
|
|
|
|
|
minZoom,
|
|
|
|
|
|
|
|
maxZoom,
|
|
|
|
|
|
|
|
padding,
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
rfInstance.setViewport({ x, y, zoom });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const svgString = rfInstance.toSvg();
|
|
|
|
|
|
|
|
const blob = new Blob([svgString], { type: "image/svg+xml" });
|
|
|
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
|
|
|
const a = document.createElement("a");
|
|
|
|
|
|
|
|
a.href = url;
|
|
|
|
|
|
|
|
a.download = "pipeline.svg";
|
|
|
|
|
|
|
|
a.click();
|
|
|
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
return <button style={{ margin: '0 8px' }} onClick={onDownload}>Export Pipeline</button>;
|
|
|
|
|
|
|
|
};
|
|
|
|
return (
|
|
|
|
|
|
|
|
<button className={buttonStyles.Button} onClick={onDownload}>
|
|
|
|
|
|
|
|
Export Pipeline
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const onLayout = () => {
|
|
|
|
const onLayout = () => {
|
|
|
|
const { nodes: ln, edges: le } = getLayouted(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);
|
|
|
|
else if (data.type==='product') props.onMoveProduct(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);
|
|
|
|
else props.onMoveCenter(id, position.x, position.y);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
|
|
<>
|
|
|
|
|
|
|
|
<Section title="Pipeline" />
|
|
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
|
|
<ReactFlowProvider>
|
|
|
|
|
|
|
|
<div ref={flowWrapper} className={styles.PipelineBlock} style={{ width: '100%', height: 600 }}>
|
|
|
|
|
|
|
|
<ReactFlow
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nodes={nodes}
|
|
|
|
|
|
|
|
edges={edges}
|
|
|
|
|
|
|
|
onNodesChange={onNodesChange}
|
|
|
|
|
|
|
|
onEdgesChange={onEdgesChange}
|
|
|
|
|
|
|
|
onConnect={onConnect}
|
|
|
|
|
|
|
|
onNodeDoubleClick={onNodeDoubleClick}
|
|
|
|
|
|
|
|
onNodeDragStop={onNodeDragStop}
|
|
|
|
|
|
|
|
onNodesDelete={handleNodesDelete}
|
|
|
|
|
|
|
|
deleteKeyCode="Delete"
|
|
|
|
|
|
|
|
maxZoom={1.25}
|
|
|
|
|
|
|
|
minZoom={0.5}
|
|
|
|
|
|
|
|
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}>Add product</button>
|
|
|
|
|
|
|
|
<button style={{ margin: '0 8px' }} onClick={props.onAddPlant}>Add plant</button>
|
|
|
|
|
|
|
|
<button style={{ margin: '0 8px' }} onClick={props.onAddCenter}>Add center</button>
|
|
|
|
|
|
|
|
<button style={{ margin: '0 8px' }} onClick={onLayout}>Auto Layout</button>
|
|
|
|
|
|
|
|
<DownloadButton />
|
|
|
|
|
|
|
|
<button style={{ margin: '0 8px' }} title="Drag & connect. Double-click to rename. Delete to remove.">?</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
return (
|
|
|
|
</ReactFlowProvider>
|
|
|
|
<>
|
|
|
|
</Card>
|
|
|
|
<Section title="Pipeline" />
|
|
|
|
</>
|
|
|
|
<Card>
|
|
|
|
|
|
|
|
<ReactFlowProvider>
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
|
|
ref={flowWrapper}
|
|
|
|
|
|
|
|
className={styles.PipelineBlock}
|
|
|
|
|
|
|
|
style={{ width: "100%", height: 600 }}
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<ReactFlow
|
|
|
|
|
|
|
|
nodes={nodes}
|
|
|
|
|
|
|
|
edges={edges}
|
|
|
|
|
|
|
|
onNodesChange={onNodesChange}
|
|
|
|
|
|
|
|
onEdgesChange={onEdgesChange}
|
|
|
|
|
|
|
|
onConnect={onConnect}
|
|
|
|
|
|
|
|
onNodeDoubleClick={onNodeDoubleClick}
|
|
|
|
|
|
|
|
onNodeDragStop={onNodeDragStop}
|
|
|
|
|
|
|
|
onNodesDelete={handleNodesDelete}
|
|
|
|
|
|
|
|
deleteKeyCode="Delete"
|
|
|
|
|
|
|
|
maxZoom={1.25}
|
|
|
|
|
|
|
|
minZoom={0.5}
|
|
|
|
|
|
|
|
snapToGrid
|
|
|
|
|
|
|
|
preventScrolling
|
|
|
|
|
|
|
|
nodeTypes={{ default: CustomNode }}
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<Background />
|
|
|
|
|
|
|
|
<Controls showInteractive={false} />
|
|
|
|
|
|
|
|
</ReactFlow>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ textAlign: "center", marginTop: "1rem" }}>
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
|
|
className={buttonStyles.Button}
|
|
|
|
|
|
|
|
onClick={props.onAddProduct}
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
Add product
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<button className={buttonStyles.Button} onClick={props.onAddPlant}>
|
|
|
|
|
|
|
|
Add plant
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<button className={buttonStyles.Button} onClick={props.onAddCenter}>
|
|
|
|
|
|
|
|
Add center
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<button className={buttonStyles.Button} onClick={onLayout}>
|
|
|
|
|
|
|
|
Auto Layout
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<DownloadButton />
|
|
|
|
|
|
|
|
<button className={`buttonStyles.Button} ${HelpButton.HelpButton}`}>
|
|
|
|
|
|
|
|
?
|
|
|
|
|
|
|
|
<span className={HelpButton.tooltip}>
|
|
|
|
|
|
|
|
"Drag & connect. Double-click to rename. Delete to remove."
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</ReactFlowProvider>
|
|
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default PipelineBlock;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export default PipelineBlock;
|
|
|
|