meeting 7/22 progress

pull/33/head
Khwaja 2 months ago
parent 052cd374c0
commit 41a51d44e5

@ -0,0 +1,67 @@
.Button {
padding: 6px 36px;
margin: 12px 6px;
line-height: 24px;
border: var(--box-border);
/* background-color: white; */
box-shadow: var(--box-shadow);
border-radius: var(--border-radius);
cursor: pointer;
color: rgba(0, 0, 0, 0.8);
text-transform: uppercase;
font-weight: bold;
font-size: 12px;
background: linear-gradient(rgb(255, 255, 255) 25%, rgb(245, 245, 245) 100%);
}
.Button:hover {
background: rgb(245, 245, 245);
}
.Button:active {
background: rgba(220, 220, 220);
}
.inline {
padding: 0 12px;
margin: 2px 4px 2px 0;
height: 32px;
font-size: 11px;
}
/* .inline:last-child {
margin: 2px 1px;
} */
.tooltip {
visibility: hidden;
background-color: #333;
color: white;
opacity: 0%;
width: 180px;
margin-top: 36px;
margin-left: -180px;
position: absolute;
z-index: 100;
text-transform: none;
font-size: 13px;
border-radius: 4px;
box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.25);
line-height: 18px;
padding: 6px;
transition: opacity 0.5s;
font-weight: normal;
text-align: left;
padding: 6px 12px;
}
.Button:hover .tooltip {
visibility: visible;
opacity: 100%;
transition: opacity 0.5s;
}
.Button:disabled {
color: rgba(0, 0, 0, 0.25);
cursor: default;
}

