pull/33/merge
Obaid Khwaja 5 days ago committed by GitHub
commit 1d3ee7c55d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

330
package-lock.json generated

@ -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"
}
}

329
web/package-lock.json generated

@ -20,20 +20,24 @@
"@types/node": "^16.18.126",
"@types/pako": "^2.0.3",
"@types/papaparse": "^5.3.16",
"@types/react": "^19.1.3",
"@types/react-dom": "^19.1.3",
"@xyflow/react": "^12.7.1",
"ajv": "^8.17.1",
"dagre": "^0.8.5",
"eslint": "^8.57.1",
"html-to-image": "^1.11.13",
"pako": "^2.1.0",
"papaparse": "^5.5.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-scripts": "^5.0.1",
"tabulator-tables": "^6.3.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"devDependencies": {
"@types/dagre": "^0.7.53",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@types/tabulator-tables": "^6.2.6",
"prettier": "3.5.3"
}
@ -3812,6 +3816,62 @@
"@types/node": "*"
}
},
"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/dagre": {
"version": "0.7.53",
"resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.53.tgz",
"integrity": "sha512-f4gkWqzPZvYmKhOsDnhq/R8mO4UMcKdxZo+i5SCkOU1wvGeHJeUXGIHeE9pnwGyPMDof1Vx5ZQo4nxpeg2TTVQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/eslint": {
"version": "8.56.12",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.12.tgz",
@ -3998,6 +4058,13 @@
"integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==",
"license": "MIT"
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/q": {
"version": "1.5.8",
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz",
@ -4017,21 +4084,24 @@
"license": "MIT"
},
"node_modules/@types/react": {
"version": "19.1.4",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz",
"integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==",
"version": "18.3.23",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz",
"integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
"version": "19.1.5",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz",
"integrity": "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==",
"version": "18.3.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.0.0"
"@types/react": "^18.0.0"
}
},
"node_modules/@types/resolve": {
@ -4530,6 +4600,38 @@
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"license": "Apache-2.0"
},
"node_modules/@xyflow/react": {
"version": "12.7.1",
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.7.1.tgz",
"integrity": "sha512-uvIPQIZdf8tt0mDWvhkEpg/7t5E/e/KE4RWjNczAEhEYA+uvLc+4A5kIPJqCjJJbVHfMiAojT5JOB5mB7/EgFw==",
"license": "MIT",
"dependencies": {
"@xyflow/system": "0.0.63",
"classcat": "^5.0.3",
"zustand": "^4.4.0"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@xyflow/system": {
"version": "0.0.63",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.63.tgz",
"integrity": "sha512-lCZRh5o7RCPE7iNe3yKzV8UuS4hijVIWJ9nbQh9eowsRJOwgy5KlUnZ3Q43SOlRsZnOht8px5phpsjBHPRn+oQ==",
"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/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@ -5832,6 +5934,12 @@
"integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
"license": "MIT"
},
"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/clean-css": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
@ -6598,8 +6706,124 @@
"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/dagre": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz",
"integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
"license": "MIT",
"dependencies": {
"graphlib": "^2.1.8",
"lodash": "^4.17.15"
}
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@ -8952,6 +9176,15 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"license": "MIT"
},
"node_modules/graphlib": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz",
"integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.15"
}
},
"node_modules/gzip-size": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
@ -9187,6 +9420,12 @@
"node": ">=12"
}
},
"node_modules/html-to-image": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz",
"integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==",
"license": "MIT"
},
"node_modules/html-webpack-plugin": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz",
@ -14079,10 +14318,13 @@
}
},
"node_modules/react": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
@ -14149,15 +14391,16 @@
}
},
"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==",
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.26.0"
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^19.1.0"
"react": "^18.3.1"
}
},
"node_modules/react-error-overlay": {
@ -14857,10 +15100,13 @@
}
},
"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"
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/schema-utils": {
"version": "4.3.2",
@ -16769,6 +17015,15 @@
"requires-port": "^1.0.0"
}
},
"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/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -17741,6 +17996,34 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"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
}
}
}
}
}

@ -15,14 +15,15 @@
"@types/node": "^16.18.126",
"@types/pako": "^2.0.3",
"@types/papaparse": "^5.3.16",
"@types/react": "^19.1.3",
"@types/react-dom": "^19.1.3",
"@xyflow/react": "^12.7.1",
"ajv": "^8.17.1",
"dagre": "^0.8.5",
"eslint": "^8.57.1",
"html-to-image": "^1.11.13",
"pako": "^2.1.0",
"papaparse": "^5.5.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-scripts": "^5.0.1",
"tabulator-tables": "^6.3.1",
"typescript": "^4.9.5",
@ -59,6 +60,9 @@
]
},
"devDependencies": {
"@types/dagre": "^0.7.53",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@types/tabulator-tables": "^6.2.6",
"prettier": "3.5.3"
}

@ -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;
}

