Network Routes page (#71)

Add Network routes page to interact with the routes API endpoint of our management
This commit is contained in:
Maycon Santos
2022-09-05 09:08:51 +02:00
committed by GitHub
parent c9f1955d6a
commit 7166eb6e2f
16 changed files with 1493 additions and 364 deletions

553
package-lock.json generated
View File

@@ -26,6 +26,7 @@
"antd": "^4.20.6",
"autoprefixer": "^10.4.4",
"axios": "^0.27.2",
"cidr-regex": "^3.1.1",
"copyfiles": "^2.4.1",
"heroicons": "^1.0.6",
"highlight.js": "^11.2.0",
@@ -2339,6 +2340,21 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@eslint/eslintrc/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@eslint/eslintrc/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -2369,6 +2385,11 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"node_modules/@eslint/eslintrc/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
@@ -4648,13 +4669,13 @@
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
@@ -4678,34 +4699,6 @@
}
}
},
"node_modules/ajv-formats/node_modules/ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"peerDependencies": {
"ajv": "^6.9.1"
}
},
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@@ -5108,6 +5101,34 @@
"webpack": ">=2"
}
},
"node_modules/babel-loader/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/babel-loader/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"peerDependencies": {
"ajv": "^6.9.1"
}
},
"node_modules/babel-loader/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"node_modules/babel-loader/node_modules/schema-utils": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz",
@@ -5721,6 +5742,17 @@
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz",
"integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg=="
},
"node_modules/cidr-regex": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/cidr-regex/-/cidr-regex-3.1.1.tgz",
"integrity": "sha512-RBqYd32aDwbCMFJRL6wHOlDNYJsPNTt8vC82ErHF5vKt8QQzxm1FrkW8s/R5pVrXMf17sba09Uoy91PKiddAsw==",
"dependencies": {
"ip-regex": "^4.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/cjs-module-lexer": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz",
@@ -6271,21 +6303,6 @@
}
}
},
"node_modules/css-minimizer-webpack-plugin/node_modules/ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/css-minimizer-webpack-plugin/node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
@@ -6297,11 +6314,6 @@
"ajv": "^8.8.2"
}
},
"node_modules/css-minimizer-webpack-plugin/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/css-minimizer-webpack-plugin/node_modules/schema-utils": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz",
@@ -7640,21 +7652,6 @@
"webpack": "^5.0.0"
}
},
"node_modules/eslint-webpack-plugin/node_modules/ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/eslint-webpack-plugin/node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
@@ -7679,11 +7676,6 @@
"node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
}
},
"node_modules/eslint-webpack-plugin/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/eslint-webpack-plugin/node_modules/schema-utils": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz",
@@ -7716,6 +7708,21 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/eslint/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/eslint/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -7761,6 +7768,11 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/eslint/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"node_modules/eslint/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
@@ -8320,6 +8332,29 @@
}
}
},
"node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"peerDependencies": {
"ajv": "^6.9.1"
}
},
"node_modules/fork-ts-checker-webpack-plugin/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -8364,6 +8399,11 @@
"node": ">=10"
}
},
"node_modules/fork-ts-checker-webpack-plugin/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
@@ -9198,6 +9238,14 @@
"node": ">= 0.4"
}
},
"node_modules/ip-regex": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz",
"integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==",
"engines": {
"node": ">=8"
}
},
"node_modules/ipaddr.js": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz",
@@ -11653,9 +11701,9 @@
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
@@ -12088,21 +12136,6 @@
"webpack": "^5.0.0"
}
},
"node_modules/mini-css-extract-plugin/node_modules/ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
@@ -12114,11 +12147,6 @@
"ajv": "^8.8.2"
}
},
"node_modules/mini-css-extract-plugin/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/mini-css-extract-plugin/node_modules/schema-utils": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz",
@@ -15713,6 +15741,34 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/schema-utils/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/schema-utils/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"peerDependencies": {
"ajv": "^6.9.1"
}
},
"node_modules/schema-utils/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"node_modules/scroll-into-view-if-needed": {
"version": "2.2.29",
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.29.tgz",
@@ -17374,21 +17430,6 @@
"webpack": "^4.0.0 || ^5.0.0"
}
},
"node_modules/webpack-dev-middleware/node_modules/ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/webpack-dev-middleware/node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
@@ -17400,11 +17441,6 @@
"ajv": "^8.8.2"
}
},
"node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/webpack-dev-middleware/node_modules/schema-utils": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz",
@@ -17477,21 +17513,6 @@
}
}
},
"node_modules/webpack-dev-server/node_modules/ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/webpack-dev-server/node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
@@ -17503,11 +17524,6 @@
"ajv": "^8.8.2"
}
},
"node_modules/webpack-dev-server/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/webpack-dev-server/node_modules/schema-utils": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz",
@@ -17770,21 +17786,6 @@
"node": ">=10.0.0"
}
},
"node_modules/workbox-build/node_modules/ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/workbox-build/node_modules/fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
@@ -17799,11 +17800,6 @@
"node": ">=10"
}
},
"node_modules/workbox-build/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/workbox-build/node_modules/source-map": {
"version": "0.8.0-beta.0",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz",
@@ -19615,6 +19611,17 @@
"strip-json-comments": "^3.1.1"
},
"dependencies": {
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -19636,6 +19643,11 @@
"argparse": "^2.0.1"
}
},
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
@@ -21403,13 +21415,13 @@
}
},
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
}
},
@@ -21419,31 +21431,8 @@
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"requires": {
"ajv": "^8.0.0"
},
"dependencies": {
"ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"requires": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
}
},
"json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
}
}
},
"ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="
},
"ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@@ -21733,6 +21722,27 @@
"schema-utils": "^2.6.5"
},
"dependencies": {
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="
},
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"schema-utils": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz",
@@ -22186,6 +22196,14 @@
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz",
"integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg=="
},
"cidr-regex": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/cidr-regex/-/cidr-regex-3.1.1.tgz",
"integrity": "sha512-RBqYd32aDwbCMFJRL6wHOlDNYJsPNTt8vC82ErHF5vKt8QQzxm1FrkW8s/R5pVrXMf17sba09Uoy91PKiddAsw==",
"requires": {
"ip-regex": "^4.1.0"
}
},
"cjs-module-lexer": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz",
@@ -22580,17 +22598,6 @@
"source-map": "^0.6.1"
},
"dependencies": {
"ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"requires": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
}
},
"ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
@@ -22599,11 +22606,6 @@
"fast-deep-equal": "^3.1.3"
}
},
"json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"schema-utils": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz",
@@ -23292,6 +23294,17 @@
"v8-compile-cache": "^2.0.3"
},
"dependencies": {
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -23322,6 +23335,11 @@
"argparse": "^2.0.1"
}
},
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
@@ -23627,17 +23645,6 @@
"schema-utils": "^4.0.0"
},
"dependencies": {
"ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"requires": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
}
},
"ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
@@ -23656,11 +23663,6 @@
"supports-color": "^8.0.0"
}
},
"json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"schema-utils": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz",
@@ -24086,6 +24088,22 @@
"tapable": "^1.0.0"
},
"dependencies": {
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -24118,6 +24136,11 @@
"universalify": "^2.0.0"
}
},
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"schema-utils": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
@@ -24716,6 +24739,11 @@
"side-channel": "^1.0.4"
}
},
"ip-regex": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz",
"integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q=="
},
"ipaddr.js": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz",
@@ -26579,9 +26607,9 @@
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
},
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"json-stable-stringify-without-jsonify": {
"version": "1.0.1",
@@ -26902,17 +26930,6 @@
"schema-utils": "^4.0.0"
},
"dependencies": {
"ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"requires": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
}
},
"ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
@@ -26921,11 +26938,6 @@
"fast-deep-equal": "^3.1.3"
}
},
"json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"schema-utils": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz",
@@ -29304,6 +29316,29 @@
"@types/json-schema": "^7.0.8",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
},
"dependencies": {
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="
},
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
}
}
},
"scroll-into-view-if-needed": {
@@ -30592,17 +30627,6 @@
"schema-utils": "^4.0.0"
},
"dependencies": {
"ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"requires": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
}
},
"ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
@@ -30611,11 +30635,6 @@
"fast-deep-equal": "^3.1.3"
}
},
"json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"schema-utils": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz",
@@ -30665,17 +30684,6 @@
"ws": "^8.4.2"
},
"dependencies": {
"ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"requires": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
}
},
"ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
@@ -30684,11 +30692,6 @@
"fast-deep-equal": "^3.1.3"
}
},
"json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"schema-utils": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz",
@@ -30871,17 +30874,6 @@
"workbox-window": "6.5.4"
},
"dependencies": {
"ajv": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz",
"integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==",
"requires": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
}
},
"fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
@@ -30893,11 +30885,6 @@
"universalify": "^2.0.0"
}
},
"json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"source-map": {
"version": "0.8.0-beta.0",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz",

