Compare commits

...

1 Commits

Author SHA1 Message Date
Eduard Gert
9e2e38764e Add control center (#494)
Some checks failed
build and push / build_n_push (push) Has been cancelled
* Add control center

* Update rdp doc link
2025-10-09 11:26:21 +02:00
33 changed files with 3857 additions and 15 deletions

View File

@@ -26,6 +26,7 @@ The dashboard makes it possible to:
- NextJS
- ReactJS
- Tailwind CSS
- [React Flow](https://reactflow.dev/) for the Control Center
- Auth0
- Nginx
- Docker

View File

@@ -7,6 +7,10 @@ server {
root /usr/share/nginx/html;
default_type application/wasm;
}
location = /ironrdp-pkg/ironrdp_web_bg.wasm {
root /usr/share/nginx/html;
default_type application/wasm;
}
location / {
try_files $uri $uri.html $uri/ =404;

824
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "2.0.0",
"dependencies": {
"@axa-fr/react-oidc": "^7.22.18",
"@dagrejs/dagre": "^1.1.5",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
@@ -30,6 +31,7 @@
"@tanstack/match-sorter-utils": "^8.8.4",
"@tanstack/react-table": "^8.10.7",
"@types/crypto-js": "^4.2.2",
"@types/d3": "^7.4.3",
"@types/lodash": "^4.14.200",
"@types/node": "20.10.6",
"@types/react": "^18",
@@ -37,6 +39,7 @@
"@types/react-window": "^1.8.8",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"@xyflow/react": "^12.8.4",
"autoprefixer": "^10",
"chart.js": "^4.4.8",
"chroma-js": "^3.1.2",
@@ -44,8 +47,10 @@
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"crypto-js": "^4.2.0",
"d3": "^7.9.0",
"date-fns": "^2.30.0",
"dayjs": "^1.11.10",
"elkjs": "^0.10.0",
"eslint": "^8",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
@@ -55,7 +60,7 @@
"ip-cidr": "^3.1.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"lucide-react": "^0.481.0",
"lucide-react": "^0.539.0",
"next": "^14.2.28",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
@@ -201,6 +206,24 @@
"ms": "^2.1.1"
}
},
"node_modules/@dagrejs/dagre": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.5.tgz",
"integrity": "sha512-Ghgrh08s12DCL5SeiR6AoyE80mQELTWhJBRmXfFoqDiFkR458vPEdgTbbjA0T+9ETNxUblnD0QW55tfdvi5pjQ==",
"license": "MIT",
"dependencies": {
"@dagrejs/graphlib": "2.2.4"
}
},
"node_modules/@dagrejs/graphlib": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.4.tgz",
"integrity": "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==",
"license": "MIT",
"engines": {
"node": ">17.0.0"
}
},
"node_modules/@emotion/is-prop-valid": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
@@ -2588,6 +2611,265 @@
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
"integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ=="
},
"node_modules/@types/d3": {
"version": "7.4.3",
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
"integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
"license": "MIT",
"dependencies": {
"@types/d3-array": "*",
"@types/d3-axis": "*",
"@types/d3-brush": "*",
"@types/d3-chord": "*",
"@types/d3-color": "*",
"@types/d3-contour": "*",
"@types/d3-delaunay": "*",
"@types/d3-dispatch": "*",
"@types/d3-drag": "*",
"@types/d3-dsv": "*",
"@types/d3-ease": "*",
"@types/d3-fetch": "*",
"@types/d3-force": "*",
"@types/d3-format": "*",
"@types/d3-geo": "*",
"@types/d3-hierarchy": "*",
"@types/d3-interpolate": "*",
"@types/d3-path": "*",
"@types/d3-polygon": "*",
"@types/d3-quadtree": "*",
"@types/d3-random": "*",
"@types/d3-scale": "*",
"@types/d3-scale-chromatic": "*",
"@types/d3-selection": "*",
"@types/d3-shape": "*",
"@types/d3-time": "*",
"@types/d3-time-format": "*",
"@types/d3-timer": "*",
"@types/d3-transition": "*",
"@types/d3-zoom": "*"
}
},
"node_modules/@types/d3-array": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
"license": "MIT"
},
"node_modules/@types/d3-axis": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
"integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
"license": "MIT",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-brush": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
"integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
"license": "MIT",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-chord": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
"integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
"license": "MIT"
},
"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-contour": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
"integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
"license": "MIT",
"dependencies": {
"@types/d3-array": "*",
"@types/geojson": "*"
}
},
"node_modules/@types/d3-delaunay": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
"integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
"license": "MIT"
},
"node_modules/@types/d3-dispatch": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
"integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
"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-dsv": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
"integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
"license": "MIT"
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
"license": "MIT"
},
"node_modules/@types/d3-fetch": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
"integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
"license": "MIT",
"dependencies": {
"@types/d3-dsv": "*"
}
},
"node_modules/@types/d3-force": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
"integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
"license": "MIT"
},
"node_modules/@types/d3-format": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
"integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
"license": "MIT"
},
"node_modules/@types/d3-geo": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
"integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/d3-hierarchy": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
"integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
"license": "MIT"
},
"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-path": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
"license": "MIT"
},
"node_modules/@types/d3-polygon": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
"integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
"license": "MIT"
},
"node_modules/@types/d3-quadtree": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
"integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
"license": "MIT"
},
"node_modules/@types/d3-random": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
"integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
"license": "MIT"
},
"node_modules/@types/d3-scale": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"license": "MIT",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-scale-chromatic": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
"integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
"license": "MIT"
},
"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-shape": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
"license": "MIT",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
"license": "MIT"
},
"node_modules/@types/d3-time-format": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
"integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
"license": "MIT"
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"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/geojson": {
"version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"license": "MIT"
},
"node_modules/@types/js-cookie": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
@@ -2911,6 +3193,38 @@
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
"license": "MIT"
},
"node_modules/@xyflow/react": {
"version": "12.8.6",
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.6.tgz",
"integrity": "sha512-SksAm2m4ySupjChphMmzvm55djtgMDPr+eovPDdTnyGvShf73cvydfoBfWDFllooIQ4IaiUL5yfxHRwU0c37EA==",
"license": "MIT",
"dependencies": {
"@xyflow/system": "0.0.70",
"classcat": "^5.0.3",
"zustand": "^4.4.0"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@xyflow/system": {
"version": "0.0.70",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.70.tgz",
"integrity": "sha512-PpC//u9zxdjj0tfTSmZrg3+sRbTz6kop/Amky44U2Dl51sxzDTIUfXMwETOYpmr2dqICWXBIJwXL2a9QWtX2XA==",
"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.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
@@ -3724,6 +4038,12 @@
"url": "https://joebell.co.uk"
}
},
"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.3.2",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
@@ -4209,6 +4529,416 @@
"node": "^16.0.0 || ^18.0.0 || >=20.0.0"
}
},
"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",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"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",
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-drag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-selection": "3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-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-dsv/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/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-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",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"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",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"engines": {
"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",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-transition": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3",
"d3-dispatch": "1 - 3",
"d3-ease": "1 - 3",
"d3-interpolate": "1 - 3",
"d3-timer": "1 - 3"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"d3-selection": "2 - 3"
}
},
"node_modules/d3-zoom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-drag": "2 - 3",
"d3-interpolate": "1 - 3",
"d3-selection": "2 - 3",
"d3-transition": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -4305,6 +5035,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"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",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -4400,6 +5139,12 @@
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.592.tgz",
"integrity": "sha512-D3NOkROIlF+d5ixnz7pAf3Lu/AuWpd6AYgI9O67GQXMXTcCP1gJQRotOq35eQy5Sb4hez33XH1YdTtILA7Udww=="
},
"node_modules/elkjs": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.10.2.tgz",
"integrity": "sha512-Yx3ORtbAFrXelYkAy2g0eYyVY8QG0XEmGdQXmy0eithKKjbWRfl3Xe884lfkszfBF6UKyIy4LwfcZ3AZc8oxFw==",
"license": "EPL-2.0"
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@@ -5761,6 +6506,18 @@
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
"integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ=="
},
"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/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -5860,6 +6617,15 @@
"node": ">= 0.4"
}
},
"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",
@@ -6644,9 +7410,9 @@
"license": "ISC"
},
"node_modules/lucide-react": {
"version": "0.481.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.481.0.tgz",
"integrity": "sha512-NrvUDNFwgLIvHiwTEq9boa5Kiz1KdUT8RJ+wmNijwxdn9U737Fw42c43sRxJTMqhL+ySHpGRVCWpwiF+abrEjw==",
"version": "0.539.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.539.0.tgz",
"integrity": "sha512-VVISr+VF2krO91FeuCrm1rSOLACQUYVy7NQkzrOty52Y8TlTPcXcMdQFj9bYzBgXbWCiywlwSZ3Z8u6a+6bMlg==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -7836,6 +8602,12 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"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/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -7858,6 +8630,12 @@
"queue-microtask": "^1.2.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/rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
@@ -7924,7 +8702,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true,
"license": "MIT"
},
"node_modules/scheduler": {
@@ -8905,11 +9682,12 @@
}
},
"node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": {
@@ -9101,6 +9879,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
}
}
}
}
}

