diff --git a/ordinis-admin-ui/package.json b/ordinis-admin-ui/package.json index 9c3a403..e14543b 100644 --- a/ordinis-admin-ui/package.json +++ b/ordinis-admin-ui/package.json @@ -39,12 +39,14 @@ "oidc-client-ts": "^3.5.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-force-graph-3d": "^1.29.1", "react-hook-form": "^7.54.2", "react-i18next": "^17.0.2", "react-leaflet": "^5.0.0", "react-oidc-context": "^3.3.1", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", + "three": "^0.184.0", "topojson-client": "^3.1.0", "tw-animate-css": "^1.4.0", "world-atlas": "^2.0.2" @@ -67,6 +69,7 @@ "@types/leaflet": "^1.9.21", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", + "@types/three": "^0.184.1", "@types/topojson-client": "^3.1.5", "@vitejs/plugin-react": "^4.3.4", "conventional-changelog-conventionalcommits": "^9.3.1", diff --git a/ordinis-admin-ui/pnpm-lock.yaml b/ordinis-admin-ui/pnpm-lock.yaml index 71c8276..177b14d 100644 --- a/ordinis-admin-ui/pnpm-lock.yaml +++ b/ordinis-admin-ui/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.2.5(react@19.2.5) + react-force-graph-3d: + specifier: ^1.29.1 + version: 1.29.1(react@19.2.5) react-hook-form: specifier: ^7.54.2 version: 7.75.0(react@19.2.5) @@ -110,6 +113,9 @@ importers: tailwind-merge: specifier: ^3.6.0 version: 3.6.0 + three: + specifier: ^0.184.0 + version: 0.184.0 topojson-client: specifier: ^3.1.0 version: 3.1.0 @@ -171,6 +177,9 @@ importers: '@types/react-dom': specifier: ^19.0.0 version: 19.2.3(@types/react@19.2.14) + '@types/three': + specifier: ^0.184.1 + version: 0.184.1 '@types/topojson-client': specifier: ^3.1.5 version: 3.1.5 @@ -201,6 +210,10 @@ importers: packages: + 3d-force-graph@1.80.0: + resolution: {integrity: sha512-tzI353gW1nXPpnC7VTa3JjMg+3cp77qOLUFO0vucPTfF+q5R6sQsNsIqVTbRIb7RSypn14nBa4yfkOe9ThxASw==} + engines: {node: '>=12'} + '@adobe/css-tools@4.4.4': resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} @@ -362,6 +375,9 @@ packages: resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} engines: {node: '>=20.19.0'} + '@dimforge/rapier3d-compat@0.12.0': + resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -1619,6 +1635,12 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@tweenjs/tween.js@23.1.3': + resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + + '@tweenjs/tween.js@25.0.0': + resolution: {integrity: sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -1663,12 +1685,21 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/stats.js@0.17.4': + resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} + + '@types/three@0.184.1': + resolution: {integrity: sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==} + '@types/topojson-client@3.1.5': resolution: {integrity: sha512-C79rySTyPxnQNNguTZNI1Ct4D7IXgvyAs3p9HPecnl6mNrJ5+UhvGNYcZfpROYV2lMHI48kJPxwR+F9C6c7nmw==} '@types/topojson-specification@1.0.5': resolution: {integrity: sha512-C7KvcQh+C2nr6Y2Ub4YfgvWvWCgP2nOQMtfhlnwsRL4pYmmwzBS7HclGiS87eQfDOU/DLQpX6GEscviaz4yLIQ==} + '@types/webxr@0.5.24': + resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} + '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1704,6 +1735,10 @@ packages: '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + accessor-fn@1.5.3: + resolution: {integrity: sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==} + engines: {node: '>=12'} + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -2000,26 +2035,72 @@ packages: resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} engines: {node: '>=12'} + d3-binarytree@1.0.2: + resolution: {integrity: sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + d3-dispatch@3.0.1: resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} engines: {node: '>=12'} + d3-force-3d@3.0.6: + resolution: {integrity: sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==} + engines: {node: '>=12'} + d3-force@3.0.0: resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} engines: {node: '>=12'} + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + d3-geo@3.1.1: resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} engines: {node: '>=12'} + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-octree@1.1.0: + resolution: {integrity: sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==} + d3-quadtree@3.0.1: resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} engines: {node: '>=12'} + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + d3-timer@3.0.1: resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} engines: {node: '>=12'} + data-bind-mapper@1.0.3: + resolution: {integrity: sha512-QmU3lyEnbENQPo0M1F9BMu4s6cqNNp8iJA+b/HP2sSb7pf3dxwF3+EP1eO69rwBfH9kFJ1apmzrtogAmVt2/Xw==} + engines: {node: '>=12'} + data-urls@7.0.0: resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -2205,6 +2286,9 @@ packages: picomatch: optional: true + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figures@2.0.0: resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} engines: {node: '>=4'} @@ -2229,6 +2313,10 @@ packages: resolution: {integrity: sha512-2kCCtc+JvcZ86IGAz3Z2Y0A1baIz9fL31pH/0S1IqZr9Iwnjq8izfPtrCyQKO6TLMPELLsQMre7VDqeIKCsHkA==} engines: {node: '>=18'} + float-tooltip@1.7.5: + resolution: {integrity: sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==} + engines: {node: '>=12'} + follow-redirects@1.16.0: resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} engines: {node: '>=4.0'} @@ -2517,6 +2605,10 @@ packages: resolution: {integrity: sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==} engines: {node: '>= 0.6.0'} + jerrypick@1.1.2: + resolution: {integrity: sha512-YKnxXEekXKzhpf7CLYA0A+oDP8V0OhICNCr5lv96FvSsDEmrb0GKM776JgQvHTMjr7DTTPEVv/1Ciaw0uEWzBA==} + engines: {node: '>=12'} + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -2566,6 +2658,10 @@ packages: resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} engines: {node: '>=18'} + kapsule@1.16.3: + resolution: {integrity: sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==} + engines: {node: '>=12'} + keyv@5.6.0: resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} @@ -2678,6 +2774,10 @@ packages: lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} @@ -2740,6 +2840,9 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + meshoptimizer@1.1.1: + resolution: {integrity: sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -2793,6 +2896,21 @@ packages: nerf-dart@1.0.0: resolution: {integrity: sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==} + ngraph.events@1.4.0: + resolution: {integrity: sha512-NeDGI4DSyjBNBRtA86222JoYietsmCXbs8CEB0dZ51Xeh4lhVl1y3wpWLumczvnha8sFQIW4E0vvVWwgmX2mGw==} + + ngraph.forcelayout@3.3.1: + resolution: {integrity: sha512-MKBuEh1wujyQHFTW57y5vd/uuEOK0XfXYxm3lC7kktjJLRdt/KEKEknyOlc6tjXflqBKEuYBBcu7Ax5VY+S6aw==} + + ngraph.graph@20.1.2: + resolution: {integrity: sha512-W/G3GBR3Y5UxMLHTUCPP9v+pbtpzwuAEIqP5oZV+9IwgxAIEZwh+Foc60iPc1idlnK7Zxu0p3puxAyNmDvBd0Q==} + + ngraph.merge@1.0.0: + resolution: {integrity: sha512-5J8YjGITUJeapsomtTALYsw7rFveYkM+lBj3QiYZ79EymQcuri65Nw3knQtFxQBU1r5iOaVRXrSwMENUPK62Vg==} + + ngraph.random@1.2.0: + resolution: {integrity: sha512-4EUeAGbB2HWX9njd6bP6tciN6ByJfoaAvmVL9QTaZSeXrW46eNGA9GajiXiPBbvFqxUWFkEbyo6x5qsACUuVfA==} + node-emoji@2.2.0: resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} engines: {node: '>=18'} @@ -3046,10 +3164,17 @@ packages: resolution: {integrity: sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g==} engines: {node: '>=4'} + polished@4.3.1: + resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==} + engines: {node: '>=10'} + postcss@8.5.13: resolution: {integrity: sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==} engines: {node: ^10 || ^12 || >=14} + preact@10.29.1: + resolution: {integrity: sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==} + prettier@3.8.3: resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} engines: {node: '>=14'} @@ -3066,6 +3191,9 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -3096,6 +3224,12 @@ packages: peerDependencies: react: ^19.2.5 + react-force-graph-3d@1.29.1: + resolution: {integrity: sha512-5Vp+PGpYnO+zLwgK2NvNqdXHvsWLrFzpDfJW1vUA1twjo9SPvXqfUYQrnRmAbD+K2tOxkZw1BkbH31l5b4TWHg==} + engines: {node: '>=12'} + peerDependencies: + react: '*' + react-hook-form@7.75.0: resolution: {integrity: sha512-Ovv94H+0p3sJ7B9B5QxPuCP1u8V/cHuVGyH55cSwodYDtoJwK+fqk3vjfIgSX59I2U/bU4z0nRJ9HMLpNiWEmw==} engines: {node: '>=18.0.0'} @@ -3118,9 +3252,18 @@ packages: typescript: optional: true + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-kapsule@2.5.7: + resolution: {integrity: sha512-kifAF4ZPD77qZKc4CKLmozq6GY1sBzPEJTIJb0wWFK6HsePJatK3jXplZn2eeAt3x67CDozgi7/rO8fNQ/AL7A==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.13.1' + react-leaflet@5.0.0: resolution: {integrity: sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==} peerDependencies: @@ -3424,6 +3567,21 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + three-forcegraph@1.43.4: + resolution: {integrity: sha512-FtmiZP/T16ZQaHza3JDaDn0YTXFtg9e7pGnTeU8nzu0NNkx7MpWbF/GvmpbQsWHx3rukHtkRv1fTorLPB3FDEA==} + engines: {node: '>=12'} + peerDependencies: + three: '>=0.118.3' + + three-render-objects@1.41.1: + resolution: {integrity: sha512-0H7l7yREPVKfO3HL7RjPQ67T0phHgnyMeEc4ww/OCEfK6jbsm7psEcrR0SGFqGDyS/pDQTPi4DyPbS/xlHRJKw==} + engines: {node: '>=12'} + peerDependencies: + three: '>=0.179' + + three@0.184.0: + resolution: {integrity: sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==} + through2@2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} @@ -3434,6 +3592,9 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} @@ -3773,6 +3934,14 @@ packages: snapshots: + 3d-force-graph@1.80.0: + dependencies: + accessor-fn: 1.5.3 + kapsule: 1.16.3 + three: 0.184.0 + three-forcegraph: 1.43.4(three@0.184.0) + three-render-objects: 1.41.1(three@0.184.0) + '@adobe/css-tools@4.4.4': {} '@asamuzakjp/css-color@5.1.11': @@ -3950,6 +4119,8 @@ snapshots: '@csstools/css-tokenizer@4.0.0': {} + '@dimforge/rapier3d-compat@0.12.0': {} + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -5041,6 +5212,10 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 + '@tweenjs/tween.js@23.1.3': {} + + '@tweenjs/tween.js@25.0.0': {} + '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': @@ -5090,6 +5265,17 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/stats.js@0.17.4': {} + + '@types/three@0.184.1': + dependencies: + '@dimforge/rapier3d-compat': 0.12.0 + '@tweenjs/tween.js': 23.1.3 + '@types/stats.js': 0.17.4 + '@types/webxr': 0.5.24 + fflate: 0.8.2 + meshoptimizer: 1.1.1 + '@types/topojson-client@3.1.5': dependencies: '@types/geojson': 7946.0.16 @@ -5099,6 +5285,8 @@ snapshots: dependencies: '@types/geojson': 7946.0.16 + '@types/webxr@0.5.24': {} + '@vitejs/plugin-react@4.7.0(vite@6.4.2(jiti@2.6.1)(lightningcss@1.32.0))': dependencies: '@babel/core': 7.29.0 @@ -5151,6 +5339,8 @@ snapshots: loupe: 3.2.1 tinyrainbow: 1.2.0 + accessor-fn@1.5.3: {} + agent-base@7.1.4: {} aggregate-error@3.1.0: @@ -5454,22 +5644,69 @@ snapshots: dependencies: internmap: 2.0.3 + d3-binarytree@1.0.2: {} + + d3-color@3.1.0: {} + d3-dispatch@3.0.1: {} + d3-force-3d@3.0.6: + dependencies: + d3-binarytree: 1.0.2 + d3-dispatch: 3.0.1 + d3-octree: 1.1.0 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + d3-force@3.0.0: dependencies: d3-dispatch: 3.0.1 d3-quadtree: 3.0.1 d3-timer: 3.0.1 + d3-format@3.1.2: {} + d3-geo@3.1.1: dependencies: d3-array: 3.2.4 + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-octree@1.1.0: {} + d3-quadtree@3.0.1: {} + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + d3-timer@3.0.1: {} + data-bind-mapper@1.0.3: + dependencies: + accessor-fn: 1.5.3 + data-urls@7.0.0: dependencies: whatwg-mimetype: 5.0.0 @@ -5694,6 +5931,8 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + fflate@0.8.2: {} + figures@2.0.0: dependencies: escape-string-regexp: 1.0.5 @@ -5717,6 +5956,12 @@ snapshots: semver-regex: 4.0.5 super-regex: 1.1.0 + float-tooltip@1.7.5: + dependencies: + d3-selection: 3.0.0 + kapsule: 1.16.3 + preact: 10.29.1 + follow-redirects@1.16.0: {} form-data-encoder@4.1.0: {} @@ -5976,6 +6221,8 @@ snapshots: java-properties@1.0.2: {} + jerrypick@1.1.2: {} + jiti@2.6.1: {} js-tokens@4.0.0: {} @@ -6030,6 +6277,10 @@ snapshots: jwt-decode@4.0.0: {} + kapsule@1.16.3: + dependencies: + lodash-es: 4.18.1 + keyv@5.6.0: dependencies: '@keyv/serialize': 1.1.1 @@ -6113,6 +6364,10 @@ snapshots: lodash@4.18.1: {} + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + loupe@3.2.1: {} lowercase-keys@3.0.0: {} @@ -6164,6 +6419,8 @@ snapshots: merge2@1.4.1: {} + meshoptimizer@1.1.1: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -6201,6 +6458,22 @@ snapshots: nerf-dart@1.0.0: {} + ngraph.events@1.4.0: {} + + ngraph.forcelayout@3.3.1: + dependencies: + ngraph.events: 1.4.0 + ngraph.merge: 1.0.0 + ngraph.random: 1.2.0 + + ngraph.graph@20.1.2: + dependencies: + ngraph.events: 1.4.0 + + ngraph.merge@1.0.0: {} + + ngraph.random@1.2.0: {} + node-emoji@2.2.0: dependencies: '@sindresorhus/is': 4.6.0 @@ -6352,12 +6625,18 @@ snapshots: find-up: 2.1.0 load-json-file: 4.0.0 + polished@4.3.1: + dependencies: + '@babel/runtime': 7.29.2 + postcss@8.5.13: dependencies: nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 + preact@10.29.1: {} + prettier@3.8.3: {} pretty-format@27.5.1: @@ -6372,6 +6651,12 @@ snapshots: process-nextick-args@2.0.1: {} + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + proto-list@1.2.4: {} protocols@2.0.2: {} @@ -6396,6 +6681,13 @@ snapshots: react: 19.2.5 scheduler: 0.27.0 + react-force-graph-3d@1.29.1(react@19.2.5): + dependencies: + 3d-force-graph: 1.80.0 + prop-types: 15.8.1 + react: 19.2.5 + react-kapsule: 2.5.7(react@19.2.5) + react-hook-form@7.75.0(react@19.2.5): dependencies: react: 19.2.5 @@ -6411,8 +6703,15 @@ snapshots: react-dom: 19.2.5(react@19.2.5) typescript: 5.9.3 + react-is@16.13.1: {} + react-is@17.0.2: {} + react-kapsule@2.5.7(react@19.2.5): + dependencies: + jerrypick: 1.1.2 + react: 19.2.5 + react-leaflet@5.0.0(leaflet@1.9.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: '@react-leaflet/core': 3.0.0(leaflet@1.9.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -6737,6 +7036,31 @@ snapshots: dependencies: any-promise: 1.3.0 + three-forcegraph@1.43.4(three@0.184.0): + dependencies: + accessor-fn: 1.5.3 + d3-array: 3.2.4 + d3-force-3d: 3.0.6 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + data-bind-mapper: 1.0.3 + kapsule: 1.16.3 + ngraph.forcelayout: 3.3.1 + ngraph.graph: 20.1.2 + three: 0.184.0 + tinycolor2: 1.6.0 + + three-render-objects@1.41.1(three@0.184.0): + dependencies: + '@tweenjs/tween.js': 25.0.0 + accessor-fn: 1.5.3 + float-tooltip: 1.7.5 + kapsule: 1.16.3 + polished: 4.3.1 + three: 0.184.0 + + three@0.184.0: {} + through2@2.0.5: dependencies: readable-stream: 2.3.8 @@ -6748,6 +7072,8 @@ snapshots: tinybench@2.9.0: {} + tinycolor2@1.6.0: {} + tinyexec@0.3.2: {} tinyglobby@0.2.16: diff --git a/ordinis-admin-ui/src/components/graph/DictionaryGraph.tsx b/ordinis-admin-ui/src/components/graph/DictionaryGraph.tsx index 0b4ec20..2c369fe 100644 --- a/ordinis-admin-ui/src/components/graph/DictionaryGraph.tsx +++ b/ordinis-admin-ui/src/components/graph/DictionaryGraph.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from 'react' +import { Component, Suspense, lazy, useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import { useNavigate } from '@tanstack/react-router' import { useQueries } from '@tanstack/react-query' import { @@ -16,6 +16,50 @@ import type { DictionaryDefinition, SchemaDependent } from '@/api/client' import { LoadingBlock } from '@/ui' import { cn } from '@/lib/utils' +// 3D-режим лениво грузится — three.js (~650kb) и react-force-graph-3d не +// попадают в default chunk. Vite автоматически создаёт отдельный chunk для +// dynamic import. Suspense fallback показывается во время загрузки модуля. +const DictionaryGraph3D = lazy(() => import('./DictionaryGraph3D')) + +type GraphMode = '2d' | '3d' + +/** + * Scoped error boundary для 3D Suspense — three.js'у нужен WebGL, и если + * браузер его не поддерживает (corp VM, headless без --use-gl=*, hardware + * accel выключен) — `WebGLRenderer` бросает ошибку в render-цикле. Без + * этого ErrorBoundary fallback'a ошибка bubble'ит до AppErrorBoundary и + * вся page становится "Что-то пошло не так". Здесь мы её ловим локально, + * показываем friendly fallback + предлагаем вернуться в 2D режим. + * + * `onError` callback позволяет parent auto-reset'ить mode → '2d', чтобы + * пользователь не залипал на ошибочном состоянии. Recovery через + * componentDidUpdate (resetKey changes → state.hasError = false). + */ +class Graph3DErrorBoundary extends Component< + { children: ReactNode; onError: () => void; resetKey: number; fallback: ReactNode }, + { hasError: boolean } +> { + state = { hasError: false } + static getDerivedStateFromError() { + return { hasError: true } + } + componentDidCatch() { + // Auto-notify parent (will switch mode → '2d') чтобы next render не + // пытался монтировать 3D снова. Defer через setTimeout — синхронный + // setState из catch ломает React reconciliation. + setTimeout(() => this.props.onError(), 0) + } + componentDidUpdate(prev: { resetKey: number }) { + if (prev.resetKey !== this.props.resetKey && this.state.hasError) { + this.setState({ hasError: false }) + } + } + render() { + if (this.state.hasError) return this.props.fallback + return this.props.children + } +} + /** * Граф связей справочников — d3-force network diagram. * @@ -65,6 +109,16 @@ function colorForBundle(bundle: string): string { export function DictionaryGraph() { const dictsQuery = useDictionaries() const dicts = dictsQuery.data ?? [] + // localStorage persist чтобы preference сохранился между переходами. + // SSR-safe guard на window — vitest setup использует jsdom где window есть, + // но defensive чтобы не упасть на SSR в будущем. + const [mode, setMode] = useState(() => { + if (typeof window === 'undefined') return '2d' + return (localStorage.getItem('graph.mode') as GraphMode) === '3d' ? '3d' : '2d' + }) + useEffect(() => { + if (typeof window !== 'undefined') localStorage.setItem('graph.mode', mode) + }, [mode]) // Fetch dependents для всех dicts параллельно (cached между mounts). const dependentsResults = useQueries({ @@ -101,6 +155,8 @@ export function DictionaryGraph() { dicts={dicts} dependentsByDict={dependentsByDict} depsLoading={allDependentsLoading} + mode={mode} + onModeChange={setMode} /> ) } @@ -109,11 +165,18 @@ function GraphCanvas({ dicts, dependentsByDict, depsLoading, + mode, + onModeChange, }: { dicts: DictionaryDefinition[] dependentsByDict: Map depsLoading: boolean + mode: GraphMode + onModeChange: (m: GraphMode) => void }) { + // WebGL-fallback banner — показывается несколько секунд после авто-возврата + // в 2D из-за ошибки 3D renderer'а (нет WebGL / hardware accel выключено). + const [webglFallbackNotice, setWebglFallbackNotice] = useState(false) const navigate = useNavigate() const wrapperRef = useRef(null) const svgRef = useRef(null) @@ -230,45 +293,128 @@ function GraphCanvas({ Загрузка связей… )} - {/* Zoom controls — overlay top-right. */} -
- - - - - {Math.round(view.zoom * 100)}% - + {/* Controls overlay — top-right. Stack из 2D/3D toggle + zoom (только в 2D). */} +
+ {/* 2D / 3D mode toggle */} +
+ + +
+ + {/* Zoom controls — только в 2D, в 3D camera orbit имеет свой wheel-zoom. */} + {mode === '2d' && ( +
+ + + + + {Math.round(view.zoom * 100)}% + +
+ )}
- { + // WebGL undefined / hardware accel off → switch back в 2D. + // Banner показывается ~5s, потом сам исчезает. + onModeChange('2d') + setWebglFallbackNotice(true) + setTimeout(() => setWebglFallbackNotice(false), 5000) + }} + fallback={ +
+
+

3D-режим недоступен

+

+ Браузер не смог инициализировать WebGL. Возможно отключено + аппаратное ускорение (chrome://gpu) или используется среда без + поддержки 3D. Возврат к 2D-режиму… +

+
+
+ } + > + + +
+ } + > + + + + )} + + {webglFallbackNotice && ( +
+ 3D недоступен в этом браузере — переключено на 2D. +
+ )} + + {mode === '2d' && {/* === end zoom/pan transform === */} - + } {/* Legend */}
Bundle
diff --git a/ordinis-admin-ui/src/components/graph/DictionaryGraph3D.tsx b/ordinis-admin-ui/src/components/graph/DictionaryGraph3D.tsx new file mode 100644 index 0000000..8c2ff89 --- /dev/null +++ b/ordinis-admin-ui/src/components/graph/DictionaryGraph3D.tsx @@ -0,0 +1,159 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { useNavigate } from '@tanstack/react-router' +import ForceGraph3D, { type ForceGraphMethods } from 'react-force-graph-3d' +import type { DictionaryDefinition, SchemaDependent } from '@/api/client' + +/** + * 3D-режим графа справочников через react-force-graph-3d (three.js + d3-force-3d). + * + * Этот компонент динамически импортируется из DictionaryGraph, чтобы three.js + * (~650kb minified) не попадал в default chunk. Lazy-loaded только когда + * пользователь явно переключает в 3D режим. + * + * Поведение: + * - Те же nodes/links что в 2D-моде (общая data shape) + * - Camera orbit controls (drag = rotate, wheel = zoom) + * - Hover → tooltip с displayName + record count + * - Click → navigate /dictionaries/$name + * - Цвета и размеры узлов идентичны 2D (single source of truth) + * - Directional particles на edges — лёгкая визуализация направления FK + * + * Performance: 3D-граф handles 200+ nodes легко (GPU rendering), но default + * configuration уже good enough для current ~37 dicts. + */ + +type Node3D = { + id: string + name: string + displayName: string + bundle: string + scope: string + recordCount: number + // Internal three coords — populated by force engine + x?: number + y?: number + z?: number +} + +type Link3D = { + source: string | Node3D + target: string | Node3D + sourceField: string + targetField: string +} + +const BUNDLE_COLORS: Record = { + cuod: '#b05a2e', + geo: '#4f7038', + comms: '#a8762a', + delivery: '#3a2818', + default: '#52483d', +} + +const colorForBundle = (bundle: string): string => + BUNDLE_COLORS[bundle] ?? BUNDLE_COLORS.default + +type Props = { + dicts: DictionaryDefinition[] + dependentsByDict: Map + width: number + height: number +} + +export default function DictionaryGraph3D({ + dicts, + dependentsByDict, + width, + height, +}: Props) { + const navigate = useNavigate() + const graphRef = useRef | undefined>(undefined) + const [hoverId, setHoverId] = useState(null) + + // Build graph data — same logic as 2D mode, без simulation refs (three + // worker handles physics). + const graphData = useMemo(() => { + const nodes: Node3D[] = dicts.map((d) => ({ + id: d.name, + name: d.name, + displayName: d.displayName ?? d.name, + bundle: d.bundle, + scope: d.scope, + recordCount: d.recordCount ?? 0, + })) + const nodeIds = new Set(nodes.map((n) => n.id)) + const links: Link3D[] = [] + dicts.forEach((target) => { + const deps = dependentsByDict.get(target.name) ?? [] + deps.forEach((dep) => { + if (nodeIds.has(dep.sourceDict)) { + links.push({ + source: dep.sourceDict, + target: target.name, + sourceField: dep.sourceField, + targetField: dep.targetField, + }) + } + }) + }) + return { nodes, links } + }, [dicts, dependentsByDict]) + + // Tune camera + force layout once after mount. + useEffect(() => { + const fg = graphRef.current + if (!fg) return + // Подальше отвести камеру для просторного 3D-обзора. + fg.cameraPosition({ x: 0, y: 0, z: 400 }) + }, []) + + // CSS variable resolution — three не понимает var(--color-*), + // grab raw hex values from document root для accent + ink. + const accentHex = '#b05a2e' + const lineHex = '#e6e1d4' + + return ( +
+ + ref={graphRef} + graphData={graphData} + width={width} + height={height} + backgroundColor="rgba(0,0,0,0)" + nodeId="id" + nodeLabel={(n) => + `
${n.displayName}
${n.bundle} · ${n.recordCount} записей
` + } + nodeColor={(n) => (hoverId === n.id ? accentHex : colorForBundle(n.bundle))} + nodeVal={(n) => 1 + Math.log10((n.recordCount ?? 1) + 1) * 2} + nodeOpacity={hoverId ? 0.85 : 0.95} + nodeResolution={16} + linkColor={(l) => { + const sid = typeof l.source === 'string' ? l.source : (l.source as Node3D).id + const tid = typeof l.target === 'string' ? l.target : (l.target as Node3D).id + return hoverId && (sid === hoverId || tid === hoverId) ? accentHex : lineHex + }} + linkOpacity={hoverId ? 0.4 : 0.5} + linkWidth={(l) => { + const sid = typeof l.source === 'string' ? l.source : (l.source as Node3D).id + const tid = typeof l.target === 'string' ? l.target : (l.target as Node3D).id + return hoverId && (sid === hoverId || tid === hoverId) ? 1.5 : 0.5 + }} + linkDirectionalArrowLength={3} + linkDirectionalArrowRelPos={1} + linkDirectionalArrowColor={() => accentHex} + linkDirectionalParticles={(l) => { + const sid = typeof l.source === 'string' ? l.source : (l.source as Node3D).id + const tid = typeof l.target === 'string' ? l.target : (l.target as Node3D).id + return hoverId && (sid === hoverId || tid === hoverId) ? 2 : 0 + }} + linkDirectionalParticleSpeed={0.006} + linkDirectionalParticleWidth={2} + onNodeHover={(n) => setHoverId(n?.id ?? null)} + onNodeClick={(n) => navigate({ to: '/dictionaries/$name', params: { name: n.name } })} + // Slight cooldown extension чтобы layout успел осесть. + cooldownTicks={120} + /> +
+ ) +} diff --git a/ordinis-admin-ui/vite.config.ts b/ordinis-admin-ui/vite.config.ts index dd179ef..dbc0cbf 100644 --- a/ordinis-admin-ui/vite.config.ts +++ b/ordinis-admin-ui/vite.config.ts @@ -129,6 +129,20 @@ export default defineConfig({ // Axios + парсеры if (id.match(/node_modules\/(axios|jose|crypto-js)\//)) return 'vendor-net' + // 3D-graph (three.js + react-force-graph-3d + transitive deps). + // Изолирован в свой chunk, чтобы НЕ попасть в vendor-misc, который + // загружается eagerly. DictionaryGraph3D импортируется через + // React.lazy() — Rollup auto-defers этот chunk до первого + // дин-импорта. ~500kb gzipped (~600kb three.js + force graph + // wrappers), грузится только при переключении в 3D режим. + if ( + id.match( + /node_modules\/(three|3d-force-graph|d3-force-3d|kapsule|react-kapsule|three-forcegraph|three-render-objects|react-force-graph-3d|@tweenjs)/, + ) + ) { + return 'vendor-graph3d' + } + // Остальное — в общий vendor (не должно быть много) return 'vendor-misc' },