View File

@@ -21,6 +21,7 @@
"antd": "^4.20.6",
"autoprefixer": "^10.4.4",
"axios": "^0.27.2",
"cidr-regex": "^3.1.1",
"copyfiles": "^2.4.1",
"heroicons": "^1.0.6",
"highlight.js": "^11.2.0",

View File

@@ -8,6 +8,7 @@ import SetupKeys from "./views/SetupKeys";
import AddPeer from "./views/AddPeer";
import Users from './views/Users';
import AccessControl from './views/AccessControl';
import Routes from './views/Routes';
import Banner from "./components/Banner";
import {store} from "./store";
import { Col, Layout, Row} from 'antd';
@@ -68,6 +69,7 @@ function App() {
<Route path="/add-peer" component={withOidcSecure(AddPeer)}/>
<Route path="/setup-keys" component={withOidcSecure(SetupKeys)}/>
<Route path="/acls" component={withOidcSecure(AccessControl)}/>
<Route path="/routes" component={withOidcSecure(Routes)}/>
<Route path="/users" component={withOidcSecure(Users)}/>
</Switch>
</Content>

View File

@@ -34,6 +34,7 @@ const Navbar = () => {
{ label: (<Link to="/add-peer">Add Peer</Link>), key: '/add-peer' },
{ label: (<Link to="/setup-keys">Setup Keys</Link>), key: '/setup-keys' },
{ label: (<Link to="/acls">Access Control</Link>), key: '/acls' },
{ label: (<Link to="/routes">Network Routes</Link>), key: '/routes' },
{ label: (<Link to="/users">Users</Link>), key: '/users' }
] as ItemType[])
const logoutWithRedirect = () =>

View File

@@ -0,0 +1,322 @@
import React, {useEffect, useRef, useState} from 'react';
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "typesafe-actions";
import { actions as routeActions } from '../store/route';
import {
Col,
Row,
Input,
InputNumber,
Space,
Switch,
SelectProps,
Button, Drawer, Form, Divider, Select, Tag, Radio, RadioChangeEvent, Typography
} from "antd";
import {CloseOutlined, FlagFilled, QuestionCircleFilled} from "@ant-design/icons";
import {Route} from "../store/route/types";
import {Header} from "antd/es/layout/layout";
import {RuleObject} from "antd/lib/form";
import {useOidcAccessToken} from "@axa-fr/react-oidc";
import cidrRegex from 'cidr-regex';
const { Paragraph } = Typography;
interface FormRoute extends Route {
}
const RouteUpdate = () => {
const {accessToken} = useOidcAccessToken()
const dispatch = useDispatch()
const setupNewRouteVisible = useSelector((state: RootState) => state.route.setupNewRouteVisible)
const peers = useSelector((state: RootState) => state.peer.data)
const route = useSelector((state: RootState) => state.route.route)
const savedRoute = useSelector((state: RootState) => state.route.savedRoute)
const [editName, setEditName] = useState(false)
const [editDescription, setEditDescription] = useState(false)
const options: SelectProps['options'] = [];
const [formRoute, setFormRoute] = useState({} as FormRoute)
const [form] = Form.useForm()
const inputNameRef = useRef<any>(null)
const inputDescriptionRef = useRef<any>(null)
const peerSeparator = " - "
const optionsDisabledEnabled = [{label: 'Enabled', value: true}, {label: 'Disabled', value: false}]
useEffect(() => {
if (editName) inputNameRef.current!.focus({
cursor: 'end',
});
}, [editName]);
useEffect(() => {
if (editDescription) inputDescriptionRef.current!.focus({
cursor: 'end',
});
}, [editDescription]);
useEffect(() => {
if (!route) return
let peerName = ''
let p = peers.find(_p => _p.ip === route.peer)
peerName = p ? p.name + peerSeparator + p.ip : route.peer
const fRoute = {
...route,
peer: peerName
} as FormRoute
setFormRoute(fRoute)
form.setFieldsValue(fRoute)
}, [route])
peers.forEach((p) => {
let os:string
os = p.os
if (!os.toLowerCase().startsWith("darwin") && !os.toLowerCase().startsWith("windows")) {
options?.push({
label: p.name + peerSeparator + p.ip,
value: p.name + peerSeparator + p.ip,
disabled: false
})
}
})
const createRouteToSave = ():Route => {
let peerID = ''
if (formRoute.peer != '') {
peerID = formRoute.peer.split(peerSeparator)[1]
}
console.log(formRoute)
// let p = peers.find(_p => _p.name === formRoute.peer)
// peerID = p ? p.ip : ''
return {
id: formRoute.id,
network: formRoute.network,
network_id: formRoute.network_id,
description: formRoute.description,
peer: peerID,
enabled: formRoute.enabled,
masquerade: formRoute.masquerade,
metric: formRoute.metric
} as Route
}
const handleFormSubmit = () => {
form.validateFields()
.then((values) => {
const routeToSave = createRouteToSave()
dispatch(routeActions.saveRoute.request({getAccessTokenSilently:accessToken, payload: routeToSave}))
})
.catch((errorInfo) => {
console.log('errorInfo', errorInfo)
});
};
const setVisibleNewRoute = (status:boolean) => {
dispatch(routeActions.setSetupNewRouteVisible(status));
}
const onCancel = () => {
if (savedRoute.loading) return
setEditName(false)
dispatch(routeActions.setRoute({
network: '',
network_id: '',
description: '',
peer: "",
metric: 9999,
masquerade: false,
enabled: true
} as Route))
setVisibleNewRoute(false)
}
const onChange = (data:any) => {
setFormRoute({...formRoute, ...data})
}
const dropDownRender = (menu: React.ReactElement) => (
<>
{menu}
</>
)
const toggleEditName = (status:boolean) => {
setEditName(status);
}
const toggleEditDescription = (status:boolean) => {
setEditDescription(status);
}
const networkRangeValidator = (_: RuleObject, value: string) => {
if (!cidrRegex().test(value)) {
return Promise.reject(new Error("Please enter a valid CIDR, e.g. 192.168.1.0/24"))
}
if (Number(value.split("/")[1]) < 7) {
return Promise.reject(new Error("Please enter a network mask larger than /7"))
}
return Promise.resolve()
}
return (
<>
{route &&
<Drawer
headerStyle={{display: "none"}}
forceRender={true}
visible={setupNewRouteVisible}
bodyStyle={{paddingBottom: 80}}
onClose={onCancel}
autoFocus={true}
footer={
<Space style={{display: 'flex', justifyContent: 'end'}}>
<Button onClick={onCancel} disabled={savedRoute.loading}>Cancel</Button>
<Button type="primary" disabled={savedRoute.loading} onClick={handleFormSubmit}>{`${formRoute.id ? 'Save' : 'Create'}`}</Button>
</Space>
}
>
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}>
<Row gutter={16}>
<Col span={24}>
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
<Row align="top">
<Col flex="none" style={{display: "flex"}}>
{!editName && !editDescription && formRoute.id &&
<button type="button" aria-label="Close" className="ant-drawer-close"
style={{paddingTop: 3}}
onClick={onCancel}>
<span role="img" aria-label="close" className="anticon anticon-close">
<CloseOutlined size={16}/>
</span>
</button>
}
</Col>
<Col flex="auto">
{ !editName && formRoute.id ? (
<div className={"access-control input-text ant-drawer-title"} onClick={() => toggleEditName(true)}>{formRoute.id ? formRoute.network_id : 'New Route'}</div>
) : (
<Form.Item
name="network_id"
label="Network Identifier"
tooltip="You can enable high-availability by assigning the same network identifier and network CIDR to multiple routes"
rules={[{required: true, message: 'Please add an identifier for this access route', whitespace: true}]}
>
<Input placeholder="e.g. aws-eu-central-1-vpc" ref={inputNameRef} onPressEnter={() => toggleEditName(false)} onBlur={() => toggleEditName(false)} autoComplete="off" maxLength={40}/>
</Form.Item>
)}
{ !editDescription ? (
<div className={"access-control input-text ant-drawer-subtitle"} onClick={() => toggleEditDescription(true)}>{formRoute.description && formRoute.description.trim() !== "" ? formRoute.description : 'Add description...'}</div>
) : (
<Form.Item
name="description"
label="Description"
style={{marginTop: 24}}
>
<Input placeholder="Add description..." ref={inputDescriptionRef} onPressEnter={() => toggleEditDescription(false)} onBlur={() => toggleEditDescription(false)} autoComplete="off" maxLength={200}/>
</Form.Item>
)}
</Col>
</Row>
<Row align="top">
<Col flex="auto">
</Col>
</Row>
</Header>
</Col>
<Col span={24}>
</Col>
<Col span={24}>
<Form.Item
name="network"
label="Network CIDR"
tooltip="Use CIDR notation. e.g. 192.168.10.0/24 or 172.16.0.0/16"
rules={[{validator: networkRangeValidator}]}
>
<Input placeholder="e.g. 172.16.0.0/16" autoComplete="off" minLength={9} maxLength={43}/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="enabled"
label="Status"
>
<Radio.Group
options={optionsDisabledEnabled}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="peer"
label="Routing peer"
tooltip="Assign a peer as a routing peer for the Network CIDR"
>
<Select
showSearch
style={{ width: '100%' }}
placeholder="Select Peer"
// onChange={handlePeerChange}
dropdownRender={dropDownRender}
options={options}
allowClear={true}
/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="masquerade"
label="Masquerade"
tooltip="Enabling this option hides other NetBird network IPs behind the routing peer local address when accessing the target Network CIDR. This option allows access to your private networks without configuring routes on your local routers or other devices."
>
<Switch size={"small"} checked={formRoute.masquerade}/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="metric"
label="Metric"
tooltip="Choose from 1 to 9999. Lower number has higher priority"
>
<InputNumber min={1} max={9999} autoComplete="off"/>
</Form.Item>
</Col>
<Col span={24}>
<Row wrap={false} gutter={12}>
<Col flex="none">
<FlagFilled/>
</Col>
<Col flex="auto">
<Paragraph>
You can enable high-availability by assigning the same network identifier and network CIDR to multiple routes.
</Paragraph>
</Col>
</Row>
</Col>
<Col span={24}>
<Divider></Divider>
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
href="https://docs.netbird.io/docs/overview/routes" style={{color: 'rgb(07, 114, 128)'}}>Learn
more about network routes</Button>
</Col>
</Row>
</Form>
</Drawer>
}
</>
)
}
export default RouteUpdate