View File

@@ -14,6 +14,7 @@
},
"dependencies": {
"@axa-fr/react-oidc": "^7.22.18",
"@dagrejs/dagre": "^1.1.5",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
@@ -35,6 +36,7 @@
"@tanstack/match-sorter-utils": "^8.8.4",
"@tanstack/react-table": "^8.10.7",
"@types/crypto-js": "^4.2.2",
"@types/d3": "^7.4.3",
"@types/lodash": "^4.14.200",
"@types/node": "20.10.6",
"@types/react": "^18",
@@ -42,6 +44,7 @@
"@types/react-window": "^1.8.8",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"@xyflow/react": "^12.8.4",
"autoprefixer": "^10",
"chart.js": "^4.4.8",
"chroma-js": "^3.1.2",
@@ -49,8 +52,10 @@
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"crypto-js": "^4.2.0",
"d3": "^7.9.0",
"date-fns": "^2.30.0",
"dayjs": "^1.11.10",
"elkjs": "^0.10.0",
"eslint": "^8",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
@@ -60,7 +65,7 @@
"ip-cidr": "^3.1.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"lucide-react": "^0.481.0",
"lucide-react": "^0.539.0",
"next": "^14.2.28",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",

View File

@@ -0,0 +1,8 @@
import { globalMetaTitle } from "@utils/meta";
import type { Metadata } from "next";
import BlankLayout from "@/layouts/BlankLayout";
export const metadata: Metadata = {
title: `Control Center - ${globalMetaTitle}`,
};
export default BlankLayout;

File diff suppressed because it is too large Load Diff

View File

@@ -167,4 +167,10 @@ p {
.xterm-viewport {
@apply m-0 p-0 box-border;
}
}
/* Control Center */
.react-flow__node-groupNode .selected{
@apply border-netbird;
}

View File

@@ -0,0 +1,22 @@
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
export default function ControlCenterIcon(props: IconProps) {
return (
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
{...iconProperties(props)}
>
<path d="M5 3a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5Zm0 12a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2H5Zm12 0a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2h-2Zm0-12a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2h-2Z" />
<path
fillRule="evenodd"
d="M10 6.5a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1ZM10 18a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Zm-4-4a1 1 0 0 1-1-1v-2a1 1 0 1 1 2 0v2a1 1 0 0 1-1 1Zm12 0a1 1 0 0 1-1-1v-2a1 1 0 1 1 2 0v2a1 1 0 0 1-1 1Z"
clipRule="evenodd"
/>
</svg>
);
}

View File

@@ -1,9 +1,12 @@
"use client";
import { ScrollArea } from "@components/ScrollArea";
import { SmallBadge } from "@components/ui/SmallBadge";
import { cn } from "@utils/helpers";
import * as React from "react";
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
import ActivityIcon from "@/assets/icons/ActivityIcon";
import ControlCenterIcon from "@/assets/icons/ControlCenterIcon";
import DNSIcon from "@/assets/icons/DNSIcon";
import DocsIcon from "@/assets/icons/DocsIcon";
import PeerIcon from "@/assets/icons/PeerIcon";
@@ -67,6 +70,23 @@ export default function Navigation({
>
<div>
<SidebarItemGroup>
<SidebarItem
icon={<ControlCenterIcon size={16} />}
label={
<div className={"flex items-center gap-2"}>
Control Center
<SmallBadge
text={"Beta"}
variant={"sky"}
className={"text-[8px] leading-none py-[3px] px-[5px]"}
textClassName={"top-0"}
/>
</div>
}
href={"/control-center"}
visible={permission.policies.read}
/>
<SidebarItem
icon={<PeerIcon />}
label="Peers"

View File

@@ -0,0 +1,48 @@
import { SegmentedTabs } from "@components/SegmentedTabs";
import { FolderGit2, MonitorSmartphoneIcon, NetworkIcon } from "lucide-react";
import * as React from "react";
export enum FlowView {
NETWORKS = "networks",
GROUPS = "groups",
PEERS = "peers",
}
type Props = {
value?: FlowView;
onChange?: (value: FlowView) => void;
};
export const FlowSelector = ({ value, onChange }: Props) => {
return (
<SegmentedTabs value={value} onChange={(v) => onChange?.(v as FlowView)}>
<SegmentedTabs.List
className={
"border-b rounded-b-lg text-sm font-medium bg-nb-gray-930 p-1"
}
>
<SegmentedTabs.Trigger
value={FlowView.PEERS}
className={"text-xs px-3 py-1"}
>
<MonitorSmartphoneIcon size={12} />
Peers
</SegmentedTabs.Trigger>
<SegmentedTabs.Trigger
value={FlowView.GROUPS}
className={"text-xs px-3 py-1"}
>
<FolderGit2 size={12} />
Groups
</SegmentedTabs.Trigger>
<SegmentedTabs.Trigger
value={FlowView.NETWORKS}
className={"text-xs px-3 py-[0.45rem]"}
>
<NetworkIcon size={12} />
Networks
</SegmentedTabs.Trigger>
</SegmentedTabs.List>
</SegmentedTabs>
);
};

View File

@@ -0,0 +1,48 @@
import Button from "@components/Button";
import useFetchApi from "@utils/api";
import { cn } from "@utils/helpers";
import * as React from "react";
import { useMemo } from "react";
import CircleIcon from "@/assets/icons/CircleIcon";
import { Network, NetworkRouter } from "@/interfaces/Network";
import { Peer } from "@/interfaces/Peer";
type Props = {
network: Network;
};
export const NetworkRoutingPeerCount = ({ network }: Props) => {
const { data: routers, isLoading: isRoutersLoading } =
useFetchApi<NetworkRouter[]>("/networks/routers");
const { data: peers, isLoading: isPeersLoading } =
useFetchApi<Peer[]>("/peers");
const routingPeerStatusColor = useMemo(() => {
if (!network) return "bg-nb-gray-500";
const routerCount = network.routers?.length || 0;
if (routerCount === 0) return "bg-nb-gray-500";
if (routerCount === 1) return "bg-yellow-400";
if (routerCount > 1) return "bg-green-400";
return "bg-nb-gray-500";
}, [network]);
const networkRouters = useMemo(() => {
if (!network || !peers) return [];
const routerIds = network?.routers?.map((r) => r) || [];
return routers?.filter((r) => routerIds.includes(r.id)) || [];
}, [network, peers, routers]);
return (
<Button
variant={"secondary"}
size={"xs"}
className={"!bg-nb-gray-930 !text-nb-gray-300 cursor-default"}
>
<CircleIcon
size={8}
className={cn("shrink-0 block", routingPeerStatusColor)}
/>
{network.routers?.length || 0} Routing Peer(s)
</Button>
);
};

View File

@@ -0,0 +1,125 @@
import { Edge, useInternalNode } from "@xyflow/react";
import React from "react";
import { getEdgeParams } from "@/modules/control-center/utils/edge-helper";
type AnimatedLineProps = Edge<
{
label?: string;
color?: string;
},
"animated-line"
>;
function AnimatedLine({ id, source, target, data }: AnimatedLineProps) {
const sourceNode = useInternalNode(source);
const targetNode = useInternalNode(target);
if (!sourceNode || !targetNode) return null;
const { sx, sy, tx, ty } = getEdgeParams(sourceNode, targetNode);
const labelX = (sx + tx) / 2;
const labelY = (sy + ty) / 2;
let angle = Math.atan2(ty - sy, tx - sx) * (180 / Math.PI);
if (angle < -90 || angle > 90) {
angle += 180;
}
const label = data?.label || "";
const hasLabel = label?.length > 0;
const fontSize = 12;
const paddingX = hasLabel ? 2 : 0;
const paddingY = hasLabel ? 2 : 0;
const gapWidth = hasLabel ? 4 : 0;
const labelTextWidth = label.length * 7;
const labelWidth = gapWidth + labelTextWidth + paddingX * 2;
const labelHeight = fontSize + paddingY * 2;
const dx = tx - sx;
const dy = ty - sy;
const length = Math.sqrt(dx * dx + dy * dy);
const gap = labelWidth / 2;
const nx = dx / length;
const ny = dy / length;
const preLabelX = labelX - nx * gap;
const preLabelY = labelY - ny * gap;
const postLabelX = labelX + nx * gap;
const postLabelY = labelY + ny * gap;
const color = data?.color || "#0e9f6e";
return (
<>
<line
x1={sx}
y1={sy}
x2={preLabelX}
y2={preLabelY}
stroke={color}
strokeWidth={2}
strokeDasharray="5, 5"
>
<animate
attributeName="stroke-dashoffset"
from="20"
to="0"
dur="0.5s"
repeatCount="indefinite"
/>
</line>
<line
x1={postLabelX}
y1={postLabelY}
x2={tx}
y2={ty}
stroke={color}
strokeWidth={2}
strokeDasharray="5, 5"
>
<animate
attributeName="stroke-dashoffset"
from="20"
to="0"
dur="0.5s"
repeatCount="indefinite"
/>
</line>
{label && hasLabel && (
<foreignObject
x={labelX - labelWidth / 2}
y={labelY - labelHeight / 2}
width={labelWidth}
height={labelHeight}
style={{ overflow: "visible" }}
>
<div
style={{
width: labelWidth,
height: labelHeight,
fontSize,
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: `${paddingY}px ${paddingX}px`,
transform: `rotate(${angle}deg)`,
transformOrigin: "center center",
boxSizing: "border-box",
background: "none",
}}
className={
"flex items-center justify-center gap-1 select-none pointer-events-none z-10 text-green-50"
}
>
<div className={"whitespace-nowrap"}>{label}</div>
</div>
</foreignObject>
)}
</>
);
}
export default AnimatedLine;

View File

@@ -0,0 +1,70 @@
import { BaseEdge, type EdgeProps, getSmoothStepPath } from "@xyflow/react";
import React from "react";
export function BidirectionalEdges({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
}: EdgeProps) {
const [forwardPath] = getSmoothStepPath({
sourceX: sourceX - 5,
sourceY: sourceY - 5,
sourcePosition,
targetX: targetX + 15,
targetY: targetY - 5,
targetPosition,
});
const [backwardPath] = getSmoothStepPath({
sourceX: targetX + 5,
sourceY: targetY + 5,
sourcePosition: targetPosition,
targetX: sourceX - 15,
targetY: sourceY + 5,
targetPosition: sourcePosition,
});
return (
<>
<BaseEdge
id={`${id}-forward`}
path={forwardPath}
style={{
strokeWidth: 2,
stroke: "#0e9f6e",
strokeDasharray: "5, 5",
}}
>
<animate
attributeName="stroke-dashoffset"
from="20"
to="0"
dur="0.5s"
repeatCount="indefinite"
/>
</BaseEdge>
<BaseEdge
id={`${id}-backward`}
path={backwardPath}
style={{
strokeWidth: 2,
stroke: "#0e9f6e",
strokeDasharray: "5, 5",
}}
>
<animate
attributeName="stroke-dashoffset"
from="20"
to="0"
dur="0.5s"
repeatCount="indefinite"
/>
</BaseEdge>
</>
);
}

View File

@@ -0,0 +1,92 @@
import {
BaseEdge,
type EdgeProps,
getSimpleBezierPath,
getSmoothStepPath,
getStraightPath,
} from "@xyflow/react";
import React from "react";
type Props = {
data: {
enabled: boolean;
type: "smoothstep" | "straight" | "bezier";
};
} & EdgeProps;
export function DirectionIn({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
data,
}: Props) {
const { enabled, type = "straight" } = data;
const getPath = () => {
switch (type) {
case "straight":
return getStraightPath({
sourceX,
sourceY,
targetX,
targetY,
});
case "bezier":
return getSimpleBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
case "smoothstep":
return getSmoothStepPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
default:
return getSmoothStepPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
}
};
const [edgePath] = getPath();
return (
<BaseEdge
id={id}
path={edgePath}
style={{
opacity: enabled ? 1 : 0.6,
strokeWidth: 2,
stroke: enabled ? "#0e9f6e" : "#787878",
strokeDasharray: "5, 5",
}}
>
{enabled && (
<animate
attributeName="stroke-dashoffset"
from="20"
to="0"
dur="0.5s"
repeatCount="indefinite"
/>
)}
</BaseEdge>
);
}

View File

@@ -0,0 +1,53 @@
import {
BaseEdge,
EdgeProps,
getBezierPath,
useInternalNode,
} from "@xyflow/react";
import React from "react";
import { getEdgeParams } from "@/modules/control-center/utils/edge-helper";
function FloatingEdge({ id, source, target, markerEnd, style }: EdgeProps) {
const sourceNode = useInternalNode(source);
const targetNode = useInternalNode(target);
if (!sourceNode || !targetNode) {
return null;
}
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(
sourceNode,
targetNode,
);
const [edgePath] = getBezierPath({
sourceX: sx,
sourceY: sy,
sourcePosition: sourcePos,
targetPosition: targetPos,
targetX: tx,
targetY: ty,
});
return (
<BaseEdge
id={id}
path={edgePath}
style={{
strokeWidth: 2,
stroke: "#0e9f6e",
strokeDasharray: "5, 5",
}}
>
<animate
attributeName="stroke-dashoffset"
from="20"
to="0"
dur="0.5s"
repeatCount="indefinite"
/>
</BaseEdge>
);
}
export default FloatingEdge;

View File

@@ -0,0 +1,45 @@
import { BaseEdge, type EdgeProps, getSimpleBezierPath } from "@xyflow/react";
import React from "react";
import { useSourceGroupEnabled } from "@/modules/control-center/utils/helpers";
type Props = {
data: {
enabled: boolean;
};
} & EdgeProps;
export function SimpleConnection({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
data,
source,
}: Props) {
const [edgePath] = getSimpleBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
const enabled = useSourceGroupEnabled(source);
return (
<BaseEdge
id={id}
path={edgePath}
style={{
strokeWidth: 1.5,
stroke: "#595959",
strokeDasharray: "0, 0",
opacity: enabled ? 1 : 0.6,
}}
></BaseEdge>
);
}

View File

@@ -0,0 +1,111 @@
import TruncatedText from "@components/ui/TruncatedText";
import { getOperatingSystem } from "@hooks/useOperatingSystem";
import { cn } from "@utils/helpers";
import { GlobeIcon, NetworkIcon, WorkflowIcon } from "lucide-react";
import * as React from "react";
import RoundedFlag from "@/assets/countries/RoundedFlag";
import { NetworkResource } from "@/interfaces/Network";
import { OperatingSystem } from "@/interfaces/OperatingSystem";
import type { Peer } from "@/interfaces/Peer";
import { OSLogo } from "@/modules/peers/PeerOSCell";
type DeviceCardProps = {
device?: Peer;
resource?: NetworkResource;
className?: string;
};
export const DeviceCard = ({
device,
resource,
className,
}: DeviceCardProps) => {
if (!device && !resource) return;
return (
<div
className={cn(
"flex shrink-0 items-center gap-2.5 text-nb-gray-300 text-left py-1 pl-3 pr-4 rounded-md group/machine my-0 w-[200px]",
className,
)}
>
<div
className={cn(
"flex items-center justify-center rounded-md h-9 w-9 shrink-0 bg-nb-gray-850 transition-all",
"group-hover:bg-nb-gray-800 relative",
)}
>
{device && <PeerOSIcon os={device.os} />}
{resource?.type && <ResourceIcon type={resource.type} />}
{device?.country_code && (
<div className={"absolute -bottom-[4px] -right-[4px]"}>
<div
className={cn(
"flex items-center justify-center rounded-full border-[3px] shrink-0",
"border-nb-gray-940",
)}
>
<RoundedFlag country={device?.country_code} size={10} />
</div>
</div>
)}
</div>
<div className={"flex flex-col gap-0 justify-center mt-2 leading-tight"}>
<span
className={
"mb-1.5 font-normal text-[0.85rem] text-nb-gray-100 flex items-center gap-2"
}
>
<TruncatedText
text={device?.name || resource?.name || "Unknown"}
maxWidth={"150px"}
hideTooltip={true}
/>
</span>
<span
className={
"text-sm font-normal text-nb-gray-400 -top-[0.3rem] relative"
}
>
{device?.ip || resource?.address}
</span>
</div>
</div>
);
};
const PeerOSIcon = ({ os }: { os: string }) => {
const osType = getOperatingSystem(os);
return (
<div
className={cn(
"flex items-center justify-center grayscale brightness-[100%] contrast-[40%]",
"w-4 h-4 shrink-0",
osType === OperatingSystem.WINDOWS && "p-[2.5px]",
osType === OperatingSystem.APPLE && "p-[2.7px]",
osType === OperatingSystem.FREEBSD && "p-[1.5px]",
)}
>
<OSLogo os={os} />
</div>
);
};
const ResourceIcon = ({
type,
size = 15,
}: {
type: "domain" | "host" | "subnet";
size?: number;
}) => {
switch (type) {
case "domain":
return <GlobeIcon size={size} />;
case "subnet":
return <NetworkIcon size={size} />;
case "host":
return <WorkflowIcon size={size} />;
default:
return <WorkflowIcon size={size} />;
}
};

View File

@@ -0,0 +1,80 @@
import { GroupBadgeIcon } from "@components/ui/GroupBadgeIcon";
import { cn } from "@utils/helpers";
import { Handle, type Node, Position } from "@xyflow/react";
import * as React from "react";
import { useMemo } from "react";
import { Group } from "@/interfaces/Group";
type GroupNodeProps = Node<
{
group: Group;
enabled: boolean;
hoverable?: boolean;
onClick?: (g: Group) => void;
},
"groupNode"
>;
export const GroupNode = ({ data, id }: GroupNodeProps) => {
const { enabled = true, group, hoverable = true, onClick } = data;
const countLabel = useMemo(() => {
const peerCount = group?.peers_count || 0;
const resourceCount = group?.resources_count || 0;
if (resourceCount === 0) {
return `${peerCount} Peer(s)`;
}
if (peerCount === 0) {
return `${resourceCount} Resource(s)`;
}
return `${peerCount} Peer(s), ${resourceCount} Resource(s)`;
}, [group?.peers_count, group?.resources_count]);
return (
<div
className={cn(
"cc-group-node bg-nb-gray-940 border border-nb-gray-800 rounded-lg overflow-hidden transition-all",
!enabled && "opacity-60",
hoverable && "hover:bg-nb-gray-930 cursor-pointer",
)}
onClick={() => onClick?.(group)}
>
<div
className={
"flex w-full items-center justify-between text-nb-gray-300 gap-2 text-sm pl-3 pr-5 py-3 font-normal"
}
>
<div className={"flex items-center gap-3 font-normal text-sm"}>
<div
className={
"h-9 w-9 bg-nb-gray-850 rounded-md flex items-center justify-center shrink-0"
}
>
<GroupBadgeIcon id={group?.id} issued={group?.issued} size={14} />
</div>
<div>
<div className={" text-nb-gray-200 font-normal whitespace-nowrap"}>
{group.name}
</div>
<div className={"text-nb-gray-400 whitespace-nowrap text-xs"}>
{countLabel}
</div>
</div>
</div>
</div>
<Handle
type="source"
position={Position.Right}
id={"sr"}
className={"opacity-0"}
/>
<Handle
type="target"
position={Position.Left}
id={"tl"}
className={"opacity-0"}
/>
</div>
);
};

View File

@@ -0,0 +1,102 @@
import useFetchApi from "@utils/api";
import { cn } from "@utils/helpers";
import { Handle, type Node, Position } from "@xyflow/react";
import { NetworkIcon } from "lucide-react";
import * as React from "react";
import CircleIcon from "@/assets/icons/CircleIcon";
import { DeviceCard } from "@/modules/control-center/nodes/DeviceCard";
import { Network, NetworkResource } from "@/interfaces/Network";
type NetworkNodeType = {
network: Network;
};
type NetworkNodeProps = Node<NetworkNodeType, "networkNode">;
export const NetworkNode = ({ data }: NetworkNodeProps) => {
const { data: networkResources, isLoading: isLoadingResources } = useFetchApi<
NetworkResource[]
>("/networks/resources");
const n = data.network as Network;
const resourceIds = n?.resources || [];
const routingPeers = n?.routers || [];
const resources =
networkResources?.filter((r) => resourceIds.includes(r?.id || "")) || [];
return (
<div
className={cn(
"bg-nb-gray-940 border border-nb-gray-800 rounded-2xl overflow-hidden group hover:bg-nb-gray-935 transition-all cursor-pointer",
)}
>
<div
className={cn(
"flex w-full items-center justify-between text-nb-gray-300 gap-2 text-sm pl-6 pr-6 py-3.5 font-normal bg-nb-gray-935 border-b border-nb-gray-800 group-hover:bg-nb-gray-930 transition-all",
resources?.length === 0 && "border-b-0",
)}
>
<div className={"flex items-center gap-3 font-normal text-sm"}>
<div>
<div
className={
" text-nb-gray-100 font-medium whitespace-nowrap flex items-center gap-2"
}
>
<NetworkIcon size={12} />
{n?.name}
</div>
<div className={"text-nb-gray-400 whitespace-nowrap mt-0.5"}>
{resources?.length || 0} Resources
</div>
</div>
</div>
<div className={"flex items-center gap-2 text-xs"}>
<CircleIcon
size={8}
className={cn(
"shrink-0 block",
routingPeers?.length === 0 && "bg-nb-gray-500",
routingPeers?.length === 1 && "bg-yellow-400",
routingPeers?.length > 1 && "bg-green-400",
)}
/>
{routingPeers?.length || 0} Routing Peer(s)
</div>
</div>
{resources && resources.length > 0 && (
<div className={"p-2 flex flex-col gap-4 relative"}>
<div className={"grid grid-cols-2 relative z-0"}>
{resources?.slice(0, 6).map((r) => {
return <DeviceCard resource={r} key={r.id} />;
})}
</div>
<div
className={cn(
"absolute w-full h-full bg-gradient-to-b from-transparent via-nb-gray-940/20 to-nb-gray-940 z-10 left-0 top-0 pointer-events-none",
resources?.length > 6 ? "opacity-100" : "opacity-0",
)}
></div>
</div>
)}
<Handle
type="source"
position={Position.Right}
id={"sr"}
style={{
opacity: 0,
}}
/>
<Handle
type="target"
position={Position.Left}
id={"tl"}
style={{
opacity: 0,
}}
/>
</div>
);
};

View File

@@ -0,0 +1,44 @@
import { cn } from "@utils/helpers";
import { Handle, type Node, Position } from "@xyflow/react";
import * as React from "react";
import type { Peer } from "@/interfaces/Peer";
import { DeviceCard } from "@/modules/control-center/nodes/DeviceCard";
import { useAnySourceGroupEnabled } from "@/modules/control-center/utils/helpers";
type PeerNodeProps = Node<
{
peer: Peer;
enabled?: boolean;
},
"peerNode"
>;
export const PeerNode = ({ data, id }: PeerNodeProps) => {
const { peer, enabled } = data;
const isEnabled = useAnySourceGroupEnabled(id);
return (
<div
className={
"border-0 border-nb-gray-800 rounded-lg overflow-hidden transition-all"
}
>
<DeviceCard
device={peer}
className={cn("p-0", !isEnabled && "opacity-60")}
/>
<Handle
type="source"
position={Position.Right}
id={"sr"}
className={"opacity-0"}
/>
<Handle
type="target"
position={Position.Left}
id={"tl"}
className={"opacity-0"}
/>
</div>
);
};

View File

@@ -0,0 +1,66 @@
import { GroupBadgeIcon } from "@components/ui/GroupBadgeIcon";
import { cn } from "@utils/helpers";
import { Handle, type Node, Position } from "@xyflow/react";
import * as React from "react";
import { getPolicyProtocolAndPortText } from "@/modules/control-center/utils/helpers";
import { Policy } from "@/interfaces/Policy";
type PolicyNode = Node<
{
policy: Policy;
},
"policyNode"
>;
export const PolicyNode = ({ data }: PolicyNode) => {
const rule = data.policy.rules?.[0];
const label = getPolicyProtocolAndPortText(data.policy);
const isActive = rule?.enabled;
return (
<div
className={cn(
"relative bg-nb-gray-940 hover:bg-nb-gray-930 cursor-pointer border border-nb-gray-800 rounded-full flex justify-between overflow-hidden",
!isActive && "opacity-60",
)}
>
<div className={"flex items-center justify-center"}>
<div
className={cn(
"h-2 w-2 rounded-full ml-3 mr-2",
isActive ? "bg-green-400" : "bg-nb-gray-400",
)}
></div>
</div>
<div className={"pt-2.5 pb-[0.6rem] pr-3 flex gap-4 leading-none"}>
<div
className={
" text-nb-gray-200 font-normal whitespace-nowrap text-[0.8rem] flex items-center justify-center w-full"
}
>
<div className={"truncate max-w-[200px]"}>{rule?.name}</div>
</div>
</div>
<div
className={
"border-l border-nb-gray-800 flex items-center text-nb-gray-300 text-[0.65rem] pl-2 pr-3 font-mono"
}
>
<div>{label === "" ? "All" : label}</div>
</div>
<Handle
type="source"
position={Position.Right}
id={"sr"}
className={"opacity-0"}
/>
<Handle
type="target"
position={Position.Left}
id={"tl"}
className={"opacity-0"}
/>
</div>
);
};

View File

@@ -0,0 +1,41 @@
import { cn } from "@utils/helpers";
import { Handle, type Node, Position } from "@xyflow/react";
import * as React from "react";
import { NetworkResource } from "@/interfaces/Network";
import { DeviceCard } from "@/modules/control-center/nodes/DeviceCard";
import { useAnySourceGroupEnabled } from "@/modules/control-center/utils/helpers";
type ResourceNode = Node<
{
resource: NetworkResource;
enabled?: boolean;
},
"resourceNode"
>;
export const ResourceNode = ({ data, id }: ResourceNode) => {
const { enabled, resource } = data;
const isEnabled = useAnySourceGroupEnabled(id);
return (
<div
className={
"cursor-pointer border-0 border-nb-gray-800 rounded-lg overflow-hidden transition-all"
}
>
<DeviceCard
resource={resource}
className={cn("p-0", !isEnabled && "opacity-60")}
/>
<Handle
type="target"
position={Position.Left}
id={"tl"}
style={{
opacity: 0,
}}
/>
</div>
);
};

View File

@@ -0,0 +1,135 @@
import {
SelectDropdown,
SelectOption,
} from "@components/select/SelectDropdown";
import { GroupBadgeIcon } from "@components/ui/GroupBadgeIcon";
import useFetchApi from "@utils/api";
import { Handle, type Node, Position } from "@xyflow/react";
import { sortBy } from "lodash";
import { ChevronsUpDown } from "lucide-react";
import * as React from "react";
import { useMemo } from "react";
import { Group } from "@/interfaces/Group";
type NodeProps = Node<
{
currentGroup: string;
onChange: (id: string) => void;
},
"selectGroupNode"
>;
export const SelectGroupNode = ({ data, id }: NodeProps) => {
const { data: groups, isLoading: isGroupsLoading } =
useFetchApi<Group[]>("/groups");
const groupOptions: SelectOption[] = sortBy(
groups?.map(
(g) =>
({
value: g.id,
label: g.name,
icon: () => (
<GroupBadgeIcon id={g?.id} issued={g?.issued} size={14} />
),
}) as SelectOption,
) || [],
"label",
"asc",
);
const group = groups?.find((g) => g.id === data.currentGroup);
const countLabel = useMemo(() => {
const peerCount = group?.peers_count || 0;
const resourceCount = group?.resources_count || 0;
if (resourceCount === 0) {
return `${peerCount} Peer(s)`;
}
if (peerCount === 0) {
return `${resourceCount} Resource(s)`;
}
return `${peerCount} Peer(s), ${resourceCount} Resource(s)`;
}, [group]);
return (
<div
className={
"bg-nb-gray-930 border hover:bg-nb-gray-910 cursor-pointer border-nb-gray-800 rounded-lg overflow-hidden transition-all"
}
>
<SelectDropdown
variant={"secondary"}
value={data.currentGroup}
onChange={data.onChange}
options={groupOptions}
showSearch={true}
searchPlaceholder={"Search groups..."}
popoverWidth={280}
className={"!bg-nb-gray-920 !hover:bg-nb-gray-925 !text-nb-gray-300"}
size={"xs"}
maxHeight={300}
>
<div className={"flex items-center justify-between gap-8 pr-3"}>
{group && (
<div
className={
"flex w-full items-center justify-between text-nb-gray-300 gap-2 text-sm pl-3 pr-5 py-3 font-normal"
}
>
<div className={"flex items-center gap-3 font-normal text-sm"}>
<div
className={
"h-9 w-9 bg-nb-gray-850 rounded-md flex items-center justify-center shrink-0"
}
>
<GroupBadgeIcon
id={group?.id}
issued={group?.issued}
size={14}
/>
</div>
<div>
<div
className={
" text-nb-gray-200 font-normal whitespace-nowrap text-left"
}
>
{group.name}
</div>
<div
className={
"text-nb-gray-400 whitespace-nowrap text-xs text-left"
}
>
{countLabel}
</div>
</div>
</div>
</div>
)}
<ChevronsUpDown size={18} className={"shrink-0"} />
</div>
</SelectDropdown>
<Handle
type="source"
position={Position.Right}
id={"sr"}
style={{
height: 20,
width: "1px",
border: "none",
backgroundColor: "#3f444b",
borderRadius: "0px 4px 4px 0px",
right: -2,
}}
/>
<Handle
type="target"
position={Position.Left}
id={"tl"}
className={"opacity-0"}
/>
</div>
);
};

View File

@@ -0,0 +1,102 @@
import {
SelectDropdown,
SelectOption,
} from "@components/select/SelectDropdown";
import useFetchApi from "@utils/api";
import { cn } from "@utils/helpers";
import { Handle, type Node, Position } from "@xyflow/react";
import { sortBy } from "lodash";
import { ChevronsUpDown } from "lucide-react";
import * as React from "react";
import { OperatingSystem } from "@/interfaces/OperatingSystem";
import type { Peer } from "@/interfaces/Peer";
import { DeviceCard } from "@/modules/control-center/nodes/DeviceCard";
import { OSLogo } from "@/modules/peers/PeerOSCell";
type PeerNodeProps = Node<
{
currentPeer: string;
onPeerChange: (peerId: string) => void;
},
"selectPeerNode"
>;
export const SelectPeerNode = ({ data, id }: PeerNodeProps) => {
const { data: peers, isLoading: isPeersLoading } =
useFetchApi<Peer[]>("/peers");
const peerSelectOptions: SelectOption[] = sortBy(
peers?.map(
(p) =>
({
value: p.id,
label: p.name,
icon: () => {
const os = p.os as unknown as OperatingSystem;
return (
<div
className={cn(
"flex items-center justify-center grayscale brightness-[100%] contrast-[40%]",
"w-4 h-4 shrink-0",
os === OperatingSystem.WINDOWS && "p-[2.5px]",
os === OperatingSystem.APPLE && "p-[2.7px]",
os === OperatingSystem.FREEBSD && "p-[1.5px]",
)}
>
<OSLogo os={p.os} />
</div>
);
},
}) as SelectOption,
) || [],
"label",
"asc",
);
const peer = peers?.find((p) => p.id === data.currentPeer);
return (
<div
className={
"bg-nb-gray-930 border hover:bg-nb-gray-910 cursor-pointer border-nb-gray-800 rounded-lg overflow-hidden transition-all"
}
>
<SelectDropdown
variant={"secondary"}
value={data.currentPeer}
onChange={data.onPeerChange}
options={peerSelectOptions}
showSearch={true}
searchPlaceholder={"Search peers..."}
popoverWidth={280}
className={"!bg-nb-gray-920 !hover:bg-nb-gray-925 !text-nb-gray-300"}
size={"xs"}
maxHeight={300}
>
<div className={"flex items-center justify-between gap-8 pr-3"}>
{peer && <DeviceCard device={peer} />}
<ChevronsUpDown size={18} className={"shrink-0"} />
</div>
</SelectDropdown>
<Handle
type="source"
position={Position.Right}
id={"sr"}
style={{
height: 20,
width: "1px",
border: "none",
backgroundColor: "#3f444b",
borderRadius: "0px 4px 4px 0px",
right: -2,
}}
/>
<Handle
type="target"
position={Position.Left}
id={"tl"}
className={"opacity-0"}
/>
</div>
);
};

View File

@@ -0,0 +1,90 @@
import { InternalNode, Node, Position } from "@xyflow/react";
type IntersectionPoint = {
x: number;
y: number;
};
function getNodeIntersection(
intersectionNode: InternalNode<Node>,
targetNode: InternalNode<Node>,
) {
const { width: intersectionNodeWidth, height: intersectionNodeHeight } =
intersectionNode.measured;
const intersectionNodePosition = intersectionNode.internals.positionAbsolute;
const targetPosition = targetNode.internals.positionAbsolute;
const measuredTargetWidth = targetNode.measured.width || 0;
const measuredTargetHeight = targetNode.measured.height || 0;
const w = (intersectionNodeWidth || 0) / 2;
const h = (intersectionNodeHeight || 0) / 2;
const x2 = intersectionNodePosition.x + w;
const y2 = intersectionNodePosition.y + h;
const x1 = targetPosition.x + measuredTargetWidth / 2;
const y1 = targetPosition.y + measuredTargetHeight / 2;
const xx1 = (x1 - x2) / (2 * w) - (y1 - y2) / (2 * h);
const yy1 = (x1 - x2) / (2 * w) + (y1 - y2) / (2 * h);
const a = 1 / (Math.abs(xx1) + Math.abs(yy1));
const xx3 = a * xx1;
const yy3 = a * yy1;
const x = w * (xx3 + yy3) + x2;
const y = h * (-xx3 + yy3) + y2;
return { x, y };
}
function getEdgePosition(
node: InternalNode<Node>,
intersectionPoint: IntersectionPoint,
) {
const n = { ...node.internals.positionAbsolute, ...node };
const nx = Math.round(n.x);
const ny = Math.round(n.y);
const px = Math.round(intersectionPoint.x);
const py = Math.round(intersectionPoint.y);
const measuredWidth = n.measured.width || 0;
const measuredHeight = n.measured.height || 0;
if (px <= nx + 1) {
return Position.Left;
}
if (px >= nx + measuredWidth - 1) {
return Position.Right;
}
if (py <= ny + 1) {
return Position.Top;
}
if (py >= n.y + measuredHeight - 1) {
return Position.Bottom;
}
return Position.Top;
}
export function getEdgeParams(
source: InternalNode<Node>,
target: InternalNode<Node>,
) {
const sourceIntersectionPoint: IntersectionPoint = getNodeIntersection(
source,
target,
);
const targetIntersectionPoint: IntersectionPoint = getNodeIntersection(
target,
source,
);
const sourcePos = getEdgePosition(source, sourceIntersectionPoint);
const targetPos = getEdgePosition(target, targetIntersectionPoint);
return {
sx: sourceIntersectionPoint.x,
sy: sourceIntersectionPoint.y,
tx: targetIntersectionPoint.x,
ty: targetIntersectionPoint.y,
sourcePos,
targetPos,
};
}

View File

@@ -0,0 +1,13 @@
import AnimatedLine from "@/modules/control-center/edges/AnimatedLine";
import { BidirectionalEdges } from "@/modules/control-center/edges/BidirectionalEdges";
import { DirectionIn } from "@/modules/control-center/edges/DirectionIn";
import FloatingEdge from "@/modules/control-center/edges/FloatingEdge";
import { SimpleConnection } from "@/modules/control-center/edges/SimpleConnection";
export const EDGE_TYPES = {
in: DirectionIn,
bi: BidirectionalEdges,
floating: FloatingEdge,
"floating-straight": AnimatedLine,
simple: SimpleConnection,
};

View File

@@ -0,0 +1,145 @@
import { useReactFlow } from "@xyflow/react";
import { orderBy } from "lodash";
import { Group } from "@/interfaces/Group";
import { Network } from "@/interfaces/Network";
import { Peer } from "@/interfaces/Peer";
import { Policy } from "@/interfaces/Policy";
export const getDestinationGroupsFromPolicy = (policy: Policy) => {
const rule = policy.rules?.[0];
if (!rule) return [];
const destinations = rule.destinations as Group[];
if (!destinations) return [];
return destinations;
};
export const getSourceGroupsFromPolicy = (policy: Policy) => {
const rule = policy.rules?.[0];
if (!rule) return [];
const sources = rule.sources as Group[];
if (!sources) return [];
return sources;
};
export const getNetworksFromPolicy = (networks: Network[], policy: Policy) => {
const policyId = policy.id;
if (!policyId) return [];
return networks.filter((network) => {
return network.policies?.some((p) => p === policyId);
});
};
export const getPeersFromGroup = (group: Group, peers: Peer[]) => {
return peers.filter((peer) => {
const groupIds = peer.groups?.map((g) => g.id) || [];
return groupIds.includes(group.id);
});
};
export const getPolicyProtocolAndPortText = (
policy: Policy,
maxPorts?: number,
) => {
const rule = policy.rules?.[0];
if (!rule) return "";
let p = rule.protocol;
if (p === "all") {
return "";
} else if (p === "icmp") {
return "ICMP";
} else {
const ports = getPolicyPortsText(policy);
if (!ports || ports.length === 0) {
return p.toUpperCase();
}
if (ports.length > (maxPorts ?? 3)) {
const firstFour = ports.slice(0, 4);
return `${p.toUpperCase()}:${firstFour.join(",")}, ...`;
}
return `${p.toUpperCase()}:${ports.join(",")}`;
}
};
export const getPolicyPortsText = (policy: Policy) => {
const rule = policy.rules?.[0];
if (!rule) return undefined;
const ports = rule.ports || [];
const portRanges = rule.port_ranges || [];
if (ports.length === 0 && portRanges.length === 0) {
return undefined;
}
const portStrings = ports.map((port) => String(port));
const rangeStrings = portRanges.map((range) => {
if (range.start === range.end) return String(range.start);
return `${range.start}-${range.end}`;
});
return orderBy(
[...portStrings, ...rangeStrings],
[(x) => Number(x.split("-")[0])],
["asc"],
);
};
export const getResourcePolicyByGroups = (
groups: Group[],
policies: Policy[],
): Policy[] => {
const groupIds = groups.map((group) => group.id);
return policies.filter((policy) => {
const rule = policy.rules?.[0];
if (!rule) return false;
const destinations = rule.destinations as Group[];
return destinations?.some((d) => groupIds.includes(d.id));
});
};
export function useSourceGroupEnabled(sourceId: string) {
const { getNode } = useReactFlow();
const node = getNode(sourceId);
return node?.data?.enabled ?? false;
}
export function useAnySourceGroupEnabled(sourceId: string) {
const { getNodes, getEdges } = useReactFlow();
const nodes = getNodes();
const edges = getEdges();
const incomingEdges = edges.filter((e) => e.target === sourceId);
const sourceNodes = incomingEdges
.map((edge) => nodes.find((n) => n.id === edge.source))
.filter(Boolean);
const sourceEnabledStates = sourceNodes.map((n) => n?.data?.enabled);
return sourceEnabledStates.some(Boolean);
}
export function getFirstGroup(groups?: Group[], policies?: Policy[]) {
const sortedGroups = orderBy(groups, "peers_count", "desc");
const groupsWithoutAll = sortedGroups?.filter((g) => g.name !== "All");
const groupsWithPolicies = orderBy(
groupsWithoutAll?.filter((g) => {
return policies?.some((p) => {
const sources = getSourceGroupsFromPolicy(p);
return sources?.some((source) => source.id === g.id);
});
}),
"peers_count",
"desc",
);
if (groupsWithPolicies && groupsWithPolicies?.length > 0) {
return groupsWithPolicies[0];
}
if (groupsWithoutAll && groupsWithoutAll?.length > 0) {
return groupsWithoutAll[0];
}
return sortedGroups?.[0];
}

View File

@@ -0,0 +1,245 @@
import { Edge, Node } from "@xyflow/react";
import * as d3 from "d3";
interface SimulationNode extends Node {
x: number;
y: number;
vx?: number;
vy?: number;
}
export const DEFAULT_MAX_ZOOM = 0.8;
export const DEFAULT_MIN_ZOOM = 0.2;
export const applyD3ForceLayout = (nodes: Node[], edges: Edge[]) => {
const simulationNodes: SimulationNode[] = nodes.map((node) => ({
...node,
x: node.position?.x || 0,
y: node.position?.y || 0,
}));
const simulationLinks = edges.map((edge) => ({
...edge,
source: edge.source,
target: edge.target,
}));
// Apply minimal D3 simulation for final positioning with reduced link distance
const simulation = d3
.forceSimulation(simulationNodes)
.force(
"link",
d3
.forceLink(simulationLinks)
.id((d: any) => d.id)
.distance(60) // Reduced distance to minimize crossings
.strength(0.05), // Reduced strength to maintain radial structure
)
.force("collision", d3.forceCollide().radius(300));
// Run simulation for fewer iterations to preserve radial structure
for (let i = 0; i < 1000; i++) {
simulation.tick();
}
const updatedNodes: Node[] = simulationNodes.map((node) => ({
...node,
position: {
x: node.x,
y: node.y,
},
}));
const updatedEdges: Edge[] = edges.map((edge) => {
const sourceNode = simulationNodes.find((n) => n.id === edge.source);
const targetNode = simulationNodes.find((n) => n.id === edge.target);
return {
...edge,
data: {
...edge.data,
points:
sourceNode && targetNode
? [
{ x: sourceNode.x, y: sourceNode.y },
{ x: targetNode.x, y: targetNode.y },
]
: undefined,
},
};
});
simulation.stop();
return { updatedNodes, updatedEdges };
};
export const applyD3HierarchicalLayout = (
nodes: Node[],
edges: Edge[],
width = 280,
spacing = 100,
view?: string,
options?: {
policy?: { width: number; spacing: number };
destinationGroup?: { width: number; spacing: number };
peersAndResources?: { width: number; spacing: number };
},
) => {
const simulationNodes: SimulationNode[] = nodes.map((node) => ({
...node,
x: node.position?.x || 0,
y: node.position?.y || 0,
}));
const columnWidth = width;
const nodeSpacing = spacing;
const startX = 0;
const centerY = 0;
const groupNodes = simulationNodes.filter((n) => n.type === "groupNode");
const sourceGroupNodes = simulationNodes.filter(
(n) => n.type === "sourceGroupNode",
);
const destinationGroupNodes = simulationNodes.filter(
(n) => n.type === "destinationGroupNode",
);
const policyNodes = simulationNodes.filter((n) => n.type === "policyNode");
const networkNodes = simulationNodes.filter((n) => n.type === "networkNode");
const resourceNodes = simulationNodes.filter(
(n) => n.type === "resourceNode",
);
const peerNodes = simulationNodes.filter((n) => n.type === "peerNode");
const expandedGroupPeers = simulationNodes.filter(
(n) => n.type === "expandedGroupPeer",
);
let networkAndResourceNodes = [...networkNodes, ...resourceNodes];
if (view === "group") {
networkAndResourceNodes = [...networkAndResourceNodes, ...peerNodes];
}
if (view === "peer") {
networkAndResourceNodes = [
...networkAndResourceNodes,
...expandedGroupPeers,
];
}
// Peers
if (peerNodes.length > 0 && view !== "group") {
centerNodesVertically(
peerNodes,
startX + (view === "group" ? columnWidth * 4 : 0),
nodeSpacing,
centerY,
);
}
// Groups or Source Groups
centerNodesVertically(groupNodes, startX, nodeSpacing, centerY);
centerNodesVertically(
sourceGroupNodes,
startX + columnWidth,
nodeSpacing,
centerY,
);
// Policies
centerNodesVertically(
policyNodes,
startX + (options?.policy?.width ?? columnWidth),
options?.policy?.spacing ?? nodeSpacing,
centerY + 14,
);
// Destination Groups
centerNodesVertically(
destinationGroupNodes,
startX + (options?.destinationGroup?.width ?? columnWidth),
options?.destinationGroup?.spacing ?? nodeSpacing,
centerY,
);
// Networks
centerNodesVertically(
networkAndResourceNodes,
startX + (options?.peersAndResources?.width ?? columnWidth),
options?.peersAndResources?.spacing ?? nodeSpacing,
centerY + 5,
);
const simulation = d3
.forceSimulation(simulationNodes)
.force("charge", d3.forceManyBody().strength(0))
.force("collision", d3.forceCollide().radius(0))
.alphaDecay(0.05)
.velocityDecay(0.7);
simulation.force("position", (alpha) => {
simulationNodes.forEach((node) => {
let targetX = node.x;
let targetY = node.y;
const dx = targetX - node.x;
const dy = targetY - node.y;
node.vx = (node.vx || 0) + dx * alpha * 0.1;
node.vy = (node.vy || 0) + dy * alpha * 0.1;
});
});
for (let i = 0; i < 100; i++) {
simulation.tick();
}
const updatedNodes: Node[] = simulationNodes.map((node) => ({
...node,
position: {
x: node.x,
y: node.y,
},
}));
const updatedEdges: Edge[] = edges.map((edge) => {
const sourceNode = simulationNodes.find((n) => n.id === edge.source);
const targetNode = simulationNodes.find((n) => n.id === edge.target);
return {
...edge,
data: {
...edge.data,
points:
sourceNode && targetNode
? [
{ x: sourceNode.x, y: sourceNode.y },
{ x: targetNode.x, y: targetNode.y },
]
: undefined,
},
};
});
simulation.stop();
return { updatedNodes, updatedEdges };
};
const centerNodesVertically = (
nodesList: SimulationNode[],
x: number,
nodeSpacing: number,
centerY: number,
enable = true,
) => {
if (nodesList.length === 0) return;
const totalHeight = (nodesList.length - 1) * nodeSpacing;
const startY = centerY - totalHeight / 2;
nodesList.forEach((node, index) => {
node.x = x;
node.y = (enable ? startY : 0) + index * nodeSpacing;
});
};

View File

@@ -0,0 +1,20 @@
import { GroupNode } from "@/modules/control-center/nodes/GroupNode";
import { NetworkNode } from "@/modules/control-center/nodes/NetworkNode";
import { PeerNode } from "@/modules/control-center/nodes/PeerNode";
import { PolicyNode } from "@/modules/control-center/nodes/PolicyNode";
import { ResourceNode } from "@/modules/control-center/nodes/ResourceNode";
import { SelectGroupNode } from "@/modules/control-center/nodes/SelectGroupNode";
import { SelectPeerNode } from "@/modules/control-center/nodes/SelectPeerNode";
export const NODE_TYPES = {
groupNode: GroupNode,
sourceGroupNode: GroupNode,
destinationGroupNode: GroupNode,
networkNode: NetworkNode,
resourceNode: ResourceNode,
policyNode: PolicyNode,
peerNode: PeerNode,
expandedGroupPeer: PeerNode,
selectPeerNode: SelectPeerNode,
selectGroupNode: SelectGroupNode,
};

View File

@@ -38,7 +38,8 @@ export enum RDPStatus {
CONNECTING = 2,
}
export const RDP_DOCS_LINK = "https://docs.netbird.io/";
export const RDP_DOCS_LINK =
"https://docs.netbird.io/how-to/browser-client#rdp-connection";
export const useRemoteDesktop = (client: any) => {
const [status, setStatus] = useState(RDPStatus.DISCONNECTED);

View File

@@ -2,7 +2,6 @@ import FullTooltip from "@components/FullTooltip";
import InlineLink from "@components/InlineLink";
import { ExternalLinkIcon } from "lucide-react";
import * as React from "react";
import { SSH_DOCS_LINK } from "@/modules/remote-access/ssh/useSSH";
type Props = {
disabled?: boolean;
@@ -34,7 +33,10 @@ export const SSHTooltip = ({
</div>
<div>
Learn more about{" "}
<InlineLink href={SSH_DOCS_LINK} target={"_blank"}>
<InlineLink
href={"https://docs.netbird.io/how-to/ssh"}
target={"_blank"}
>
SSH <ExternalLinkIcon size={12} />
</InlineLink>
</div>

View File

@@ -20,7 +20,8 @@ export enum SSHStatus {
CONNECTING = 2,
}
export const SSH_DOCS_LINK = "https://docs.netbird.io/";
export const SSH_DOCS_LINK =
"https://docs.netbird.io/how-to/browser-client#ssh-connection";
export const useSSH = (client: any) => {
const [status, setStatus] = useState(SSHStatus.DISCONNECTED);

View File

@@ -28,6 +28,7 @@ const config: Config = {
"920": "#25282d",
"925": "#1e2123",
"930": "#25282c",
"935": "#1f2124",
"940": "#1c1d21",
"950": "#181a1d",
},