diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..84ebd4b --- /dev/null +++ b/package-lock.json @@ -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 + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9b76d7d --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/web/package-lock.json b/web/package-lock.json index 5a66f8e..a225849 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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 + } + } } } } diff --git a/web/package.json b/web/package.json index 1cd27c5..1a76852 100644 --- a/web/package.json +++ b/web/package.json @@ -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" } diff --git a/web/src/components/CaseBuilder/Button.module.css b/web/src/components/CaseBuilder/Button.module.css new file mode 100644 index 0000000..fd956dd --- /dev/null +++ b/web/src/components/CaseBuilder/Button.module.css @@ -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; +} \ No newline at end of file diff --git a/web/src/components/CaseBuilder/CaseBuilder.tsx b/web/src/components/CaseBuilder/CaseBuilder.tsx index 214ffdf..c00efd8 100644 --- a/web/src/components/CaseBuilder/CaseBuilder.tsx +++ b/web/src/components/CaseBuilder/CaseBuilder.tsx @@ -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(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 (
-
+
+
+
+ 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)} + /> +
+
+
); diff --git a/web/src/components/CaseBuilder/InitialData.ts b/web/src/components/CaseBuilder/InitialData.ts new file mode 100644 index 0000000..54bec93 --- /dev/null +++ b/web/src/components/CaseBuilder/InitialData.ts @@ -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; + products: Record; + centers: Record; + + +} + +export interface RELOGScenario { + Parameters: { + version: string; + }; + + Plants: Record; + Products: Record< string, ProductNode>; + Centers: Record; +} \ No newline at end of file diff --git a/web/src/components/CaseBuilder/NodesAndEdges.tsx b/web/src/components/CaseBuilder/NodesAndEdges.tsx new file mode 100644 index 0000000..b5713b0 --- /dev/null +++ b/web/src/components/CaseBuilder/NodesAndEdges.tsx @@ -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>) { + const typeClass = + data.type === 'plant' ? styles.PlantNode : + data.type === 'product' ? styles.ProductNode: + styles.CenterNode; + + return ( +
+ + +
{data.label}
+ + +
+ + ); + +} + + \ No newline at end of file diff --git a/web/src/components/CaseBuilder/PipelineBlock.module.css b/web/src/components/CaseBuilder/PipelineBlock.module.css new file mode 100644 index 0000000..8ce60db --- /dev/null +++ b/web/src/components/CaseBuilder/PipelineBlock.module.css @@ -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; +} \ No newline at end of file diff --git a/web/src/components/CaseBuilder/PipelineBlock.tsx b/web/src/components/CaseBuilder/PipelineBlock.tsx new file mode 100644 index 0000000..c6ad8ca --- /dev/null +++ b/web/src/components/CaseBuilder/PipelineBlock.tsx @@ -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; + plants: Record; + centers: Record; +} + +function getLayouted( + nodes: Node[], + edges: Edge[], +): { nodes: Node[]; 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 = (props) => { + const mapRef = useRef>({}); + + const flowWrapper = useRef(null); + const [rfInstance, setRfInstance] = useState(null); + + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState>( + [], + ); + + const rebuild = useCallback(() => { + const m: Record = {}; + const newNodes: Node[] = []; + 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) => { + 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[]) => { + 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) => { + 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( + " ${rawSvg} ", + ); + printWin.document.close(); + printWin.onload = () => printWin.print(); + }; + return ( + + ); + } + + 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 ( + <> +
+ + +
+ + + + +
+ +
+ + + + + + +
+
+
+ + ); +}; + +export default PipelineBlock; diff --git a/web/src/components/CaseBuilder/defaults.ts b/web/src/components/CaseBuilder/defaults.ts new file mode 100644 index 0000000..70d5b19 --- /dev/null +++ b/web/src/components/CaseBuilder/defaults.ts @@ -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: {} +}; + diff --git a/web/src/components/Common/Buttons/HelpButton.module.css b/web/src/components/Common/Buttons/HelpButton.module.css index c26d1e5..87358b2 100644 --- a/web/src/components/Common/Buttons/HelpButton.module.css +++ b/web/src/components/Common/Buttons/HelpButton.module.css @@ -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. - */ - -.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; +.HelpButton { + padding: 6px 36px; + margin: 12px 6px; + line-height: 24px; + border: var(--box-border); + box-shadow: var(--box-shadow); border-radius: var(--border-radius); - box-shadow: var(--box-shadow); - line-height: 20px; - transition: opacity 0.5s; - font-weight: normal; - text-align: left; - padding: 6px 12px; + 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; } -.icon { - color: var(--contrast-60); - font-size: 16px; - padding: 8px 8px 8px 0; + +.HelpButton:hover { + background: rgb(245, 245, 245); } -.HelpButton { - border: 0; - background-color: transparent; - cursor: pointer; + +.HelpButton:active { + background: rgba(220, 220, 220); +} + + +.tooltip { + visibility: hidden; + background-color: #333; + color: white; + opacity: 0; + width: 180px; + margin-top: 36px; + margin-left: -90px; + 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 12px; + transition: opacity 0.5s; + font-weight: normal; + text-align: left; } + .HelpButton:hover .tooltip { - visibility: visible; - opacity: 100%; - transition: opacity 0.5s; + visibility: visible; + opacity: 1; + transition: opacity 0.5s; +} + + +.HelpButton:disabled { + color: rgba(0, 0, 0, 0.25); + cursor: default; } diff --git a/web/src/components/Common/Card.module.css b/web/src/components/Common/Card.module.css new file mode 100644 index 0000000..e0b1404 --- /dev/null +++ b/web/src/components/Common/Card.module.css @@ -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; +} diff --git a/web/src/components/Common/Card.tsx b/web/src/components/Common/Card.tsx new file mode 100644 index 0000000..3eef2e5 --- /dev/null +++ b/web/src/components/Common/Card.tsx @@ -0,0 +1,13 @@ +import styles from "./Card.module.css"; +import React, { ReactNode } from "react"; + + +interface CardProps { + children: ReactNode; +} + +const Card: React.FC = ({children}) => { + return
{children}
; +}; + +export default Card; diff --git a/web/src/components/Common/Section.module.css b/web/src/components/Common/Section.module.css new file mode 100644 index 0000000..b740757 --- /dev/null +++ b/web/src/components/Common/Section.module.css @@ -0,0 +1,6 @@ +.Section { + line-height: 36px; + margin: 12px; + font-size: 16px; + font-weight: bold; +} \ No newline at end of file diff --git a/web/src/components/Common/Section.tsx b/web/src/components/Common/Section.tsx new file mode 100644 index 0000000..861a0dd --- /dev/null +++ b/web/src/components/Common/Section.tsx @@ -0,0 +1,11 @@ +import styles from "./Section.module.css"; + +interface SectionProps { + title: string; +} + +const Section = ({ title }: SectionProps) => { + return

{title}

; +}; + +export default Section; diff --git a/web/src/cssmodules.d.ts b/web/src/cssmodules.d.ts new file mode 100644 index 0000000..22c9d95 --- /dev/null +++ b/web/src/cssmodules.d.ts @@ -0,0 +1,5 @@ +// src/cssmodules.d.ts +declare module '*.module.css' { + const classes: { [key: string]: string }; + export default classes; +} diff --git a/web/src/index.css b/web/src/index.css new file mode 100644 index 0000000..b55e2ed --- /dev/null +++ b/web/src/index.css @@ -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; +} \ No newline at end of file diff --git a/web/tsconfig.json b/web/tsconfig.json index a5a6cae..beab08f 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -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"] }