View File

@@ -7,6 +7,7 @@ import { sagas as setupKeySagas } from './setup-key';
import { sagas as userSagas } from './user';
import { sagas as ruleSagas } from './rule';
import { sagas as groupSagas } from './group';
import { sagas as routeSagas } from './route';
import rootReducer from './root-reducer';
import { apiClient } from '../services/api-client';
@@ -23,5 +24,6 @@ sagaMiddleware.run(setupKeySagas);
sagaMiddleware.run(userSagas);
sagaMiddleware.run(ruleSagas);
sagaMiddleware.run(groupSagas);
sagaMiddleware.run(routeSagas);
export { apiClient, rootReducer, store };

View File

@@ -3,11 +3,13 @@ import { actions as SetupKeyActions } from './setup-key';
import { actions as UserActions } from './user';
import { actions as GroupActions } from './group';
import { actions as RuleActions } from './rule';
import { actions as RouteActions } from './route';
export default {
peer: PeerActions,
setupKey: SetupKeyActions,
user: UserActions,
group: GroupActions,
rule: RuleActions
rule: RuleActions,
route: RouteActions
};

View File

@@ -5,11 +5,13 @@ import { reducer as setupKey } from './setup-key';
import { reducer as user } from './user';
import { reducer as group } from './group';
import { reducer as rule } from './rule';
import { reducer as route } from './route';
export default combineReducers({
peer,
setupKey,
user,
group,
rule
rule,
route
});

View File

@@ -0,0 +1,34 @@
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
import {Route} from './types';
import {ApiError, CreateResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
const actions = {
getRoutes: createAsyncAction(
'GET_ROUTES_REQUEST',
'GET_ROUTES_SUCCESS',
'GET_ROUTES_FAILURE',
)<RequestPayload<null>, Route[], ApiError>(),
saveRoute: createAsyncAction(
'SAVE_ROUTE_REQUEST',
'SAVE_ROUTE_SUCCESS',
'SAVE_ROUTE_FAILURE',
)<RequestPayload<Route>, CreateResponse<Route | null>, CreateResponse<Route | null>>(),
setSavedRoute: createAction('SET_CREATE_ROUTE')<CreateResponse<Route | null>>(),
resetSavedRoute: createAction('RESET_CREATE_ROUTE')<null>(),
deleteRoute: createAsyncAction(
'DELETE_ROUTE_REQUEST',
'DELETE_ROUTE_SUCCESS',
'DELETE_ROUTE_FAILURE'
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
setDeletedRoute: createAction('SET_DELETED_ROUTE')<DeleteResponse<string | null>>(),
resetDeletedRoute: createAction('RESET_DELETED_ROUTE')<null>(),
removeRoute: createAction('REMOVE_ROUTE')<string>(),
setRoute: createAction('SET_ROUTE')<Route>(),
setSetupNewRouteVisible: createAction('SET_SETUP_NEW_ROUTE_VISIBLE')<boolean>()
};
export type ActionTypes = ActionType<typeof actions>;
export default actions;

7
src/store/route/index.ts Normal file
View File

@@ -0,0 +1,7 @@
import actions, { ActionTypes as _actionTypes } from './actions';
import reducer from './reducer';
import sagas from './sagas';
export type ActionTypes = _actionTypes;
export { actions, reducer, sagas };

View File

@@ -0,0 +1,89 @@
import { createReducer } from 'typesafe-actions';
import { combineReducers } from 'redux';
import { Route } from './types';
import actions, { ActionTypes } from './actions';
import {ApiError, DeleteResponse, CreateResponse} from "../../services/api-client/types";
type StateType = Readonly<{
data: Route[] | null;
route: Route | null;
loading: boolean;
failed: ApiError | null;
saving: boolean;
deleteRoute: DeleteResponse<string | null>;
savedRoute: CreateResponse<Route | null>;
setupNewRouteVisible: boolean
}>;
const initialState: StateType = {
data: [],
route: null,
loading: false,
failed: null,
saving: false,
deleteRoute: <DeleteResponse<string | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
},
savedRoute: <CreateResponse<Route | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
},
setupNewRouteVisible: false
};
const data = createReducer<Route[], ActionTypes>(initialState.data as Route[])
.handleAction(actions.getRoutes.success,(_, action) => action.payload)
.handleAction(actions.getRoutes.failure, () => []);
const route = createReducer<Route, ActionTypes>(initialState.route as Route)
.handleAction(actions.setRoute, (store, action) => action.payload);
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
.handleAction(actions.getRoutes.request, () => true)
.handleAction(actions.getRoutes.success, () => false)
.handleAction(actions.getRoutes.failure, () => false);
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
.handleAction(actions.getRoutes.request, () => null)
.handleAction(actions.getRoutes.success, () => null)
.handleAction(actions.getRoutes.failure, (store, action) => action.payload);
const saving = createReducer<boolean, ActionTypes>(initialState.saving)
.handleAction(actions.getRoutes.request, () => true)
.handleAction(actions.getRoutes.success, () => false)
.handleAction(actions.getRoutes.failure, () => false);
const deletedRoute = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deleteRoute)
.handleAction(actions.deleteRoute.request, () => initialState.deleteRoute)
.handleAction(actions.deleteRoute.success, (store, action) => action.payload)
.handleAction(actions.deleteRoute.failure, (store, action) => action.payload)
.handleAction(actions.setDeletedRoute, (store, action) => action.payload)
.handleAction(actions.resetDeletedRoute, () => initialState.deleteRoute)
const savedRoute = createReducer<CreateResponse<Route | null>, ActionTypes>(initialState.savedRoute)
.handleAction(actions.saveRoute.request, () => initialState.savedRoute)
.handleAction(actions.saveRoute.success, (store, action) => action.payload)
.handleAction(actions.saveRoute.failure, (store, action) => action.payload)
.handleAction(actions.setSavedRoute, (store, action) => action.payload)
.handleAction(actions.resetSavedRoute, () => initialState.savedRoute)
const setupNewRouteVisible = createReducer<boolean, ActionTypes>(initialState.setupNewRouteVisible)
.handleAction(actions.setSetupNewRouteVisible, (store, action) => action.payload)
export default combineReducers({
data,
route,
loading,
failed,
saving,
deletedRoute,
savedRoute,
setupNewRouteVisible
});

