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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 6m21s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 6m21s
This commit is contained in:
Generated
+461
-396
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
+163
-591
@@ -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 (
|
||||
<div
|
||||
className="card shadow-sm"
|
||||
style={{
|
||||
width: 200,
|
||||
borderRadius: 12,
|
||||
border: `2px solid ${countryColor}`,
|
||||
overflow: 'hidden',
|
||||
background: '#fff',
|
||||
}}
|
||||
>
|
||||
<div className="px-2 py-1 d-flex align-items-center" style={{ background: '#f8fafc', borderBottom: '1px solid #eef2f7' }}>
|
||||
<span
|
||||
className="badge"
|
||||
style={{
|
||||
background: '#fff',
|
||||
color: '#475569',
|
||||
border: `1px solid ${countryColor}`,
|
||||
borderRadius: 8,
|
||||
fontSize: 11,
|
||||
}}
|
||||
>
|
||||
{String(s.country || '').toUpperCase().slice(0, 3)}
|
||||
</span>
|
||||
<div className="ms-2 text-truncate" style={{ fontWeight: 700, color: '#1f2937', fontSize: 13 }}>{s.dns}</div>
|
||||
</div>
|
||||
<div className="px-2 py-2" style={{ lineHeight: 1.2 }}>
|
||||
<div style={{ fontSize: 12, color: '#334155' }}>{s.ip}</div>
|
||||
<div style={{ fontSize: 10, color: '#6b7280' }}>{s.provider}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}),
|
||||
[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 (
|
||||
<div className="text-center py-5">
|
||||
<div className="text-muted">
|
||||
@@ -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 (
|
||||
<div ref={containerRef} style={{ width: '100%', height: '500px', border: '1px solid #e5e7eb', borderRadius: '8px', overflow: 'hidden', background: '#f8fafc', position: 'relative' }}>
|
||||
<svg
|
||||
ref={svgRef}
|
||||
width="100%"
|
||||
height="100%"
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseUp}
|
||||
style={{ cursor: (draggedNode || isPanningMode) ? 'grabbing' : 'grab' }}
|
||||
<div style={{ width: '100%', height: 500, border: '1px solid #e5e7eb', borderRadius: 8, overflow: 'hidden' }}>
|
||||
<ReactFlow
|
||||
ref={flowRef}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
defaultEdgeOptions={{
|
||||
type: 'default',
|
||||
markerEnd: { type: MarkerType.ArrowClosed },
|
||||
}}
|
||||
onInit={onInit}
|
||||
>
|
||||
<defs>
|
||||
{/* Лёгкая сетка фона */}
|
||||
<pattern id="grid" width="30" height="30" patternUnits="userSpaceOnUse">
|
||||
<path d="M 30 0 L 0 0 0 30" fill="none" stroke="#f2f4f6" strokeWidth="1" />
|
||||
</pattern>
|
||||
{/* Определение стрелок под цвет канала */}
|
||||
{Array.from(new Set(connections.map(c => c.tunnelType))).map((t) => {
|
||||
const style = getTunnelStyle(t);
|
||||
return (
|
||||
<marker key={`arrow-${t}`}
|
||||
id={`arrow-${t}`}
|
||||
markerWidth="12"
|
||||
markerHeight="8"
|
||||
refX="10"
|
||||
refY="4"
|
||||
orient="auto"
|
||||
>
|
||||
<polygon points="0 0, 12 4, 0 8" fill={style.color} />
|
||||
</marker>
|
||||
);
|
||||
})}
|
||||
{/* Свечение для выделения */}
|
||||
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="2" result="coloredBlur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
{/* Тень карточки */}
|
||||
<filter id="cardShadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="3" floodOpacity="0.2" />
|
||||
</filter>
|
||||
{/* Тень для ярлыков ссылок */}
|
||||
<filter id="labelShadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feDropShadow dx="0" dy="1" stdDeviation="2" floodOpacity="0.25" />
|
||||
</filter>
|
||||
</defs>
|
||||
<g ref={gRef}>
|
||||
{/* Подложка сетки (внутри слоя зума) */}
|
||||
<rect x={-10000} y={-10000} width={20000} height={20000} fill="url(#grid)" />
|
||||
{/* Связи */}
|
||||
{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 (
|
||||
<g key={`link-${index}`}
|
||||
onMouseEnter={() => setHoveredLinkId(id)}
|
||||
onMouseLeave={() => setHoveredLinkId(null)}
|
||||
>
|
||||
{/* Линия связи */}
|
||||
<path
|
||||
d={`M ${fromPos.x} ${fromPos.y} Q ${cx} ${cy} ${toPos.x} ${toPos.y}`}
|
||||
fill="none"
|
||||
stroke={style.color}
|
||||
strokeWidth={isHovered ? style.width + 1 : style.width}
|
||||
strokeDasharray={style.dash}
|
||||
markerEnd={`url(#arrow-${connection.tunnelType})`}
|
||||
filter={isHovered ? 'url(#glow)' : undefined}
|
||||
opacity={isHovered ? 1 : 0.9}
|
||||
/>
|
||||
|
||||
{/* Подпись связи: ярлык с подложкой и halo */}
|
||||
{(() => {
|
||||
const labelW = 140;
|
||||
const labelH = 38;
|
||||
const lx = cx - labelW / 2;
|
||||
const ly = cy - labelH / 2 - 4; // слегка выше вершины
|
||||
return (
|
||||
<g style={{ pointerEvents: 'none' }}>
|
||||
<rect
|
||||
x={lx}
|
||||
y={ly}
|
||||
width={labelW}
|
||||
height={labelH}
|
||||
rx="16"
|
||||
fill="#ffffff"
|
||||
stroke={style.color}
|
||||
strokeWidth="1"
|
||||
filter="url(#labelShadow)"
|
||||
/>
|
||||
{/* Halo для заголовка */}
|
||||
<text
|
||||
x={cx}
|
||||
y={ly + 16}
|
||||
textAnchor="middle"
|
||||
fontSize="12"
|
||||
fontWeight="700"
|
||||
stroke="#ffffff"
|
||||
strokeWidth="3"
|
||||
strokeLinejoin="round"
|
||||
fill="#374151"
|
||||
style={{ paintOrder: 'stroke fill' }}
|
||||
>
|
||||
{connection.tunnelType}
|
||||
</text>
|
||||
{/* Линия IP с более мелким шрифтом */}
|
||||
<text
|
||||
x={cx}
|
||||
y={ly + 30}
|
||||
textAnchor="middle"
|
||||
fontSize="10"
|
||||
fill="#6b7280"
|
||||
stroke="#ffffff"
|
||||
strokeWidth="2"
|
||||
strokeLinejoin="round"
|
||||
style={{ paintOrder: 'stroke fill' }}
|
||||
>
|
||||
{connection.ipA} ⇄ {connection.ipB}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})()}
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Узлы (серверы) */}
|
||||
{servers.map((server) => {
|
||||
const pos = getNodePosition(server.ip);
|
||||
const countryColor = getCountryColor(server.country);
|
||||
const isDragging = draggedNode === server.ip;
|
||||
|
||||
return (
|
||||
<g key={server.ip}>
|
||||
{/* Карточка узла */}
|
||||
<rect
|
||||
x={pos.x - NODE_WIDTH / 2}
|
||||
y={pos.y - NODE_HEIGHT / 2}
|
||||
rx={12}
|
||||
ry={12}
|
||||
width={NODE_WIDTH}
|
||||
height={NODE_HEIGHT}
|
||||
fill="#ffffff"
|
||||
stroke={countryColor}
|
||||
strokeWidth={2}
|
||||
filter="url(#cardShadow)"
|
||||
style={{ cursor: 'grab' }}
|
||||
onMouseDown={(e) => { 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 (
|
||||
<g>
|
||||
<rect
|
||||
x={badgeX}
|
||||
y={badgeY}
|
||||
rx={8}
|
||||
ry={8}
|
||||
width={52}
|
||||
height={20}
|
||||
fill="#fff"
|
||||
stroke={countryColor}
|
||||
/>
|
||||
{/* Рендер флага через foreignObject как в HTML (надёжнее для Windows/Chrome) */}
|
||||
<foreignObject x={badgeX + 4} y={badgeY + 3} width={16} height={14} style={{ pointerEvents: 'none' }}>
|
||||
<div xmlns="http://www.w3.org/1999/xhtml"
|
||||
style={{fontSize:'14px', lineHeight:'14px', fontFamily:'Segoe UI Emoji, Noto Color Emoji, Apple Color Emoji, "Twemoji Mozilla", "EmojiOne Color", sans-serif'}}>
|
||||
{flag}
|
||||
</div>
|
||||
</foreignObject>
|
||||
<text
|
||||
x={badgeX + 30}
|
||||
y={badgeY + 13}
|
||||
fontSize="11"
|
||||
fill="#475569"
|
||||
dominantBaseline="middle"
|
||||
>
|
||||
{String(server.country).toUpperCase().slice(0,3)}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})()}
|
||||
{/* Название */}
|
||||
<text
|
||||
x={pos.x}
|
||||
y={pos.y - 4}
|
||||
textAnchor="middle"
|
||||
fontSize="13"
|
||||
fill="#1f2937"
|
||||
fontWeight="700"
|
||||
>
|
||||
{server.dns}
|
||||
</text>
|
||||
{/* IP */}
|
||||
<text
|
||||
x={pos.x}
|
||||
y={pos.y + 14}
|
||||
textAnchor="middle"
|
||||
fontSize="12"
|
||||
fill="#334155"
|
||||
>
|
||||
{server.ip}
|
||||
</text>
|
||||
{/* Провайдер */}
|
||||
<text
|
||||
x={pos.x}
|
||||
y={pos.y + 30}
|
||||
textAnchor="middle"
|
||||
fontSize="10"
|
||||
fill="#6b7280"
|
||||
>
|
||||
{server.provider}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Доп. определения добавлены выше */}
|
||||
</g>
|
||||
</svg>
|
||||
{/* Контролы масштабирования + ползунок */}
|
||||
<div className="position-absolute" style={{ right: 10, top: 10, zIndex: 1000, pointerEvents: 'auto', width: 220 }}>
|
||||
<div className="btn-group btn-group-sm" style={{ width: '100%' }}>
|
||||
<button className="btn btn-outline-secondary" onClick={() => zoomBy(1/1.2)} title="Уменьшить">
|
||||
<IconZoomOut size={16} />
|
||||
</button>
|
||||
<button className="btn btn-outline-secondary" onClick={() => zoomBy(1.2)} title="Увеличить">
|
||||
<IconZoomIn size={16} />
|
||||
</button>
|
||||
<button className="btn btn-outline-secondary" onClick={fitToView} title="Подогнать к окну">
|
||||
Fit
|
||||
</button>
|
||||
<button className="btn btn-outline-secondary" onClick={resetZoom} title="Сброс масштаба">
|
||||
100%
|
||||
</button>
|
||||
<button className="btn btn-outline-secondary" onClick={toggleFullscreen} title={isFullscreen ? 'Выйти из полноэкранного' : 'Во весь экран'}>
|
||||
{isFullscreen ? <IconMinimize size={16} /> : <IconMaximize size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 d-flex align-items-center" style={{ gap: 8 }}>
|
||||
<span className="text-muted" style={{ fontSize: 12, width: 34 }}>{Math.round(transformState.k * 100)}%</span>
|
||||
<input
|
||||
type="range"
|
||||
min={ZOOM_MIN}
|
||||
max={ZOOM_MAX}
|
||||
step={ZOOM_STEP}
|
||||
value={transformState.k}
|
||||
onChange={(e) => zoomTo(parseFloat(e.target.value))}
|
||||
className="form-range"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Background variant="dots" gap={20} size={1} color="#e5e7eb" />
|
||||
<MiniMap
|
||||
nodeColor={(n) => getCountryColor(n.data?.server?.country)}
|
||||
nodeStrokeWidth={2}
|
||||
maskColor="rgba(0,0,0,0.05)"
|
||||
/>
|
||||
<Controls position="top-right" showInteractive={false} />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GraphView;
|
||||
export default GraphView;
|
||||
Reference in New Issue
Block a user