From 21f6f46bd3ea9a6902c0c1568217c069b5baef21 Mon Sep 17 00:00:00 2001 From: Denis Shatskiy Date: Mon, 11 Aug 2025 08:00:53 +0700 Subject: [PATCH] feat: Integrate @xyflow/react for graph visualization in GraphView component, update package.json and package-lock.json to include new dependencies and remove unused ones for improved performance and maintainability --- frontend/package-lock.json | 857 ++++++++++++++++++++----------------- frontend/package.json | 4 +- frontend/src/GraphView.jsx | 754 +++++++------------------------- 3 files changed, 626 insertions(+), 989 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index acd4d99..57ac903 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,9 +10,9 @@ "dependencies": { "@tabler/core": "^1.3.2", "@tabler/icons-react": "^3.34.0", + "@xyflow/react": "^12.3.4", "axios": "^1.10.0", "bootstrap": "^5.3.7", - "d3": "^7.9.0", "react": "^19.1.0", "react-bootstrap": "^2.10.10", "react-dom": "^19.1.0", @@ -29,6 +29,9 @@ "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.2.0", "vite": "^7.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-musl": "4.40.0" } }, "node_modules/@ampproject/remapping": { @@ -634,16 +637,17 @@ "dev": true, "license": "MIT" }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.44.2", + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz", + "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ] }, "node_modules/@swc/helpers": { @@ -754,6 +758,55 @@ "@babel/types": "^7.20.7" } }, + "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/estree": { "version": "1.0.8", "dev": true, @@ -819,6 +872,38 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" } }, + "node_modules/@xyflow/react": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.2.tgz", + "integrity": "sha512-VifLpxOy74ck283NQOtBn1e8igmB7xo7ADDKxyBHkKd8IKpyr16TgaYOhzqVwNMdB4NT+m++zfkic530L+gEXw==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.66", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.66", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.66.tgz", + "integrity": "sha512-TTxESDwPsATnuDMUeYYtKe4wt9v8bRO29dgYBhR8HyhSCzipnAdIL/1CDfFd+WqS1srVreo24u6zZeVIDk4r3Q==", + "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/acorn": { "version": "8.15.0", "dev": true, @@ -1002,6 +1087,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "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/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -1034,15 +1125,6 @@ "node": ">= 0.8" } }, - "node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, "node_modules/concat-map": { "version": "0.0.1", "dev": true, @@ -1070,96 +1152,6 @@ "version": "3.1.3", "license": "MIT" }, - "node_modules/d3": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", - "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", - "license": "ISC", - "dependencies": { - "d3-array": "3", - "d3-axis": "3", - "d3-brush": "3", - "d3-chord": "3", - "d3-color": "3", - "d3-contour": "4", - "d3-delaunay": "6", - "d3-dispatch": "3", - "d3-drag": "3", - "d3-dsv": "3", - "d3-ease": "3", - "d3-fetch": "3", - "d3-force": "3", - "d3-format": "3", - "d3-geo": "3", - "d3-hierarchy": "3", - "d3-interpolate": "3", - "d3-path": "3", - "d3-polygon": "3", - "d3-quadtree": "3", - "d3-random": "3", - "d3-scale": "4", - "d3-scale-chromatic": "3", - "d3-selection": "3", - "d3-shape": "3", - "d3-time": "3", - "d3-time-format": "4", - "d3-timer": "3", - "d3-transition": "3", - "d3-zoom": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-axis": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", - "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-brush": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", - "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "3", - "d3-transition": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-chord": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", - "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", - "license": "ISC", - "dependencies": { - "d3-path": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", @@ -1169,30 +1161,6 @@ "node": ">=12" } }, - "node_modules/d3-contour": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", - "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", - "license": "ISC", - "dependencies": { - "d3-array": "^3.2.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", - "license": "ISC", - "dependencies": { - "delaunator": "5" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/d3-dispatch": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", @@ -1215,31 +1183,6 @@ "node": ">=12" } }, - "node_modules/d3-dsv": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", - "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", - "license": "ISC", - "dependencies": { - "commander": "7", - "iconv-lite": "0.6", - "rw": "1" - }, - "bin": { - "csv2json": "bin/dsv2json.js", - "csv2tsv": "bin/dsv2dsv.js", - "dsv2dsv": "bin/dsv2dsv.js", - "dsv2json": "bin/dsv2json.js", - "json2csv": "bin/json2dsv.js", - "json2dsv": "bin/json2dsv.js", - "json2tsv": "bin/json2dsv.js", - "tsv2csv": "bin/dsv2dsv.js", - "tsv2json": "bin/dsv2json.js" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/d3-ease": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", @@ -1249,62 +1192,6 @@ "node": ">=12" } }, - "node_modules/d3-fetch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", - "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", - "license": "ISC", - "dependencies": { - "d3-dsv": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-force": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", - "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-geo": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", - "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2.5.0 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-hierarchy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", - "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/d3-interpolate": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", @@ -1317,71 +1204,6 @@ "node": ">=12" } }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-polygon": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", - "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-quadtree": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", - "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-random": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", - "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-interpolate": "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", @@ -1391,42 +1213,6 @@ "node": ">=12" } }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/d3-timer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", @@ -1492,15 +1278,6 @@ "dev": true, "license": "MIT" }, - "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", - "license": "ISC", - "dependencies": { - "robust-predicates": "^3.0.2" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "license": "MIT", @@ -1900,6 +1677,21 @@ "node": ">= 6" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "license": "MIT", @@ -2021,18 +1813,6 @@ "node": ">= 0.4" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ignore": { "version": "5.3.2", "dev": true, @@ -2064,15 +1844,6 @@ "node": ">=0.8.19" } }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -2588,62 +2359,6 @@ "node": ">=4" } }, - "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", - "license": "Unlicense" - }, - "node_modules/rollup": { - "version": "4.44.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.44.2", - "@rollup/rollup-android-arm64": "4.44.2", - "@rollup/rollup-darwin-arm64": "4.44.2", - "@rollup/rollup-darwin-x64": "4.44.2", - "@rollup/rollup-freebsd-arm64": "4.44.2", - "@rollup/rollup-freebsd-x64": "4.44.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.44.2", - "@rollup/rollup-linux-arm-musleabihf": "4.44.2", - "@rollup/rollup-linux-arm64-gnu": "4.44.2", - "@rollup/rollup-linux-arm64-musl": "4.44.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.44.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.44.2", - "@rollup/rollup-linux-riscv64-gnu": "4.44.2", - "@rollup/rollup-linux-riscv64-musl": "4.44.2", - "@rollup/rollup-linux-s390x-gnu": "4.44.2", - "@rollup/rollup-linux-x64-gnu": "4.44.2", - "@rollup/rollup-linux-x64-musl": "4.44.2", - "@rollup/rollup-win32-arm64-msvc": "4.44.2", - "@rollup/rollup-win32-ia32-msvc": "4.44.2", - "@rollup/rollup-win32-x64-msvc": "4.44.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", - "license": "BSD-3-Clause" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, "node_modules/scheduler": { "version": "0.26.0", "license": "MIT" @@ -2789,6 +2504,15 @@ "punycode": "^2.1.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/vite": { "version": "7.0.2", "dev": true, @@ -2862,6 +2586,319 @@ } } }, + "node_modules/vite/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", + "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-android-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz", + "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz", + "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz", + "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz", + "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz", + "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz", + "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz", + "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz", + "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz", + "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz", + "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz", + "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz", + "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz", + "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz", + "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz", + "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz", + "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz", + "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/vite/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz", + "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/vite/node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/rollup": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz", + "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.40.0", + "@rollup/rollup-android-arm64": "4.40.0", + "@rollup/rollup-darwin-arm64": "4.40.0", + "@rollup/rollup-darwin-x64": "4.40.0", + "@rollup/rollup-freebsd-arm64": "4.40.0", + "@rollup/rollup-freebsd-x64": "4.40.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", + "@rollup/rollup-linux-arm-musleabihf": "4.40.0", + "@rollup/rollup-linux-arm64-gnu": "4.40.0", + "@rollup/rollup-linux-arm64-musl": "4.40.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-gnu": "4.40.0", + "@rollup/rollup-linux-riscv64-musl": "4.40.0", + "@rollup/rollup-linux-s390x-gnu": "4.40.0", + "@rollup/rollup-linux-x64-gnu": "4.40.0", + "@rollup/rollup-linux-x64-musl": "4.40.0", + "@rollup/rollup-win32-arm64-msvc": "4.40.0", + "@rollup/rollup-win32-ia32-msvc": "4.40.0", + "@rollup/rollup-win32-x64-msvc": "4.40.0", + "fsevents": "~2.3.2" + } + }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", @@ -2908,6 +2945,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/frontend/package.json b/frontend/package.json index 1f6648e..c8b65d8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,12 +14,12 @@ "@tabler/icons-react": "^3.34.0", "axios": "^1.10.0", "bootstrap": "^5.3.7", - "d3": "^7.9.0", "react": "^19.1.0", "react-bootstrap": "^2.10.10", "react-dom": "^19.1.0", "react-router-dom": "^6.30.1", - "react-sigma": "^1.2.35" + "react-sigma": "^1.2.35", + "@xyflow/react": "^12.3.4" }, "devDependencies": { "@eslint/js": "^9.29.0", diff --git a/frontend/src/GraphView.jsx b/frontend/src/GraphView.jsx index d10189f..c556e3d 100644 --- a/frontend/src/GraphView.jsx +++ b/frontend/src/GraphView.jsx @@ -1,226 +1,154 @@ -import React, { useState, useRef, useEffect } from 'react'; -import * as d3 from 'd3'; -import { IconZoomIn, IconZoomOut, IconMaximize, IconMinimize } from '@tabler/icons-react'; +import React, { useMemo, useCallback, useEffect, useRef } from 'react'; +import { + ReactFlow, + Background, + Controls, + MiniMap, + useEdgesState, + useNodesState, + addEdge, + MarkerType, +} from '@xyflow/react'; +import '@xyflow/react/dist/style.css'; function GraphView({ servers, connections }) { - // Debug helper (enable/disable via window.GRAPH_DEBUG = true/false in console) - const isDebug = typeof window !== 'undefined' ? (window.GRAPH_DEBUG !== false) : true; - const dbg = (...args) => { if (isDebug && typeof console !== 'undefined') console.log('[GraphView]', ...args); }; + const flowRef = useRef(null); + const instanceRef = useRef(null); - const [nodePositions, setNodePositions] = useState({}); - const [draggedNode, setDraggedNode] = useState(null); - const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); - const [hoveredLinkId, setHoveredLinkId] = useState(null); - const containerRef = useRef(null); - const svgRef = useRef(null); - const gRef = useRef(null); - const zoomRef = useRef(null); - const [isFullscreen, setIsFullscreen] = useState(false); - const [transformState, setTransformState] = useState({ k: 1, x: 0, y: 0 }); - const [isPanningMode, setIsPanningMode] = useState(false); - const panModeRef = useRef(false); + const getTunnelStyle = useCallback((tunnelType) => { + const map = { + GRE: { color: '#206bc4', dash: '0', width: 2.5 }, + IPSec: { color: '#f59f00', dash: '6,4', width: 2.5 }, + WireGuard: { color: '#2fb344', dash: '0', width: 3.2 }, + OpenVPN: { color: '#be4bdb', dash: '3,3', width: 2.5 }, + }; + return map[tunnelType] || { color: '#667382', dash: '5,5', width: 2 }; + }, []); - const ZOOM_MIN = 0.2; - const ZOOM_MAX = 4; - const ZOOM_STEP = 0.01; + const getCountryColor = useCallback((country) => { + const colors = { + RU: '#dc2626', + US: '#2563eb', + DE: '#059669', + SE: '#7c3aed', + NL: '#ea580c', + SG: '#0891b2', + }; + return colors[country] || '#6b7280'; + }, []); - // Размеры карточки узла - const NODE_WIDTH = 160; - const NODE_HEIGHT = 74; + const computeGridPosition = useCallback((index, total) => { + const cols = Math.ceil(Math.sqrt(total)); + const row = Math.floor(index / cols); + const col = index % cols; + const spacingX = 280; + const spacingY = 180; + return { x: 80 + col * spacingX, y: 80 + row * spacingY }; + }, []); + + const initialNodesData = useMemo(() => { + return (servers || []).map((s, i) => ({ + id: String(s.ip), + type: 'server', + position: computeGridPosition(i, servers.length), + data: { server: s }, + })); + }, [servers, computeGridPosition]); + + const [nodes, setNodes, onNodesChange] = useNodesState(initialNodesData); - // Автоматическая раскладка (force-directed) useEffect(() => { - if (!servers || servers.length === 0) return; + setNodes(initialNodesData); + }, [initialNodesData, setNodes]); - const nodes = servers.map((s) => ({ id: s.ip })); - const links = (connections || []).map((c) => ({ source: c.from, target: c.to, type: c.tunnelType })); - - const distanceForType = (t) => ({ GRE: 200, IPSec: 220, WireGuard: 180, OpenVPN: 200 }[t] || 210); - - const simulation = d3 - .forceSimulation(nodes) - .force('link', d3.forceLink(links).id((d) => d.id).distance((d) => distanceForType(d.type))) - .force('charge', d3.forceManyBody().strength(-500)) - .force('collide', d3.forceCollide(70)) - .force('center', d3.forceCenter(450, 300)) - .stop(); - - for (let i = 0; i < 300; i += 1) simulation.tick(); - - const pos = {}; - nodes.forEach((n) => { - pos[n.id] = { x: n.x, y: n.y }; + const initialEdgesData = useMemo(() => { + return (connections || []).map((c, idx) => { + const style = getTunnelStyle(c.tunnelType); + return { + id: `${c.from}-${c.to}-${idx}`, + source: String(c.from), + target: String(c.to), + label: c.ipA && c.ipB ? `${c.tunnelType} ${c.ipA} ⇄ ${c.ipB}` : c.tunnelType, + style: { stroke: style.color, strokeWidth: style.width, strokeDasharray: style.dash }, + markerEnd: { type: MarkerType.ArrowClosed, color: style.color, width: 20, height: 20 }, + animated: false, + }; }); - setNodePositions(pos); + }, [connections, getTunnelStyle]); - // После раскладки подгоняем граф по области видимости - try { - const svgEl = svgRef.current; - const box = svgEl.getBoundingClientRect(); - const padding = 40; - let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; - nodes.forEach((n) => { - minX = Math.min(minX, n.x - NODE_WIDTH / 2); - minY = Math.min(minY, n.y - NODE_HEIGHT / 2); - maxX = Math.max(maxX, n.x + NODE_WIDTH / 2); - maxY = Math.max(maxY, n.y + NODE_HEIGHT / 2); - }); - const contentW = Math.max(1, maxX - minX); - const contentH = Math.max(1, maxY - minY); - const scale = Math.max(0.5, Math.min(1.4, Math.min((box.width - padding) / contentW, (box.height - padding) / contentH))); - const tx = (box.width - scale * (minX + maxX)) / 2; - const ty = (box.height - scale * (minY + maxY)) / 2; - const t = d3.zoomIdentity.translate(tx, ty).scale(scale); - d3.select(svgEl).transition().duration(350).call(zoomRef.current.transform, t); - } catch (e) { - // ignore - } + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdgesData); + + useEffect(() => { + setEdges(initialEdgesData); + }, [initialEdgesData, setEdges]); + + const onConnect = useCallback((params) => setEdges((eds) => addEdge({ ...params, type: 'default' }, eds)), [setEdges]); + + const nodeTypes = useMemo( + () => ({ + server: ({ data }) => { + const s = data.server || {}; + const countryColor = getCountryColor(s.country); + return ( +
+
+ + {String(s.country || '').toUpperCase().slice(0, 3)} + +
{s.dns}
+
+
+
{s.ip}
+
{s.provider}
+
+
+ ); + }, + }), + [getCountryColor] + ); + + const onInit = useCallback((inst) => { + instanceRef.current = inst; + // Автоподгон при инициализации + requestAnimationFrame(() => { + try { + inst.fitView({ padding: 0.2, includeHiddenNodes: true, minZoom: 0.2, maxZoom: 2 }); + } catch {} + }); + }, []); + + useEffect(() => { + if (!instanceRef.current) return; + // Подгон при изменении данных + const i = instanceRef.current; + const t = setTimeout(() => { + try { + i.fitView({ padding: 0.2, includeHiddenNodes: true, minZoom: 0.2, maxZoom: 2 }); + } catch {} + }, 50); + return () => clearTimeout(t); }, [servers, connections]); - // Зум/панорамирование - useEffect(() => { - const svgEl = svgRef.current; - const svg = d3.select(svgEl); - const g = d3.select(gRef.current); - const vb = svgEl?.viewBox?.baseVal; - const width = (vb && vb.width) ? vb.width : (svgEl?.clientWidth || 900); - const height = (vb && vb.height) ? vb.height : (svgEl?.clientHeight || 600); - - const zoom = d3 - .zoom() - .scaleExtent([ZOOM_MIN, ZOOM_MAX]) - .extent([[0, 0], [width, height]]) - .translateExtent([[-10000, -10000], [10000, 10000]]) - .on('zoom', (event) => { - const t = event.transform; - g.attr('transform', `translate(${t.x},${t.y}) scale(${t.k})`); - setTransformState({ k: t.k, x: t.x, y: t.y }); - }); - zoomRef.current = zoom; - svg - .call(zoom) - .on('dblclick.zoom', null); - dbg('zoom initialized', { width, height }); - }, []); - - // Горячая клавиша: пробел включает режим панорамирования (чтобы тянуть в любом месте, даже на узлах) - useEffect(() => { - const down = (e) => { - if (e.code === 'Space') { - e.preventDefault(); - setIsPanningMode(true); - panModeRef.current = true; - } - }; - const up = (e) => { - if (e.code === 'Space') { - e.preventDefault(); - setIsPanningMode(false); - panModeRef.current = false; - } - }; - window.addEventListener('keydown', down, { passive: false }); - window.addEventListener('keyup', up, { passive: false }); - return () => { - window.removeEventListener('keydown', down); - window.removeEventListener('keyup', up); - }; - }, []); - - // Контролы масштабирования - const zoomBy = (factor) => { - if (!zoomRef.current || !svgRef.current) return; - const svgEl = svgRef.current; - const vb = svgEl.viewBox?.baseVal; - const cx = (vb && vb.width) ? vb.width / 2 : svgEl.clientWidth / 2; - const cy = (vb && vb.height) ? vb.height / 2 : svgEl.clientHeight / 2; - const current = d3.zoomTransform(svgEl); - dbg('zoomBy click', { factor, current: { k: current.k, x: current.x, y: current.y }, center: { cx, cy } }); - const svgSel = d3.select(svgEl); - svgSel.interrupt(); - svgSel - .transition() - .duration(220) - .call(zoomRef.current.scaleBy, factor, [cx, cy]) - .on('end', () => { - const nt = d3.zoomTransform(svgEl); - dbg('zoomBy end', { next: { k: nt.k, x: nt.x, y: nt.y } }); - }); - }; - - const zoomTo = (scale) => { - if (!zoomRef.current || !svgRef.current) return; - const svgEl = svgRef.current; - const vb = svgEl.viewBox?.baseVal; - const cx = (vb && vb.width) ? vb.width / 2 : svgEl.clientWidth / 2; - const cy = (vb && vb.height) ? vb.height / 2 : svgEl.clientHeight / 2; - const nextScale = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, scale)); - const svgSel = d3.select(svgEl); - svgSel.interrupt(); - svgSel - .transition() - .duration(200) - .call(zoomRef.current.scaleTo, nextScale, [cx, cy]); - }; - - const fitToView = () => { - const svgEl = svgRef.current; - if (!svgEl) return; - const vb = svgEl.viewBox?.baseVal; - const box = { width: (vb && vb.width) ? vb.width : svgEl.clientWidth, height: (vb && vb.height) ? vb.height : svgEl.clientHeight }; - const padding = 40; - const positions = nodePositions; - const ids = Object.keys(positions); - if (ids.length === 0) return; - let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; - ids.forEach((id) => { - const p = positions[id]; - minX = Math.min(minX, p.x - NODE_WIDTH / 2); - minY = Math.min(minY, p.y - NODE_HEIGHT / 2); - maxX = Math.max(maxX, p.x + NODE_WIDTH / 2); - maxY = Math.max(maxY, p.y + NODE_HEIGHT / 2); - }); - const contentW = Math.max(1, maxX - minX); - const contentH = Math.max(1, maxY - minY); - const scale = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, Math.min((box.width - padding) / contentW, (box.height - padding) / contentH))); - const tx = (box.width - scale * (minX + maxX)) / 2; - const ty = (box.height - scale * (minY + maxY)) / 2; - const t = d3.zoomIdentity.translate(tx, ty).scale(scale); - d3.select(svgEl).interrupt().transition().duration(250).call(zoomRef.current.transform, t); - }; - - // Полноэкранный режим - useEffect(() => { - const handler = () => setIsFullscreen(Boolean(document.fullscreenElement)); - document.addEventListener('fullscreenchange', handler); - return () => document.removeEventListener('fullscreenchange', handler); - }, []); - - const toggleFullscreen = () => { - const el = containerRef.current; - if (!el) return; - dbg('toggleFullscreen', { isFullscreen: Boolean(document.fullscreenElement) }); - if (!document.fullscreenElement) { - el.requestFullscreen?.(); - } else { - document.exitFullscreen?.(); - } - }; - - const resetZoom = () => { - if (!zoomRef.current || !svgRef.current) return; - const svgEl = svgRef.current; - d3.select(svgEl) - .transition() - .duration(220) - .call(zoomRef.current.transform, d3.zoomIdentity) - .on('end', () => { - const nt = d3.zoomTransform(svgEl); - dbg('resetZoom end', { k: nt.k, x: nt.x, y: nt.y }); - }); - }; - - if (servers.length === 0) { + if (!servers || servers.length === 0) { return (
@@ -231,389 +159,33 @@ function GraphView({ servers, connections }) { ); } - // Инициализация позиций узлов при первом рендере - const getInitialNodePosition = (index, total) => { - if (nodePositions[servers[index]?.ip]) { - return nodePositions[servers[index].ip]; - } - - const cols = Math.ceil(Math.sqrt(total)); - const row = Math.floor(index / cols); - const col = index % cols; - const spacing = 180; - return { - x: 120 + col * spacing, - y: 120 + row * spacing - }; - }; - - // Обработчик начала перетаскивания - const handleMouseDown = (e, serverIp) => { - // В режиме панорамирования — не начинаем перетаскивание узла, даём d3.zoom обработать drag - if (panModeRef.current) return; - const target = gRef.current || svgRef.current; - const pt = (gRef.current?.ownerSVGElement || svgRef.current).createSVGPoint(); - pt.x = e.clientX; - pt.y = e.clientY; - const svgP = pt.matrixTransform(target.getScreenCTM().inverse()); - - const currentPos = nodePositions[serverIp] || getInitialNodePosition( - servers.findIndex(s => s.ip === serverIp), - servers.length - ); - - setDragOffset({ - x: svgP.x - currentPos.x, - y: svgP.y - currentPos.y - }); - setDraggedNode(serverIp); - }; - - // Обработчик перетаскивания - const handleMouseMove = (e) => { - if (!draggedNode) return; - const target = gRef.current || svgRef.current; - const pt = (gRef.current?.ownerSVGElement || svgRef.current).createSVGPoint(); - pt.x = e.clientX; - pt.y = e.clientY; - const svgP = pt.matrixTransform(target.getScreenCTM().inverse()); - - setNodePositions(prev => ({ - ...prev, - [draggedNode]: { - x: svgP.x - dragOffset.x, - y: svgP.y - dragOffset.y - } - })); - }; - - // Обработчик окончания перетаскивания - const handleMouseUp = () => { - setDraggedNode(null); - }; - - // Получение позиции узла - const getNodePosition = (serverIp) => { - return nodePositions[serverIp] || getInitialNodePosition( - servers.findIndex(s => s.ip === serverIp), - servers.length - ); - }; - - // Цвета для разных стран - const getCountryColor = (country) => { - const colors = { - 'RU': '#dc2626', // красный - 'US': '#2563eb', // синий - 'DE': '#059669', // зеленый - 'SE': '#7c3aed', // фиолетовый - 'NL': '#ea580c', // оранжевый - 'SG': '#0891b2' // голубой - }; - return colors[country] || '#6b7280'; - }; - - // Цвета/стили для разных типов туннелей - const getTunnelStyle = (tunnelType) => { - const map = { - GRE: { color: '#206bc4', dash: '0', width: 3 }, - IPSec: { color: '#f59f00', dash: '6,4', width: 3 }, - WireGuard: { color: '#2fb344', dash: '0', width: 4 }, - OpenVPN: { color: '#be4bdb', dash: '3,3', width: 3 }, - }; - return map[tunnelType] || { color: '#667382', dash: '5,5', width: 2 }; - }; - - // Флаг страны по ISO-2 с поддержкой нестандартных кодов из списка (SWE->SE) - const getFlagEmoji = (code) => { - if (!code) return ''; - const map = { SWE: 'SE', UK: 'GB' }; - const ccRaw = String(code).trim().toUpperCase(); - const cc = (map[ccRaw] || ccRaw).slice(0, 2); - if (cc.length !== 2) return ccRaw; - return cc.replace(/./g, (ch) => String.fromCodePoint(127397 + ch.charCodeAt())); - }; - return ( -
- + - - {/* Лёгкая сетка фона */} - - - - {/* Определение стрелок под цвет канала */} - {Array.from(new Set(connections.map(c => c.tunnelType))).map((t) => { - const style = getTunnelStyle(t); - return ( - - - - ); - })} - {/* Свечение для выделения */} - - - - - - - - {/* Тень карточки */} - - - - {/* Тень для ярлыков ссылок */} - - - - - - {/* Подложка сетки (внутри слоя зума) */} - - {/* Связи */} - {connections.map((connection, index) => { - const fromServer = servers.find(s => s.ip === connection.from); - const toServer = servers.find(s => s.ip === connection.to); - - if (!fromServer || !toServer) return null; - - const fromPos = getNodePosition(connection.from); - const toPos = getNodePosition(connection.to); - const style = getTunnelStyle(connection.tunnelType); - const id = `${connection.from}-${connection.to}-${index}`; - const isHovered = hoveredLinkId === id; - - // Кривая линия с небольшим отступом от прямой для читаемости - const mx = (fromPos.x + toPos.x) / 2; - const my = (fromPos.y + toPos.y) / 2; - const dx = toPos.x - fromPos.x; - const dy = toPos.y - fromPos.y; - const len = Math.sqrt(dx * dx + dy * dy) || 1; - const nx = (-dy / len) * 30; // перпендикуляр, 30px - const ny = (dx / len) * 30; - const cx = mx + nx; - const cy = my + ny; - - return ( - setHoveredLinkId(id)} - onMouseLeave={() => setHoveredLinkId(null)} - > - {/* Линия связи */} - - - {/* Подпись связи: ярлык с подложкой и halo */} - {(() => { - const labelW = 140; - const labelH = 38; - const lx = cx - labelW / 2; - const ly = cy - labelH / 2 - 4; // слегка выше вершины - return ( - - - {/* Halo для заголовка */} - - {connection.tunnelType} - - {/* Линия IP с более мелким шрифтом */} - - {connection.ipA} ⇄ {connection.ipB} - - - ); - })()} - - ); - })} - - {/* Узлы (серверы) */} - {servers.map((server) => { - const pos = getNodePosition(server.ip); - const countryColor = getCountryColor(server.country); - const isDragging = draggedNode === server.ip; - - return ( - - {/* Карточка узла */} - { e.stopPropagation(); handleMouseDown(e, server.ip); }} - /> - {/* Плашка страны */} - {(() => { - const badgeX = pos.x - NODE_WIDTH / 2 + 8; - const badgeY = pos.y - NODE_HEIGHT / 2 - 10; - const flag = getFlagEmoji(server.country); - return ( - - - {/* Рендер флага через foreignObject как в HTML (надёжнее для Windows/Chrome) */} - -
- {flag} -
-
- - {String(server.country).toUpperCase().slice(0,3)} - -
- ); - })()} - {/* Название */} - - {server.dns} - - {/* IP */} - - {server.ip} - - {/* Провайдер */} - - {server.provider} - -
- ); - })} - - {/* Доп. определения добавлены выше */} -
-
- {/* Контролы масштабирования + ползунок */} -
-
- - - - - -
-
- {Math.round(transformState.k * 100)}% - zoomTo(parseFloat(e.target.value))} - className="form-range" - style={{ width: '100%' }} - /> -
-
+ + getCountryColor(n.data?.server?.country)} + nodeStrokeWidth={2} + maskColor="rgba(0,0,0,0.05)" + /> + +
); } -export default GraphView; \ No newline at end of file +export default GraphView; \ No newline at end of file