139
src/store/route/sagas.ts Normal file
View File

@@ -0,0 +1,139 @@
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
import {ApiError, ApiResponse, CreateResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
import {Route} from './types'
import service from './service';
import actions from './actions';
import { actions as groupActions } from '../group';
import {Group} from "../group/types";
export function* getRoutes(action: ReturnType<typeof actions.getRoutes.request>): Generator {
try {
yield put(actions.setDeletedRoute({
loading: false,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>))
const effect = yield call(service.getRoutes, action.payload);
const response = effect as ApiResponse<Route[]>;
yield put(actions.getRoutes.success(response.body));
} catch (err) {
yield put(actions.getRoutes.failure(err as ApiError));
}
}
export function* setCreatedRoute(action: ReturnType<typeof actions.setSavedRoute>): Generator {
yield put(actions.setSavedRoute(action.payload))
}
function getNewGroupIds(dataString:string[], responses:Group[]):string[] {
return responses.filter(r => dataString.includes(r.name)).map(r => r.id || '')
}
export function* saveRoute(action: ReturnType<typeof actions.saveRoute.request>): Generator {
try {
yield put(actions.setSavedRoute({
loading: true,
success: false,
failure: false,
error: null,
data: null
} as CreateResponse<Route | null>))
const routeToSave = action.payload.payload
const payloadToSave = {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: {
id: routeToSave.id,
description: routeToSave.description,
enabled: routeToSave.enabled,
masquerade: routeToSave.masquerade,
metric: routeToSave.metric,
network: routeToSave.network,
network_id: routeToSave.network_id,
peer: routeToSave.peer
} as Route
}
let effect
if (!routeToSave.id) {
effect = yield call(service.createRoute, payloadToSave);
} else {
payloadToSave.payload.id = routeToSave.id
effect = yield call(service.editRoute, payloadToSave);
}
const response = effect as ApiResponse<Route>;
yield put(actions.saveRoute.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as CreateResponse<Route | null>));
yield put(groupActions.getGroups.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
yield put(actions.getRoutes.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
} catch (err) {
yield put(actions.saveRoute.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null
} as CreateResponse<Route | null>));
}
}
export function* setDeleteRoute(action: ReturnType<typeof actions.setDeletedRoute>): Generator {
yield put(actions.setDeletedRoute(action.payload))
}
export function* deleteRoute(action: ReturnType<typeof actions.deleteRoute.request>): Generator {
try {
yield call(actions.setDeletedRoute,{
loading: true,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>)
const effect = yield call(service.deletedRoute, action.payload);
const response = effect as ApiResponse<any>;
yield put(actions.deleteRoute.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as DeleteResponse<string | null>));
const routes = (yield select(state => state.route.data)) as Route[]
yield put(actions.getRoutes.success(routes.filter((p:Route) => p.id !== action.payload.payload)))
} catch (err) {
yield put(actions.deleteRoute.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null
} as DeleteResponse<string | null>));
}
}
export default function* sagas(): Generator {
yield all([
takeLatest(actions.getRoutes.request, getRoutes),
takeLatest(actions.saveRoute.request, saveRoute),
takeLatest(actions.deleteRoute.request, deleteRoute)
]);
}

View File

@@ -0,0 +1,32 @@
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
import { apiClient } from '../../services/api-client';
import { Route } from './types';
export default {
async getRoutes(payload:RequestPayload<null>): Promise<ApiResponse<Route[]>> {
return apiClient.get<Route[]>(
`/api/routes`,
payload
);
},
async deletedRoute(payload:RequestPayload<string>): Promise<ApiResponse<any>> {
return apiClient.delete<any>(
`/api/routes/` + payload.payload,
payload
);
},
async createRoute(payload:RequestPayload<Route>): Promise<ApiResponse<Route>> {
return apiClient.post<Route>(
`/api/routes`,
payload
);
},
async editRoute(payload:RequestPayload<Route>): Promise<ApiResponse<Route>> {
const id = payload.payload.id
delete payload.payload.id
return apiClient.put<Route>(
`/api/routes/${id}`,
payload
);
},
};

11
src/store/route/types.ts Normal file
View File

@@ -0,0 +1,11 @@
export interface Route {
id?: string
description: string
enabled: boolean
peer: string
network: string
network_id: string
network_type?: string
metric?: number
masquerade: boolean
}

View File