@ -95,7 +95,7 @@ const CaseBuilder = () => {
if (!center) return prev;
return {
...prev,
centers: {
Centers: {
...prev.Centers,
[centerName]: { ...center, input: productName },
},
@ -120,7 +120,7 @@ const CaseBuilder = () => {
return {
...prevData,
plants: {
Plants: {
...prevData.Plants,
[plantName]: updatedPlant,
@ -140,7 +140,7 @@ const CaseBuilder = () => {
return {
...prevData,
plants: {
Plants: {
...prevData.Plants,
[plantName]: {
...plant,
@ -159,7 +159,7 @@ const CaseBuilder = () => {
const updatedOutputs = [...center.output, productName];
return {
...prev,
centers: {
Centers: {
...prev.Centers,
[centerName]: { ...center, output: updatedOutputs },
},
@ -195,48 +195,20 @@ const CaseBuilder = () => {
});
};
const onRenamePlant = (uniqueId: string, newName: string) => {
setScenario((prev) => {
const plant = prev.Plants[uniqueId];
if (!plant) return prev;
const next = {
...prev,
plants: {
...prev.Plants,
[uniqueId]: { ...plant, name: newName },
},
};
return next;
});
};
const onRenameNode = (type: EntityKey, uniqueId: string, newName: string) => {
setScenario((prevData) => {
const entities = prevData[type];
const node = entities[uniqueId];
const onRenameProduct = (uniqueId: string, newName: string) => {
setScenario((prev) => {
const product = prev.Products[uniqueId];
if (!product) return prev;
const next = {
...prev,
products: {
...prev.Products,
[uniqueId]: { ...product, name: newName },
},
};
return next;
});
};
if (!node) return prevData;
const onRenameCenter = (uniqueId: string, newName: string) => {
setScenario((prev) => {
const center = prev.Centers[uniqueId];
if (!center) return prev;
const next = {
...prev,
centers: {
...prev.Centers,
[uniqueId]: { ...center, name: newName },
return {
...prevData,
[type]: {
...entities,
[uniqueId]: { ...node, name: newName },
},
};
return next;
});
};
@ -263,9 +235,9 @@ const CaseBuilder = () => {
onRemovePlant={(id) => onRemoveNode("Plants", id)}
onRemoveProduct={(id) => onRemoveNode("Products", id)}
onRemoveCenter={(id) => onRemoveNode("Centers", id)}
onRenamePlant={onRenamePlant}
onRenameProduct={onRenameProduct}
onRenameCenter={onRenameCenter}
onRenamePlant={(id, name) => onRenameNode("Plants", id, name)}
onRenameProduct={(id, name) => onRenameNode("Products", id, name)}
onRenameCenter={(id, name) => onRenameNode("Centers", id, name)}
/>
</div>
</div>

@ -1,17 +1,28 @@
import React, { useEffect, useCallback, useRef } from 'react';
import dagre from 'dagre';
import React, { useEffect, useCallback, useRef, useState } from "react";
import dagre from "dagre";
import {
ReactFlow,ReactFlowProvider, useNodesState, useEdgesState, Background,
Controls, Node, Edge, Connection, MarkerType,
ReactFlow,
ReactFlowInstance,
ReactFlowProvider,
useNodesState,
useEdgesState,
Background,
Controls,
Node,
Edge,
Connection,
addEdge,
MarkerType,
getNodesBounds,
getViewportForBounds,
useReactFlow} from '@xyflow/react';
import { PlantNode, ProductNode, CenterNode } from './CircularData';
import CustomNode, { CustomNodeData } from './NodesAndEdges';
import Section from '../Common/Section';
import Card from '../Common/Card';
import styles from './PipelineBlock.module.css';
import { toPng } from 'html-to-image';
} from "@xyflow/react";
import { PlantNode, ProductNode, CenterNode } from "./CircularData";
import CustomNode, { CustomNodeData } from "./NodesAndEdges";
import Section from "../Common/Section";
import Card from "../Common/Card";
import styles from "./PipelineBlock.module.css";
import buttonStyles from "./Button.module.css";
import HelpButton from "../Common/Buttons/HelpButton.module.css";
interface PipelineBlockProps {
onAddPlant: () => void;
@ -34,90 +45,94 @@ interface PipelineBlockProps {
plants: Record<string, PlantNode>;
centers: Record<string, CenterNode>;
}
function getLayouted(
nodes: Node<CustomNodeData>[],
edges: Edge[]
edges: Edge[],
): { nodes: Node<CustomNodeData>[]; edges: Edge[] } {
const W = 125, H = 45;
const W = 125,
H = 45;
const g = new dagre.graphlib.Graph();
g.setDefaultEdgeLabel(() => ({}));
g.setGraph({ rankdir: 'LR' });
nodes.forEach(n => g.setNode(n.id, { width: W, height: H }));
edges.forEach(e => g.setEdge(e.source, e.target));
g.setGraph({ rankdir: "LR" });
nodes.forEach((n) => g.setNode(n.id, { width: W, height: H }));
edges.forEach((e) => g.setEdge(e.source, e.target));
dagre.layout(g);
return {
nodes: nodes.map(n => {
nodes: nodes.map((n) => {
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 mapRef = useRef<Record<string, 'plant'|'product'|'center'>>({});
const PipelineBlock: React.FC<PipelineBlockProps> = (props) => {
const mapRef = useRef<Record<string, "plant" | "product" | "center">>({});
const flowWrapper = useRef<HTMLDivElement>(null);
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge<CustomNodeData>>(
[],
);
const rebuild = useCallback(() => {
const m: Record<string, 'plant'|'product'|'center'> = {};
const m: Record<string, "plant" | "product" | "center"> = {};
const newNodes: Node<CustomNodeData>[] = [];
const newEdges: Edge[] = [];
Object.entries(props.products).forEach(([key, p]) => {
m[key] = 'product';
m[key] = "product";
newNodes.push({
id: p.uid,
type: 'default',
data: { label: p.name, type: 'product' },
type: "default",
data: { label: p.name, type: "product" },
position: { x: p.x, y: p.y },
className: 'ProductNode'
className: "ProductNode",
});
});
Object.entries(props.plants).forEach(([key, pl]) => {
m[key] = 'plant';
m[key] = "plant";
newNodes.push({
id: pl.uid,
type: 'default',
data: { label: pl.name, type: 'plant' },
type: "default",
data: { label: pl.name, type: "plant" },
position: { x: pl.x, y: pl.y },
className: 'PlantNode'
className: "PlantNode",
});
pl.inputs.forEach(input => {
pl.inputs.forEach((input) => {
newEdges.push({
id: `${input}-${key}-in`,
source: input,
target: key,
animated: true,
style: { stroke: 'black' },
markerEnd: { type: MarkerType.ArrowClosed }
style: { stroke: "black" },
markerEnd: { type: MarkerType.ArrowClosed },
});
});
pl.outputs.forEach(output => {
pl.outputs.forEach((output) => {
newEdges.push({
id: `${key}-${output}-out`,
source: key,
target: output,
animated: true,
style: { stroke: 'black' },
markerEnd: { type: MarkerType.ArrowClosed }
style: { stroke: "black" },
markerEnd: { type: MarkerType.ArrowClosed },
});
});
});
Object.entries(props.centers).forEach(([key, c]) => {
m[key] = 'center';
m[key] = "center";
newNodes.push({
id: c.uid,
type: 'default',
data: { label: c.name, type: 'center' },
type: "default",
data: { label: c.name, type: "center" },
position: { x: c.x, y: c.y },
className: 'CenterNode'
className: "CenterNode",
});
if (c.input) {
@ -126,145 +141,185 @@ const PipelineBlock: React.FC<PipelineBlockProps> = props => {
source: c.input,
target: key,
animated: true,
style: { stroke: 'black' },
markerEnd: { type: MarkerType.ArrowClosed }
style: { stroke: "black" },
markerEnd: { type: MarkerType.ArrowClosed },
});
}
c.output.forEach(o => {
c.output.forEach((o) => {
newEdges.push({
id: `${key}-${o}-out`,
source: key,
target: o,
animated: true,
style: { stroke: 'black' },
markerEnd: { type: MarkerType.ArrowClosed }
style: { stroke: "black" },
markerEnd: { type: MarkerType.ArrowClosed },
});
});
});
mapRef.current = m;
setNodes(newNodes);
setEdges(newEdges);
}, [
}, [props.products, props.plants, props.centers, setNodes, setEdges]);
props.products,
props.plants,
props.centers,
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);
useEffect(() => {
rebuild();
}, [rebuild]);
const onConnect = (connection: Connection) => {
const { source: s, target: t } = connection;
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, n: Node<CustomNodeData>) => {
const { id, position, data } = n;
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);
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 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 oldName = n.data.label;
const newName = window.prompt('Enter new name', oldName);
console.log('after rename', newName);
const newName = window.prompt("Enter new name", oldName);
console.log("after rename", newName);
const uniqueId = n.id;
if (!newName || newName===oldName) return;
if (n.data.type==='plant') props.onRenamePlant(uniqueId, newName);
if (n.data.type==='product') props.onRenameProduct(uniqueId, newName);
if (n.data.type==='center') props.onRenameCenter(uniqueId, newName);
if (!newName || newName === oldName) return;
if (n.data.type === "plant") props.onRenamePlant(uniqueId, newName);
if (n.data.type === "product") props.onRenameProduct(uniqueId, newName);
if (n.data.type === "center") props.onRenameCenter(uniqueId, newName);
};
function DownloadButton() {
const onDownload = async () => {
if (!flowWrapper.current) return;
const node = flowWrapper.current;
const { width, height } = node.getBoundingClientRect();
const dataUrl = await toPng(node, {
backgroundColor: '#fff',
width: Math.round(width),
height: Math.round(height)
});
const downloadLink = document.createElement('a');
downloadLink.href = dataUrl;
downloadLink.download = 'pipeline.png';
downloadLink.click();
if (!rfInstance || !flowWrapper.current) return;
const minZoom = 0.1;
const maxZoom = 2;
const padding = 0.1;
const nodes = rfInstance.getNodes();
const bounds = getNodesBounds(nodes);
const { clientWidth: width, clientHeight: height } = flowWrapper.current;
const { x, y, zoom } = getViewportForBounds(
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 { nodes: ln, edges: le } = getLayouted(nodes, edges);
ln.forEach(n => {
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);
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);
});
};
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>
</ReactFlowProvider>
</Card>
</>
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
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;

@ -5,14 +5,12 @@
*/
.tooltip {
visibility: hidden;
background-color: var(--contrast-80);
color: var(--contrast-10);
opacity: 0;
width: 250px;
margin-top: 36px;
margin-left: -250px;
position: absolute;
z-index: 100;
font-size: 14px;
border-radius: var(--border-radius);
@ -22,6 +20,7 @@
font-weight: normal;
text-align: left;
padding: 6px 12px;
position: relative;
}
.icon {
@ -34,6 +33,7 @@
border: 0;
background-color: transparent;
cursor: pointer;
position: relative;
}
.HelpButton:hover .tooltip {

@ -2,7 +2,7 @@
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"allowJs": true,
"allowSyntheticDefaultImports": true,
"alwaysStrict": true,
"esModuleInterop": true,
@ -29,7 +29,7 @@
"noUncheckedIndexedAccess": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"checkJs": false,
"checkJs": true,
"allowImportingTsExtensions": true
},
"include": ["src"]

Loading…
Cancel
Save