mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-01-26 01:21:04 +00:00
Network Routes page (#71)
Add Network routes page to interact with the routes API endpoint of our management
This commit is contained in:
555
package-lock.json
generated
555
package-lock.json
generated
@@ -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",
|
||||
@@ -21402,25 +21414,6 @@
|
||||
"debug": "4"
|
||||
}
|
||||
},
|
||||
"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-formats": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
||||
"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",
|
||||
@@ -21432,17 +21425,13 @@
|
||||
"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-formats": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
||||
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
||||
"requires": {
|
||||
"ajv": "^8.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = () =>
|
||||
|
||||
322
src/components/RouteUpdate.tsx
Normal file
322
src/components/RouteUpdate.tsx
Normal 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
|
||||
@@ -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 };
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
34
src/store/route/actions.ts
Normal file
34
src/store/route/actions.ts
Normal 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
7
src/store/route/index.ts
Normal 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 };
|
||||
89
src/store/route/reducer.ts
Normal file
89
src/store/route/reducer.ts
Normal 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
139
src/store/route/sagas.ts
Normal 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)
|
||||
]);
|
||||
}
|
||||
|
||||
32
src/store/route/service.ts
Normal file
32
src/store/route/service.ts
Normal 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
11
src/store/route/types.ts
Normal 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
|
||||
}
|
||||
@@ -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) =>
|
||||
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,35 +408,39 @@ 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) => {
|
||||
render={(e, record: PeerDataTable, index) => {
|
||||
let isWindows = record.os.toLocaleLowerCase().startsWith("windows")
|
||||
let toggle = <Switch size={"small"} checked={e}
|
||||
disabled={isWindows}
|
||||
@@ -370,7 +454,8 @@ export const Peers = () => {
|
||||
/>
|
||||
|
||||
if (isWindows) {
|
||||
return <Tooltip title="SSH server feature is not yet supported on Windows">
|
||||
return <Tooltip
|
||||
title="SSH server feature is not yet supported on Windows">
|
||||
{toggle}
|
||||
</Tooltip>
|
||||
} else {
|
||||
@@ -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
412
src/views/Routes.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user