mirror of https://github.com/ANL-CEEESA/RELOG.git
Merge 778e901f44
into 157cd500ef
commit
1d3ee7c55d
@ -0,0 +1,330 @@
|
||||
{
|
||||
"name": "RELOG",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@xyflow/react": "^12.7.0",
|
||||
"i": "^0.3.7",
|
||||
"nanoid": "^5.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-drag": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
||||
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-selection": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
||||
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-transition": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
||||
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-zoom": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
|
||||
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-interpolate": "*",
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
|
||||
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "19.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz",
|
||||
"integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/react": {
|
||||
"version": "12.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.7.0.tgz",
|
||||
"integrity": "sha512-U6VMEbYjiCg1byHrR7S+b5ZdHTjgCFX4KpBc634G/WtEBUvBLoMQdlCD6uJHqodnOAxpt3+G2wiDeTmXAFJzgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xyflow/system": "0.0.62",
|
||||
"classcat": "^5.0.3",
|
||||
"zustand": "^4.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/system": {
|
||||
"version": "0.0.62",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.62.tgz",
|
||||
"integrity": "sha512-Z2ufbnvuYxIOCGyzE/8eX8TAEM8Lpzc/JafjD1Tzy6ZJs/E7KGVU17Q1F5WDHVW+dbztJAdyXMG0ejR9bwSUAA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-drag": "^3.0.7",
|
||||
"@types/d3-interpolate": "^3.0.4",
|
||||
"@types/d3-selection": "^3.0.10",
|
||||
"@types/d3-transition": "^3.0.8",
|
||||
"@types/d3-zoom": "^3.0.8",
|
||||
"d3-drag": "^3.0.0",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-zoom": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/classcat": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
|
||||
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-dispatch": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-drag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
||||
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-selection": "3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-selection": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-transition": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
||||
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-ease": "1 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-timer": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"d3-selection": "2 - 3"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-zoom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
||||
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-drag": "2 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-selection": "2 - 3",
|
||||
"d3-transition": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/i": {
|
||||
"version": "0.3.7",
|
||||
"resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz",
|
||||
"integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz",
|
||||
"integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
||||
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@xyflow/react": "^12.7.0",
|
||||
"i": "^0.3.7",
|
||||
"nanoid": "^5.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6"
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
export interface PlantNode {
|
||||
uid: string;
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
inputs: string[];
|
||||
outputs: string[];
|
||||
|
||||
|
||||
}
|
||||
|
||||
export interface ProductNode {
|
||||
uid: string;
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
|
||||
export interface CenterNode {
|
||||
uid: string;
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
|
||||
//single input, multiple outputs
|
||||
input?: string;
|
||||
output: string[];
|
||||
}
|
||||
|
||||
|
||||
export interface InitialData {
|
||||
plants: Record<string, PlantNode>;
|
||||
products: Record<string, ProductNode>;
|
||||
centers: Record<string, CenterNode>;
|
||||
|
||||
|
||||
}
|
||||
|
||||
export interface RELOGScenario {
|
||||
Parameters: {
|
||||
version: string;
|
||||
};
|
||||
|
||||
Plants: Record<string, PlantNode>;
|
||||
Products: Record< string, ProductNode>;
|
||||
Centers: Record<string,CenterNode>;
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
// NodesAndEdges.tsx
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Handle, Position, NodeProps } from '@xyflow/react';
|
||||
|
||||
import styles from './PipelineBlock.module.css';
|
||||
|
||||
export interface CustomNodeData {
|
||||
|
||||
[key:string]: unknown;
|
||||
|
||||
label: string;
|
||||
|
||||
type: 'plant' | 'product' | 'center';
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default function CustomNode({ data, isConnectable }: NodeProps<Node<CustomNodeData>>) {
|
||||
const typeClass =
|
||||
data.type === 'plant' ? styles.PlantNode :
|
||||
data.type === 'product' ? styles.ProductNode:
|
||||
styles.CenterNode;
|
||||
|
||||
return (
|
||||
<div className={`${styles.node} ${typeClass}`}>
|
||||
<Handle
|
||||
|
||||
type="target"
|
||||
|
||||
position={Position.Left}
|
||||
|
||||
isConnectable={isConnectable}
|
||||
|
||||
style={{ background: '#555' }}
|
||||
|
||||
/>
|
||||
|
||||
<div>{data.label}</div>
|
||||
|
||||
<Handle
|
||||
|
||||
type="source"
|
||||
|
||||
position={Position.Right}
|
||||
|
||||
isConnectable={isConnectable}
|
||||
|
||||
style={{ background: '#555' }}
|
||||
|
||||
/>
|
||||
</div>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,41 @@
|
||||
.PipelineBlock {
|
||||
height: 800px !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1) !important;
|
||||
border-radius: var(--border-radius) !important;
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
|
||||
:global(.react-flow__node.PlantNode.selected),
|
||||
:global(.react-flow__node.ProductNode.selected),
|
||||
:global(.react-flow__node.CenterNode.selected){
|
||||
border: 2px solid #000 !important;
|
||||
|
||||
}
|
||||
|
||||
:global(.react-flow__node.PlantNode),
|
||||
:global(.react-flow__node.ProductNode),
|
||||
:global(.react-flow__node.CenterNode) {
|
||||
border-color: rgba(0, 0, 0, 0.8) !important;
|
||||
color: black !important;
|
||||
font-size: 13px !important;
|
||||
border-width: 1px !important;
|
||||
border-radius: 6px !important;
|
||||
box-shadow: 0px 2px 4px -3px black !important;
|
||||
width: 140px !important;
|
||||
height: 40px !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
|
||||
}
|
||||
|
||||
:global(.react-flow__node.PlantNode) {
|
||||
--xy-node-background-color: #8d8 !important;
|
||||
}
|
||||
:global(.react-flow__node.ProductNode) {
|
||||
--xy-node-background-color: #e6e6e6 !important;
|
||||
}
|
||||
:global(.react-flow__node.CenterNode) {
|
||||
--xy-node-background-color: #d3a610 !important;
|
||||
}
|
@ -0,0 +1,312 @@
|
||||
import React, { useEffect, useCallback, useRef, useState } from "react";
|
||||
import dagre from "dagre";
|
||||
import {
|
||||
ReactFlow,
|
||||
ReactFlowInstance,
|
||||
ReactFlowProvider,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
Background,
|
||||
Controls,
|
||||
Node,
|
||||
Edge,
|
||||
Connection,
|
||||
MarkerType,
|
||||
} from "@xyflow/react";
|
||||
import { PlantNode, ProductNode, CenterNode } from "./InitialData";
|
||||
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";
|
||||
import * as htmlToImage from "html-to-image";
|
||||
|
||||
interface PipelineBlockProps {
|
||||
onAddPlant: () => void;
|
||||
onAddProduct: () => void;
|
||||
onAddCenter: () => 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;
|
||||
onAddPlantOutput: (plantName: string, productName: 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;
|
||||
onRenameProduct: (uid: string, newName: string) => void;
|
||||
onRenamePlant: (uid: string, newName: string) => void;
|
||||
onRenameCenter: (uid: string, newName: string) => void;
|
||||
products: Record<string, ProductNode>;
|
||||
plants: Record<string, PlantNode>;
|
||||
centers: Record<string, CenterNode>;
|
||||
}
|
||||
|
||||
function getLayouted(
|
||||
nodes: Node<CustomNodeData>[],
|
||||
edges: Edge[],
|
||||
): { nodes: Node<CustomNodeData>[]; edges: Edge[] } {
|
||||
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));
|
||||
dagre.layout(g);
|
||||
return {
|
||||
nodes: nodes.map((n) => {
|
||||
const d = g.node(n.id)!;
|
||||
return { ...n, position: { x: d.x - W / 2, y: d.y - H / 2 } };
|
||||
}),
|
||||
edges,
|
||||
};
|
||||
}
|
||||
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<Edge<CustomNodeData>>(
|
||||
[],
|
||||
);
|
||||
|
||||
const rebuild = useCallback(() => {
|
||||
const m: Record<string, "plant" | "product" | "center"> = {};
|
||||
const newNodes: Node<CustomNodeData>[] = [];
|
||||
const newEdges: Edge[] = [];
|
||||
Object.entries(props.products).forEach(([key, p]) => {
|
||||
m[key] = "product";
|
||||
newNodes.push({
|
||||
id: p.uid,
|
||||
type: "default",
|
||||
data: { label: p.name, type: "product" },
|
||||
position: { x: p.x, y: p.y },
|
||||
className: "ProductNode",
|
||||
});
|
||||
});
|
||||
|
||||
Object.entries(props.plants).forEach(([key, pl]) => {
|
||||
m[key] = "plant";
|
||||
newNodes.push({
|
||||
id: pl.uid,
|
||||
type: "default",
|
||||
data: { label: pl.name, type: "plant" },
|
||||
position: { x: pl.x, y: pl.y },
|
||||
className: "PlantNode",
|
||||
});
|
||||
|
||||
pl.inputs.forEach((input) => {
|
||||
newEdges.push({
|
||||
id: `${input}-${key}-in`,
|
||||
source: input,
|
||||
target: key,
|
||||
animated: true,
|
||||
style: { stroke: "black" },
|
||||
markerEnd: { type: MarkerType.ArrowClosed },
|
||||
});
|
||||
});
|
||||
|
||||
pl.outputs.forEach((output) => {
|
||||
newEdges.push({
|
||||
id: `${key}-${output}-out`,
|
||||
source: key,
|
||||
target: output,
|
||||
animated: true,
|
||||
style: { stroke: "black" },
|
||||
markerEnd: { type: MarkerType.ArrowClosed },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Object.entries(props.centers).forEach(([key, c]) => {
|
||||
m[key] = "center";
|
||||
newNodes.push({
|
||||
id: c.uid,
|
||||
type: "default",
|
||||
data: { label: c.name, type: "center" },
|
||||
position: { x: c.x, y: c.y },
|
||||
className: "CenterNode",
|
||||
});
|
||||
|
||||
if (c.input) {
|
||||
newEdges.push({
|
||||
id: `${c.input}-${key}-in`,
|
||||
source: c.input,
|
||||
target: key,
|
||||
animated: true,
|
||||
style: { stroke: "black" },
|
||||
markerEnd: { type: MarkerType.ArrowClosed },
|
||||
});
|
||||
}
|
||||
|
||||
c.output.forEach((o) => {
|
||||
newEdges.push({
|
||||
id: `${key}-${o}-out`,
|
||||
source: key,
|
||||
target: o,
|
||||
animated: true,
|
||||
style: { stroke: "black" },
|
||||
markerEnd: { type: MarkerType.ArrowClosed },
|
||||
});
|
||||
});
|
||||
});
|
||||
mapRef.current = m;
|
||||
setNodes(newNodes);
|
||||
setEdges(newEdges);
|
||||
}, [props.products, props.plants, props.centers, setNodes, setEdges]);
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
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 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);
|
||||
};
|
||||
function DownloadButton() {
|
||||
const onDownload = async () => {
|
||||
if (!rfInstance || !flowWrapper.current) return;
|
||||
rfInstance.fitView({ padding: 0.1 });
|
||||
|
||||
const renderer = document.getElementsByClassName(
|
||||
"react-flow__renderer",
|
||||
)[0] as HTMLElement;
|
||||
if (!renderer) return;
|
||||
|
||||
const dataurl = await htmlToImage.toSvg(renderer, {
|
||||
filter: (node: Element) => node.tagName.toLowerCase() !== "i",
|
||||
});
|
||||
|
||||
const printWin = window.open("", "_blank");
|
||||
if (!printWin) return;
|
||||
printWin.document.write(
|
||||
"<html><head> <style> @page {size: A4 landscape; margin: 0; body {margin: 0}</style> </head> <body>${rawSvg}</body> </html>",
|
||||
);
|
||||
printWin.document.close();
|
||||
printWin.onload = () => printWin.print();
|
||||
};
|
||||
return (
|
||||
<button className={buttonStyles.Button} onClick={onDownload}>
|
||||
Export Pipeline
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const onLayout = () => {
|
||||
const { nodes: ln, edges: le } = getLayouted(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);
|
||||
});
|
||||
};
|
||||
|
||||
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;
|
@ -0,0 +1,34 @@
|
||||
import { InitialData, PlantNode, ProductNode, CenterNode } from "./InitialData";
|
||||
|
||||
|
||||
|
||||
export const defaultProduct: ProductNode = {
|
||||
uid: "",
|
||||
name: "",
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
|
||||
export const defaultPlant: PlantNode = {
|
||||
uid: "",
|
||||
name: "",
|
||||
x: 0,
|
||||
y: 0,
|
||||
inputs : [],
|
||||
outputs: [],
|
||||
};
|
||||
|
||||
export const defaultCenter: CenterNode = {
|
||||
uid: "",
|
||||
name: "",
|
||||
x: 0,
|
||||
y: 0,
|
||||
output: [],
|
||||
};
|
||||
|
||||
export const DefaultData: InitialData = {
|
||||
products: {},
|
||||
plants: {},
|
||||
centers: {}
|
||||
};
|
||||
|
@ -1,43 +1,60 @@
|
||||
/*
|
||||
* RELOG: Supply Chain Analysis and Optimization
|
||||
* Copyright (C) 2020-2025, UChicago Argonne, LLC. All rights reserved.
|
||||
* Released under the modified BSD license. See COPYING.md for more details.
|
||||
*/
|
||||
.HelpButton {
|
||||
padding: 6px 36px;
|
||||
margin: 12px 6px;
|
||||
line-height: 24px;
|
||||
border: var(--box-border);
|
||||
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%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
.HelpButton:hover {
|
||||
background: rgb(245, 245, 245);
|
||||
}
|
||||
|
||||
|
||||
.HelpButton:active {
|
||||
background: rgba(220, 220, 220);
|
||||
}
|
||||
|
||||
|
||||
.tooltip {
|
||||
visibility: hidden;
|
||||
background-color: var(--contrast-80);
|
||||
color: var(--contrast-10);
|
||||
background-color: #333;
|
||||
color: white;
|
||||
opacity: 0;
|
||||
width: 250px;
|
||||
width: 180px;
|
||||
margin-top: 36px;
|
||||
margin-left: -250px;
|
||||
margin-left: -90px;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
font-size: 14px;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow);
|
||||
line-height: 20px;
|
||||
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 12px;
|
||||
transition: opacity 0.5s;
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--contrast-60);
|
||||
font-size: 16px;
|
||||
padding: 8px 8px 8px 0;
|
||||
}
|
||||
|
||||
.HelpButton {
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.HelpButton:hover .tooltip {
|
||||
visibility: visible;
|
||||
opacity: 100%;
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
|
||||
.HelpButton:disabled {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
cursor: default;
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
.Card {
|
||||
border: var(--box-border);
|
||||
box-shadow: var(--box-shadow);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: white;
|
||||
padding: 12px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.Card h1 {
|
||||
margin: 12px -12px 0px -12px;
|
||||
padding: 6px 12px 0px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 35px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.Card h1:first-child {
|
||||
margin: -12px -12px 0px -12px;
|
||||
border-top: none;
|
||||
background: none;
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import styles from "./Card.module.css";
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
|
||||
interface CardProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const Card: React.FC<CardProps> = ({children}) => {
|
||||
return <div className={styles.Card}>{children}</div>;
|
||||
};
|
||||
|
||||
export default Card;
|
@ -0,0 +1,6 @@
|
||||
.Section {
|
||||
line-height: 36px;
|
||||
margin: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import styles from "./Section.module.css";
|
||||
|
||||
interface SectionProps {
|
||||
title: string;
|
||||
}
|
||||
|
||||
const Section = ({ title }: SectionProps) => {
|
||||
return <h2 className={styles.Section}>{title}</h2>;
|
||||
};
|
||||
|
||||
export default Section;
|
@ -0,0 +1,5 @@
|
||||
// src/cssmodules.d.ts
|
||||
declare module '*.module.css' {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
:root {
|
||||
--site-width: 1200px;
|
||||
--box-border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
--box-shadow: 0px 2px 4px -3px rgba(0, 0, 0, 0.2);
|
||||
--border-radius: 4px;
|
||||
--primary: #0d6efd;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #333;
|
||||
color: rgba(0, 0, 0, 0.95);
|
||||
}
|
||||
|
||||
#contentBackground {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
#content {
|
||||
max-width: var(--site-width);
|
||||
min-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 1px 6px 32px 6px;
|
||||
}
|
||||
|
||||
.react-flow__node.selected {
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2) !important;
|
||||
border-width: 2px !important;
|
||||
margin-top: -1px !important;
|
||||
margin-left: -1px !important;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
.react-flow__handle {
|
||||
width: 6px !important;
|
||||
height: 6px !important;
|
||||
background-color: white !important;
|
||||
border: 1px solid black !important;
|
||||
}
|
||||
|
||||
.react-flow__handle:hover {
|
||||
background-color: black !important;
|
||||
}
|
||||
|
||||
.react-flow__handle-right {
|
||||
right: -4px !important;
|
||||
}
|
||||
|
||||
.react-flow__handle-left {
|
||||
left: -4px !important;
|
||||
}
|
||||
|
||||
#messageTray {
|
||||
max-width: var(--site-width);
|
||||
margin: 0 auto;
|
||||
position: fixed;
|
||||
bottom: 12px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
#messageTray .message {
|
||||
background-color: rgb(221, 69, 69);
|
||||
color: #eee;
|
||||
padding: 12px;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
#messageTray .message p {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
#messageTray .message button {
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
border: 1px solid #eee;
|
||||
color: #eee;
|
||||
float: right;
|
||||
padding: 0 24px;
|
||||
line-height: 6px;
|
||||
}
|
||||
|
||||
#messageTray .message button:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
#messageTray .message button:active {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.nodata {
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
color: #888;
|
||||
margin: 0;
|
||||
}
|
Loading…
Reference in new issue