@ -9,16 +9,239 @@ import Header from "./Header";
import "tabulator-tables/dist/css/tabulator.min.css";
import "../Common/Forms/Tables.css";
import Footer from "./Footer";
import React, { useState, useRef } from "react";
import { defaultPlant, defaultProduct, defaultCenter } from "./defaults";
import PipelineBlock from "./PipelineBlock";
import "@xyflow/react/dist/style.css";
import {
PlantNode,
CenterNode,
ProductNode,
RELOGScenario,
} from "./InitialData";
import { idText } from "typescript";
declare global {
interface Window {
nextX: number;
nextY: number;
}
}
const Default_Scenario: RELOGScenario = {
Parameters: { version: "1.0" },
Plants: {},
Products: {},
Centers: {},
};
const CaseBuilder = () => {
const nextUid = useRef(1);
const [scenario, setScenario] = useState<RELOGScenario>(Default_Scenario);
const onClear = () => {};
const onSave = () => {};
const onLoad = () => {};
const nextNodePosition = (): [number, number] => {
if (window.nextX === undefined) window.nextX = 15;
if (window.nextY === undefined) window.nextY = 15;
window.nextY += 60;
if (window.nextY >= 500) {
window.nextY = 15;
window.nextX += 150;
}
return [window.nextX, window.nextY];
};
const promptName = (): string | undefined => {
const name = prompt("Name");
if (!name || name.length === 0) return;
return name;
};
type EntityKey = "Plants" | "Products" | "Centers";
const onAddNode = (type: EntityKey) => {
setScenario((prevData) => {
const name = promptName();
if (!name) return prevData;
const uid = `${name}-$${nextUid.current++}`;
const [x, y] = nextNodePosition();
let newNode;
if (type === "Plants") {
newNode = { ...defaultPlant, uid, name, x, y };
} else if (type === "Products") {
newNode = { ...defaultProduct, uid, name, x, y };
} else {
newNode = { ...defaultCenter, uid, name, x, y };
}
return {
...prevData,
[type]: {
...prevData[type],
[uid]: newNode,
},
} as RELOGScenario;
});
};
const onSetCenterInput = (centerName: string, productName: string) => {
setScenario((prev) => {
const center = prev.Centers[centerName];
if (!center) return prev;
return {
...prev,
Centers: {
...prev.Centers,
[centerName]: { ...center, input: productName },
},
};
});
};
const onSetPlantInput = (plantName: string, productName: string) => {
setScenario((prevData: RELOGScenario) => {
const plant = prevData.Plants[plantName];
if (!plant) return prevData;
const updatedPlant: PlantNode = {
...plant,
inputs: plant.inputs.includes(productName)
? plant.inputs
: [...plant.inputs, productName],
};
return {
...prevData,
Plants: {
...prevData.Plants,
[plantName]: updatedPlant,
},
};
});
};
const onAddPlantOutput = (plantName: string, productName: string) => {
setScenario((prevData) => {
const plant = prevData.Plants[plantName];
if (!plant) return prevData;
const newOutputs = plant.outputs.includes(productName)
? plant.outputs
: [...plant.outputs, productName];
return {
...prevData,
Plants: {
...prevData.Plants,
[plantName]: {
...plant,
outputs: newOutputs,
},
},
};
});
};
const onAddCenterOutput = (centerName: string, productName: string) => {
setScenario((prev) => {
const center = prev.Centers[centerName];
if (!center) return prev;
const updatedOutputs = [...center.output, productName];
return {
...prev,
Centers: {
...prev.Centers,
[centerName]: { ...center, output: updatedOutputs },
},
};
});
};
const onMoveNode = (type: EntityKey, id: string, x: number, y: number) => {
setScenario((prevData) => {
const nodesMap = prevData[type];
const node = nodesMap[id];
if (!node) return prevData;
return {
...prevData,
[type]: {
...nodesMap,
[id]: { ...node, x, y },
},
} as RELOGScenario;
});
};
const onRemoveNode = (type: EntityKey, id: string) => {
setScenario((prevData) => {
const nodesMap = { ...prevData[type] };
delete nodesMap[id];
return {
...prevData,
[type]: nodesMap,
};
});
};
const onRenameNode = (type: EntityKey, uniqueId: string, newName: string) => {
setScenario((prevData) => {
const entities = prevData[type];
const node = entities[uniqueId];
if (!node) return prevData;
return {
...prevData,
[type]: {
...entities,
[uniqueId]: { ...node, name: newName },
},
};
});
};
return (
<div>
<Header onClear={onClear} onSave={onSave} onLoad={onLoad} />
<div className="content"></div>
<div className="content">
<div id="contentBackground">
<div id="content">
<PipelineBlock
onAddPlant={() => onAddNode("Plants")}
onAddProduct={() => onAddNode("Products")}
onMovePlant={(id, x, y) => onMoveNode("Plants", id, x, y)}
onMoveProduct={(id, x, y) => onMoveNode("Products", id, x, y)}
plants={scenario.Plants}
products={scenario.Products}
onSetPlantInput={onSetPlantInput}
onAddPlantOutput={onAddPlantOutput}
onAddCenter={() => onAddNode("Centers")}
onAddCenterInput={onSetCenterInput}
onAddCenterOutput={onAddCenterOutput}
onMoveCenter={(id, x, y) => onMoveNode("Centers", id, x, y)}
centers={scenario.Centers}
onRemovePlant={(id) => onRemoveNode("Plants", id)}
onRemoveProduct={(id) => onRemoveNode("Products", id)}
onRemoveCenter={(id) => onRemoveNode("Centers", id)}
onRenamePlant={(id, name) => onRenameNode("Plants", id, name)}
onRenameProduct={(id, name) => onRenameNode("Products", id, name)}
onRenameCenter={(id, name) => onRenameNode("Centers", id, name)}
/>
</div>
</div>
</div>
<Footer />
</div>
);

@ -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;
}

@ -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,8 @@
"noUncheckedIndexedAccess": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"checkJs": true
"checkJs": true,
"allowImportingTsExtensions": true
},
"include": ["src"]
}

Loading…
Cancel
Save