@@ -4,8 +4,9 @@ import {useDispatch, useSelector} from "react-redux";
import {RootState} from "typesafe-actions";
import {actions as peerActions} from '../store/peer';
import {actions as groupActions} from '../store/group';
import {actions as routeActions} from '../store/route';
import {Container} from "../components/Container";
import { useOidcAccessToken } from '@axa-fr/react-oidc';
import {useOidcAccessToken} from '@axa-fr/react-oidc';
import {
Alert,
Button,
@@ -13,6 +14,7 @@ import {
Col,
Dropdown,
Input,
List,
Menu,
message,
Modal,
@@ -25,22 +27,22 @@ import {
Switch,
Table,
Tag,
Typography,
Tooltip
Tooltip,
Typography
} from "antd";
import {Peer} from "../store/peer/types";
import {filter} from "lodash"
import {formatOS, timeAgo} from "../utils/common";
import Icon, {ExclamationCircleOutlined, QuestionCircleOutlined, WarningOutlined} from "@ant-design/icons";
import {ExclamationCircleOutlined} from "@ant-design/icons";
import ButtonCopyMessage from "../components/ButtonCopyMessage";
import {Group, GroupPeer} from "../store/group/types";
import PeerUpdate from "../components/PeerUpdate";
import tableSpin from "../components/Spin";
import {TooltipPlacement} from "antd/es/tooltip";
const { Title, Paragraph } = Typography;
const { Column } = Table;
const { confirm } = Modal;
const {Title, Paragraph, Text} = Typography;
const {Column} = Table;
const {confirm} = Modal;
interface PeerDataTable extends Peer {
key: string;
@@ -54,6 +56,7 @@ export const Peers = () => {
const dispatch = useDispatch()
const peers = useSelector((state: RootState) => state.peer.data);
const routes = useSelector((state: RootState) => state.route.data);
const failed = useSelector((state: RootState) => state.peer.failed);
const loading = useSelector((state: RootState) => state.peer.loading);
const deletedPeer = useSelector((state: RootState) => state.peer.deletedPeer);
@@ -74,7 +77,7 @@ export const Peers = () => {
{label: "15", value: "15"}
]
const optionsOnOff = [{label: 'Online', value: 'on'},{label: 'All', value: 'all'}]
const optionsOnOff = [{label: 'Online', value: 'on'}, {label: 'All', value: 'all'}]
const itemsMenuAction = [
{
@@ -86,13 +89,13 @@ export const Peers = () => {
label: (<Button type="text" onClick={() => showConfirmDelete()}>Delete</Button>)
}
]
const actionsMenu = (<Menu items={itemsMenuAction} ></Menu>)
const actionsMenu = (<Menu items={itemsMenuAction}></Menu>)
const transformDataTable = (d:Peer[]):PeerDataTable[] => {
const transformDataTable = (d: Peer[]): PeerDataTable[] => {
const peer_ids = d.map(_p => _p.id)
return d.map((p) => {
const gs = groups
.filter(g => g.peers?.find((_p:GroupPeer) => _p.id === p.id))
.filter(g => g.peers?.find((_p: GroupPeer) => _p.id === p.id))
.map(g => ({id: g.id, name: g.name, peers_count: g.peers?.length, peers: g.peers || []}))
return {
key: p.id,
@@ -104,8 +107,9 @@ export const Peers = () => {
}
useEffect(() => {
dispatch(peerActions.getPeers.request({getAccessTokenSilently:accessToken, payload: null}));
dispatch(groupActions.getGroups.request({getAccessTokenSilently:accessToken, payload: null}));
dispatch(peerActions.getPeers.request({getAccessTokenSilently: accessToken, payload: null}));
dispatch(groupActions.getGroups.request({getAccessTokenSilently: accessToken, payload: null}));
dispatch(routeActions.getRoutes.request({getAccessTokenSilently: accessToken, payload: null}));
}, [])
useEffect(() => {
@@ -118,56 +122,76 @@ export const Peers = () => {
const deleteKey = 'deleting';
useEffect(() => {
const style = { marginTop: 85 }
const style = {marginTop: 85}
if (deletedPeer.loading) {
message.loading({ content: 'Deleting...', key: deleteKey, style });
message.loading({content: 'Deleting...', key: deleteKey, style});
} else if (deletedPeer.success) {
message.success({ content: 'Peer has been successfully removed.', key: deleteKey, duration: 2, style });
message.success({content: 'Peer has been successfully removed.', key: deleteKey, duration: 2, style});
dispatch(peerActions.resetDeletedPeer(null))
} else if (deletedPeer.error) {
message.error({ content: 'Failed to delete peer. You might not have enough permissions.', key: deleteKey, duration: 2, style });
message.error({
content: 'Failed to delete peer. You might not have enough permissions.',
key: deleteKey,
duration: 2,
style
});
dispatch(peerActions.resetDeletedPeer(null))
}
}, [deletedPeer])
const saveGroupsKey = 'saving_groups';
useEffect(() => {
const style = { marginTop: 85 }
const style = {marginTop: 85}
if (savedGroups.loading) {
message.loading({ content: 'Updating peer groups...', key: saveGroupsKey, style });
message.loading({content: 'Updating peer groups...', key: saveGroupsKey, style});
} else if (savedGroups.success) {
message.success({ content: 'Peer groups have been successfully updated.', key: saveGroupsKey, duration: 2, style });
message.success({
content: 'Peer groups have been successfully updated.',
key: saveGroupsKey,
duration: 2,
style
});
// setUpdateGroupsVisible({} as Peer, false)
dispatch(peerActions.resetSavedGroups(null))
} else if (savedGroups.error) {
message.error({ content: 'Failed to update peer groups. You might not have enough permissions.', key: saveGroupsKey, duration: 2, style });
message.error({
content: 'Failed to update peer groups. You might not have enough permissions.',
key: saveGroupsKey,
duration: 2,
style
});
dispatch(peerActions.resetSavedGroups(null))
}
}, [savedGroups])
const updatePeerKey = 'updating_peer';
useEffect(() => {
const style = { marginTop: 85 }
const style = {marginTop: 85}
if (updatedPeer.loading) {
message.loading({ content: 'Updating peer...', key: updatePeerKey, duration: 0, style })
message.loading({content: 'Updating peer...', key: updatePeerKey, duration: 0, style})
} else if (updatedPeer.success) {
message.success({ content: 'Peer has been successfully updated.', key: updatePeerKey, duration: 2, style });
dispatch(peerActions.setUpdatedPeer({ ...updatedPeer, success: false }))
message.success({content: 'Peer has been successfully updated.', key: updatePeerKey, duration: 2, style});
dispatch(peerActions.setUpdatedPeer({...updatedPeer, success: false}))
dispatch(peerActions.resetUpdatedPeer(null))
} else if (updatedPeer.error) {
message.error({ content: 'Failed to update peer. You might not have enough permissions.', key: updatePeerKey, duration: 2, style });
dispatch(peerActions.setUpdatedPeer({ ...updatedPeer, error: null }))
message.error({
content: 'Failed to update peer. You might not have enough permissions.',
key: updatePeerKey,
duration: 2,
style
});
dispatch(peerActions.setUpdatedPeer({...updatedPeer, error: null}))
dispatch(peerActions.resetUpdatedPeer(null))
}
}, [updatedPeer])
const filterDataTable = ():Peer[] => {
const filterDataTable = (): Peer[] => {
const t = textToSearch.toLowerCase().trim()
let f:Peer[] = filter(peers, (f:Peer) =>
(f.name.toLowerCase().includes(t) || f.ip.includes(t) || f.os.includes(t) || t === "")
) as Peer[]
let f: Peer[] = filter(peers, (f: Peer) =>
(f.name.toLowerCase().includes(t) || f.ip.includes(t) || f.os.includes(t) || t === "")
) as Peer[]
if (optionOnOff === "on") {
f = filter(peers, (f:Peer) => f.connected)
f = filter(peers, (f: Peer) => f.connected)
}
return f
}
@@ -181,7 +205,7 @@ export const Peers = () => {
setDataTable(transformDataTable(data))
}
const onChangeOnOff = ({ target: { value } }: RadioChangeEvent) => {
const onChangeOnOff = ({target: {value}}: RadioChangeEvent) => {
setOptionOnOff(value)
}
@@ -189,16 +213,61 @@ export const Peers = () => {
setPageSize(parseInt(value.toString()))
}
const showConfirmDelete = () => {
let peerRoutes: string[] = []
routes.forEach((r) => {
if (r.peer == peerToAction?.ip) {
peerRoutes.push(r.network_id)
}
})
let content = <Paragraph>Are you sure you want to delete peer from your account?</Paragraph>
let contentModule = <div>{content}</div>
if (peerRoutes.length) {
let contentWithRoutes =
"Removing this peer will disable the following routes: " + peerRoutes
let B = <Alert
message={contentWithRoutes}
type="warning"
showIcon
closable={false}
/>
contentModule = <div>
{content}
<Paragraph>
<Alert
message={
<div>
<>This peer is part of one or more network routes. Removing this peer will disable the following routes:</>
<List
dataSource={peerRoutes}
renderItem={item => <List.Item><Text strong>- {item}</Text></List.Item>}
bordered={false}
split={false}
itemLayout={"vertical"}
/>
</div>}
type="warning"
showIcon={false}
closable={false}
/>
</Paragraph>
</div>
}
let name = peerToAction ? peerToAction.name : ''
confirm({
icon: <ExclamationCircleOutlined />,
icon: <ExclamationCircleOutlined/>,
title: "Delete peer \"" + name + "\"",
width: 600,
content: "Are you sure you want to delete peer from your account?",
content: contentModule,
okType: 'danger',
onOk() {
dispatch(peerActions.deletedPeer.request({getAccessTokenSilently:accessToken, payload: peerToAction ? peerToAction.ip : ''}));
dispatch(peerActions.deletedPeer.request({
getAccessTokenSilently: accessToken,
payload: peerToAction ? peerToAction.ip : ''
}));
},
onCancel() {
setPeerToAction(null);
@@ -208,7 +277,7 @@ export const Peers = () => {
const showConfirmEnableSSH = (record: PeerDataTable) => {
confirm({
icon: <ExclamationCircleOutlined />,
icon: <ExclamationCircleOutlined/>,
title: "Enable SSH Server for \"" + record.name + "\"?",
width: 600,
content: "Experimental feature. Enabling this option allows remote SSH access to this machine from other connected network participants.",
@@ -220,13 +289,14 @@ export const Peers = () => {
},
});
}
function handleSwitchSSH(record: PeerDataTable, checked: boolean) {
const peer = {
id: record.id,
ssh_enabled: checked,
name: record.name
} as Peer
dispatch(peerActions.updatePeer.request({getAccessTokenSilently:accessToken, payload: peer}));
dispatch(peerActions.updatePeer.request({getAccessTokenSilently: accessToken, payload: peer}));
}
@@ -235,7 +305,7 @@ export const Peers = () => {
dispatch(peerActions.setPeer(peerToAction as Peer))
}
const setUpdateGroupsVisible = (peerToAction:Peer, status:boolean) => {
const setUpdateGroupsVisible = (peerToAction: Peer, status: boolean) => {
if (status) {
dispatch(peerActions.setPeer({...peerToAction}))
dispatch(peerActions.setUpdateGroupsVisible(true))
@@ -245,15 +315,15 @@ export const Peers = () => {
dispatch(peerActions.setUpdateGroupsVisible(false))
}
const renderPopoverGroups = (label: string, groups:Group[] | string[] | null, peerToAction:PeerDataTable) => {
const content = groups?.map((g,i) => {
const renderPopoverGroups = (label: string, groups: Group[] | string[] | null, peerToAction: PeerDataTable) => {
const content = groups?.map((g, i) => {
const _g = g as Group
const peersCount = ` - ${_g.peers_count || 0} ${(!_g.peers_count || parseInt(_g.peers_count) !== 1) ? 'peers' : 'peer'} `
return (
<div key={i}>
<Tag
color="blue"
style={{ marginRight: 3 }}
style={{marginRight: 3}}
>
<strong>{_g.name}</strong>
</Tag>
@@ -268,7 +338,8 @@ export const Peers = () => {
}
return (
<Popover placement={popoverPlacement as TooltipPlacement} key={peerToAction.key} content={mainContent} title={null}>
<Popover placement={popoverPlacement as TooltipPlacement} key={peerToAction.key} content={mainContent}
title={null}>
<Button type="link" onClick={() => setUpdateGroupsVisible(peerToAction, true)}>{label}</Button>
</Popover>
)
@@ -280,12 +351,14 @@ export const Peers = () => {
<Row>
<Col span={24}>
<Title level={4}>Peers</Title>
<Paragraph>A list of all the machines in your account including their name, IP and status.</Paragraph>
<Space direction="vertical" size="large" style={{ display: 'flex' }}>
<Paragraph>A list of all the machines in your account including their name, IP and
status.</Paragraph>
<Space direction="vertical" size="large" style={{display: 'flex'}}>
<Row gutter={[16, 24]}>
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
{/*<Input.Search allowClear value={textToSearch} onPressEnter={searchDataTable} onSearch={searchDataTable} placeholder="Search..." onChange={onChangeTextToSearch} />*/}
<Input allowClear value={textToSearch} onPressEnter={searchDataTable} placeholder="Search..." onChange={onChangeTextToSearch} />
<Input allowClear value={textToSearch} onPressEnter={searchDataTable}
placeholder="Search..." onChange={onChangeTextToSearch}/>
</Col>
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
<Space size="middle">
@@ -296,7 +369,8 @@ export const Peers = () => {
optionType="button"
buttonStyle="solid"
/>
<Select value={pageSize.toString()} options={pageSizeOptions} onChange={onChangePageSize} className="select-rows-per-page-en"/>
<Select value={pageSize.toString()} options={pageSizeOptions}
onChange={onChangePageSize} className="select-rows-per-page-en"/>
</Space>
</Col>
<Col xs={24}
@@ -307,18 +381,24 @@ export const Peers = () => {
xxl={5} span={5}>
<Row justify="end">
<Col>
<Link to="/add-peer" className="ant-btn ant-btn-primary ant-btn-block">Add Peer</Link>
<Link to="/add-peer" className="ant-btn ant-btn-primary ant-btn-block">Add
Peer</Link>
</Col>
</Row>
</Col>
</Row>
{failed &&
<Alert message={failed.code} description={failed.message} type="error" showIcon closable/>
<Alert message={failed.code} description={failed.message} type="error" showIcon
closable/>
}
{/*{loading && <Loading/>}*/}
<Card bodyStyle={{padding: 0}}>
<Table
pagination={{pageSize, showSizeChanger: false, showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} peers`)}}
pagination={{
pageSize,
showSizeChanger: false,
showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} peers`)
}}
className="card-table"
showSorterTooltip={false}
scroll={{x: true}}
@@ -328,56 +408,61 @@ export const Peers = () => {
onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
defaultSortOrder='ascend'
sorter={(a, b) => ((a as any).name.localeCompare((b as any).name))}
render={(text:string, record:PeerDataTable,) => {
return <Button type="text" onClick={() => setUpdateGroupsVisible(record, true)}>{text}</Button>
render={(text: string, record: PeerDataTable,) => {
return <Button type="text"
onClick={() => setUpdateGroupsVisible(record, true)}>{text}</Button>
}}
/>
<Column title="IP" dataIndex="ip"
sorter={(a, b) => {
const _a = (a as any).ip.split('.')
const _b = (b as any).ip.split('.')
const a_s = _a.map((i:any) => i.padStart(3, '0')).join()
const b_s = _b.map((i:any) => i.padStart(3, '0')).join()
const a_s = _a.map((i: any) => i.padStart(3, '0')).join()
const b_s = _b.map((i: any) => i.padStart(3, '0')).join()
return a_s.localeCompare(b_s)
}}
render={(text, record, index) => {
return <ButtonCopyMessage keyMessage={(record as PeerDataTable).key} text={text} messageText={'IP copied!'} styleNotification={{}}/>
return <ButtonCopyMessage keyMessage={(record as PeerDataTable).key}
text={text} messageText={'IP copied!'}
styleNotification={{}}/>
}}
/>
<Column title="Status" dataIndex="connected" align="center"
render={(text, record, index) => {
return text ? <Tag color="green">online</Tag> : <Tag color="red">offline</Tag>
return text ? <Tag color="green">online</Tag> :
<Tag color="red">offline</Tag>
}}
/>
<Column title="Groups" dataIndex="groupsCount" align="center"
render={(text, record:PeerDataTable, index) => {
render={(text, record: PeerDataTable, index) => {
return renderPopoverGroups(text, record.groups, record)
}}
/>
<Column
title="SSH Server" dataIndex="ssh_enabled" align="center"
render={(e, record:PeerDataTable, index) => {
let isWindows = record.os.toLocaleLowerCase().startsWith("windows")
let toggle = <Switch size={"small"} checked={e}
disabled={isWindows}
onClick={(checked: boolean) => {
if (checked) {
showConfirmEnableSSH(record)
} else {
handleSwitchSSH(record, checked)
}
}}
/>
render={(e, record: PeerDataTable, index) => {
let isWindows = record.os.toLocaleLowerCase().startsWith("windows")
let toggle = <Switch size={"small"} checked={e}
disabled={isWindows}
onClick={(checked: boolean) => {
if (checked) {
showConfirmEnableSSH(record)
} else {
handleSwitchSSH(record, checked)
}
}}
/>
if (isWindows) {
return <Tooltip title="SSH server feature is not yet supported on Windows">
{toggle}
</Tooltip>
} else {
return toggle
}
if (isWindows) {
return <Tooltip
title="SSH server feature is not yet supported on Windows">
{toggle}
</Tooltip>
} else {
return toggle
}
}
}
}
/>
<Column title="LastSeen" dataIndex="last_seen"
@@ -390,10 +475,11 @@ export const Peers = () => {
return formatOS(text)
}}
/>
<Column title="Version" dataIndex="version" />
<Column title="Version" dataIndex="version"/>
<Column title="" align="center"
render={(text, record, index) => {
return <Dropdown.Button type="text" overlay={actionsMenu} trigger={["click"]}
return <Dropdown.Button type="text" overlay={actionsMenu}
trigger={["click"]}
onVisibleChange={visible => {
if (visible) setPeerToAction(record as PeerDataTable)
}}></Dropdown.Button>

412
src/views/Routes.tsx Normal file
View File

@@ -0,0 +1,412 @@
import React, {useEffect, useState} from 'react';
import {
Alert,
Button, Card,
Col, Dropdown, Input, Menu, message, Modal, Popover, Radio, RadioChangeEvent,
Row, Select, Space, Switch, Table, Tag, Tooltip,
Typography
} from "antd";
import {Container} from "../components/Container";
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "typesafe-actions";
import {Route} from "../store/route/types";
import {actions as routeActions} from "../store/route";
import {actions as peerActions} from "../store/peer";
import {filter, sortBy} from "lodash";
import { ExclamationCircleOutlined,QuestionCircleOutlined} from "@ant-design/icons";
import RouteUpdate from "../components/RouteUpdate";
import tableSpin from "../components/Spin";
import {useOidcAccessToken} from '@axa-fr/react-oidc';
const { Title, Paragraph } = Typography;
const { Column } = Table;
const { confirm } = Modal;
interface RouteDataTable extends Route {
key: string;
sourceCount: number;
sourceLabel: '';
destinationCount: number;
destinationLabel: '';
}
export const Routes = () => {
const {accessToken} = useOidcAccessToken()
const dispatch = useDispatch()
const routes = useSelector((state: RootState) => state.route.data);
const failed = useSelector((state: RootState) => state.route.failed);
const loading = useSelector((state: RootState) => state.route.loading);
const deletedRoute = useSelector((state: RootState) => state.route.deletedRoute);
const savedRoute = useSelector((state: RootState) => state.route.savedRoute);
const peers = useSelector((state: RootState) => state.peer.data)
const [showTutorial, setShowTutorial] = useState(true)
const [textToSearch, setTextToSearch] = useState('');
const [optionAllEnable, setOptionAllEnable] = useState('enabled');
const [pageSize, setPageSize] = useState(5);
const [currentPage, setCurrentPage] = useState(1);
const [dataTable, setDataTable] = useState([] as RouteDataTable[]);
const [routeToAction, setRouteToAction] = useState(null as RouteDataTable | null);
const pageSizeOptions = [
{label: "5", value: "5"},
{label: "10", value: "10"},
{label: "15", value: "15"}
]
const optionsAllEnabled = [{label: 'Enabled', value: 'enabled'},{label: 'All', value: 'all'}]
const itemsMenuAction = [
{
key: "view",
label: (<Button type="text" block onClick={() => onClickViewRoute()}>View</Button>)
},
// {
// key: "delete",
// label: (<Button type="text" block onClick={() => showConfirmDeactivate()}>Deactivate</Button>)
// },
{
key: "delete",
label: (<Button type="text" block onClick={() => showConfirmDelete()}>Delete</Button>)
}
]
const actionsMenu = (<Menu items={itemsMenuAction} ></Menu>)
const isShowTutorial = (routes:Route[]):boolean => {
return (!routes.length || (routes.length === 1 && routes[0].network === "Default"))
}
const peerNameToID = (name:string):string => {
let id = name
let p = peers.find(_p => _p?.name === name)
if (p) {
id = p.ip
}
return id
}
const peerIDToName = (id:string):string => {
let name = id
let p = peers.find(_p => _p?.ip === id)
if (p) {
name = p.name
}
return name
}
const transformDataTable = (d:Route[]):RouteDataTable[] => {
return d.map(p => {
return {
key: p.id,
...p,
peer: peerIDToName(p.peer),
} as RouteDataTable
})
}
useEffect(() => {
dispatch(routeActions.getRoutes.request({getAccessTokenSilently:accessToken, payload: null}));
dispatch(peerActions.getPeers.request({getAccessTokenSilently:accessToken, payload: null}));
}, [])
useEffect(() => {
setShowTutorial(isShowTutorial(routes))
setDataTable(sortBy(transformDataTable(filterDataTable()), "name"))
}, [routes])
useEffect(() => {
setDataTable(transformDataTable(filterDataTable()))
}, [textToSearch, optionAllEnable])
const styleNotification = { marginTop: 85 }
const saveKey = 'saving';
useEffect(() => {
if (savedRoute.loading) {
message.loading({ content: 'Saving...', key: saveKey, duration: 0, style: styleNotification })
} else if (savedRoute.success) {
message.success({ content: 'Route has been successfully updated.', key: saveKey, duration: 2, style: styleNotification });
dispatch(routeActions.setSetupNewRouteVisible(false))
dispatch(routeActions.setSavedRoute({ ...savedRoute, success: false }))
dispatch(routeActions.resetSavedRoute(null))
} else if (savedRoute.error) {
message.error({ content: savedRoute.error.data? savedRoute.error.data : savedRoute.error.message, key: saveKey, duration: 2, style: styleNotification });
dispatch(routeActions.setSavedRoute({ ...savedRoute, error: null }))
dispatch(routeActions.resetSavedRoute(null))
}
}, [savedRoute])
const deleteKey = 'deleting';
useEffect(() => {
const style = { marginTop: 85 }
if (deletedRoute.loading) {
message.loading({ content: 'Deleting...', key: deleteKey, style })
} else if (deletedRoute.success) {
message.success({ content: 'Route has been successfully disabled.', key: deleteKey, duration: 2, style })
dispatch(routeActions.resetDeletedRoute(null))
} else if (deletedRoute.error) {
message.error({ content: 'Failed to remove route. You might not have enough permissions.', key: deleteKey, duration: 2, style })
dispatch(routeActions.resetDeletedRoute(null))
}
}, [deletedRoute])
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setTextToSearch(e.target.value)
};
const searchDataTable = () => {
const data = filterDataTable()
setDataTable(transformDataTable(data))
}
const onChangeAllEnabled = ({ target: { value } }: RadioChangeEvent) => {
setOptionAllEnable(value)
}
const onChangePageSize = (value: string) => {
setPageSize(parseInt(value.toString()))
}
const showConfirmDelete = () => {
confirm({
icon: <ExclamationCircleOutlined />,
width: 600,
content: <Space direction="vertical" size="small">
{routeToAction &&
<>
<Title level={5}>Delete netowork route "{routeToAction ? routeToAction.network_id : ''}"</Title>
<Paragraph>Are you sure you want to delete this route from your account?</Paragraph>
</>
}
</Space>,
okType: 'danger',
onOk() {
dispatch(routeActions.deleteRoute.request({getAccessTokenSilently:accessToken, payload: routeToAction?.id || ''}));
},
onCancel() {
setRouteToAction(null);
},
});
}
const filterDataTable = ():Route[] => {
const t = textToSearch.toLowerCase().trim()
let f:Route[] = filter(routes, (f:Route) =>
(f.network_id.toLowerCase().includes(t) ||f.network.toLowerCase().includes(t) || f.description.toLowerCase().includes(t) || peerIDToName(f.peer).toLowerCase().includes(t) || t === "")
) as Route[]
if (optionAllEnable !== "all") {
f = filter(f, (f:Route) => f.enabled)
}
return f
}
const onClickAddNewRoute = () => {
dispatch(routeActions.setSetupNewRouteVisible(true));
dispatch(routeActions.setRoute({
network: '',
network_id: '',
description: '',
peer: '',
masquerade: false,
metric: 9999,
enabled: true
} as Route))
}
const onClickViewRoute = () => {
dispatch(routeActions.setSetupNewRouteVisible(true));
dispatch(routeActions.setRoute({
id: routeToAction?.id || null,
network: routeToAction?.network,
network_id: routeToAction?.network_id,
description: routeToAction?.description,
peer: routeToAction?.peer,
metric: routeToAction?.metric,
masquerade: routeToAction?.masquerade,
enabled: routeToAction?.enabled
} as Route))
}
const setRouteAndView = (route: RouteDataTable) => {
dispatch(routeActions.setSetupNewRouteVisible(true));
dispatch(routeActions.setRoute({
id: route.id || null,
network: route.network,
network_id: route.network_id,
description: route.description,
peer: route.peer,
metric: route.metric,
masquerade: route.masquerade,
enabled: route.enabled
} as Route))
}
const showConfirmEnableMasquerade = (record: RouteDataTable, checked: boolean) => {
let label = record.network_id ? record.network_id : record.network
let tittle = "Enable Masquerade for \"" + label + "\"?"
let content = "Enabling this option hides other NetBird network IPs behind the routing peer local address when accessing the target Network CIDR. This option allows access to your private networks without configuring routes on your local routers or other devices."
if (!checked) {
tittle = "Disable Masquerade for \"" + label + "\"?"
content = "Disabling this option stops hiding all traffic coming from other NetBird peers behind the routing peer local address when accessing the target Network CIDR. You will need to configure routes for your NetBird network pointing to your routing peer on your local routers or other devices."
}
confirm({
icon: <ExclamationCircleOutlined />,
title: tittle,
width: 600,
content: content,
okType: 'danger',
onOk() {
handleSwitchMasquerade(record, checked)
},
onCancel() {
},
});
}
function handleSwitchMasquerade(record: RouteDataTable, checked: boolean) {
const route = {
...record,
peer: peerNameToID(record.peer),
masquerade: checked,
} as Route
dispatch(routeActions.saveRoute.request({getAccessTokenSilently:accessToken, payload: route}));
}
return(
<>
<Container className="container-main">
<Row>
<Col span={24}>
<Title level={4}>Network Routes</Title>
<Paragraph>Network routes allow you to create routes to access other networks without installing NetBird on every resource.</Paragraph>
<Space direction="vertical" size="large" style={{ display: 'flex' }}>
<Row gutter={[16, 24]}>
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
<Input allowClear value={textToSearch} onPressEnter={searchDataTable} placeholder="Search..." onChange={onChangeTextToSearch} />
</Col>
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
<Space size="middle">
<Radio.Group
options={optionsAllEnabled}
onChange={onChangeAllEnabled}
value={optionAllEnable}
optionType="button"
buttonStyle="solid"
/>
<Select value={pageSize.toString()} options={pageSizeOptions} onChange={onChangePageSize} className="select-rows-per-page-en"/>
</Space>
</Col>
<Col xs={24}
sm={24}
md={5}
lg={5}
xl={5}
xxl={5} span={5}>
<Row justify="end">
<Col>
<Button type="primary" disabled={savedRoute.loading} onClick={onClickAddNewRoute}>Add Route</Button>
</Col>
</Row>
</Col>
</Row>
{failed &&
<Alert message={failed.code} description={failed.message} type="error" showIcon closable/>
}
<Card bodyStyle={{padding: 0}}>
<Table
pagination={{
current: currentPage, hideOnSinglePage: showTutorial, disabled: showTutorial,
pageSize, responsive: true, showSizeChanger: false,
showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} routes`),
onChange: (page, pageSize) => {
setCurrentPage(page)
}
}}
className={`access-control-table ${showTutorial ? "card-table card-table-no-placeholder" : "card-table"}`}
showSorterTooltip={false}
scroll={{x: true}}
loading={tableSpin(loading)}
dataSource={dataTable}>
<Column title={() =>
<span>
Network Identifier
<Tooltip title="You can enable high-availability by assigning the same network identifier and network CIDR to multiple routes">
<QuestionCircleOutlined style={{ marginLeft: '0.25em', color: "gray" }}/>
</Tooltip>
</span>
}
dataIndex="network_id"
onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
defaultSortOrder='ascend'
sorter={(a, b) => ((a as any).network_id.localeCompare((b as any).network_id))}
render={(text, record, index) => {
const desc = (record as RouteDataTable).description.trim()
return <Tooltip title={desc !== "" ? desc : "no description"} arrowPointAtCenter>
<span onClick={() => setRouteAndView(record as RouteDataTable)} className="tooltip-label">{text}</span>
</Tooltip>
}}
/>
<Column title="Network CIDR" dataIndex="network"
onFilter={(value: string | number | boolean, record) => (record as any).network.includes(value)}
sorter={(a, b) => ((a as any).network.localeCompare((b as any).network))}
defaultSortOrder='ascend'
/>
<Column title="Enabled" dataIndex="enabled"
render={(text:Boolean, record:RouteDataTable, index) => {
return text ? <Tag color="green">enabled</Tag> : <Tag color="red">disabled</Tag>
}}
/>
<Column title="Routing Peer" dataIndex="peer"
onFilter={(value: string | number | boolean, record) => (record as any).peer.includes(value)}
sorter={(a, b) => ((a as any).peer.localeCompare((b as any).peer))}
// render={(peerIP:string, RouteDataTable,) => {
// let p = peers.find(_p => _p?.ip === peerIP)
// return <div>{p?.name}</div>
// }}
/>
<Column title="Metric" dataIndex="metric"
onFilter={(value: string | number | boolean, record) => (record as any).metric.includes(value)}
sorter={(a, b) => ((a as any).metric - ((b as any).metric))}
/>
<Column title="Masquerade traffic" dataIndex="masquerade"
render={(e, record: RouteDataTable, index) => {
let toggle = <Switch size={"small"} checked={e}
onClick={(checked: boolean) => {
showConfirmEnableMasquerade(record, checked)
}}
/>
return <Tooltip
title="Hides the traffic with the routing peer address">
{toggle}
</Tooltip>
}}
/>
<Column title="" align="center"
render={(text, record, index) => {
if (deletedRoute.loading || savedRoute.loading) return <></>
return <Dropdown.Button type="text" overlay={actionsMenu} trigger={["click"]}
onVisibleChange={visible => {
if (visible) setRouteToAction(record as RouteDataTable)
}}></Dropdown.Button>
}}
/>
</Table>
{showTutorial &&
<Space direction="vertical" size="small" align="center"
style={{display: 'flex', padding: '45px 15px'}}>
<Button type="link" onClick={onClickAddNewRoute}>Add new route</Button>
</Space>
}
</Card>
</Space>
</Col>
</Row>
</Container>
<RouteUpdate/>
</>
)
}
export default Routes;