Compare commits

..

12 Commits

Author SHA1 Message Date
Misha Bragin
f389862931 Support Auto Groups when updating setup keys (#75)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2022-09-13 17:51:05 +02:00
braginini
521df658ad Shorten the banner text 2022-09-13 11:01:19 +02:00
Maycon Santos
4db17c119a enable new route update 2022-09-08 10:51:36 +02:00
Maycon Santos
230a4cb05e Group network routes by identifier and range (#73)
* fix missing peer when disabling route

and passing peer to modal view

* group rows by HA routes

* Adjust high availability and show fields disabled

* add groupedRoutes to GroupedDataTable

* remove unused fields

* filter by grouped routes

* group routes by network id and network

* use better check for save button
2022-09-08 09:26:38 +02:00
braginini
06316239de Fix routes docs link 2022-09-05 19:45:14 +02:00
braginini
59eff85339 Fix routes docs link 2022-09-05 19:44:49 +02:00
braginini
ffabdf8a1a Add new release banner 2022-09-05 19:09:43 +02:00
Maycon Santos
0fe5aa13b1 Rename Network range and enable masquerade by default (#72) 2022-09-05 16:13:01 +02:00
Maycon Santos
7166eb6e2f Network Routes page (#71)
Add Network routes page to interact with the routes API endpoint of our management
2022-09-05 09:08:51 +02:00
Misha Bragin
c9f1955d6a Fix ACL modal groups input fields width (#69) 2022-08-22 15:27:52 +02:00
Misha Bragin
c3236d05a1 Fix Auth0 OIDC integration 2022-08-17 20:38:52 +02:00
Misha Bragin
25e8a52465 Refactor to generic OIDC config (#66)
Cleans up config.json providing generic OIDC properties.
This allows for using other IDP providers besides Auth0.
The change is backward compatible with the previous versions.
2022-08-15 19:32:01 +02:00
37 changed files with 2497 additions and 733 deletions

View File

@@ -1,19 +1,39 @@
#!/bin/bash
set -e
if [[ -z "${AUTH0_DOMAIN}" ]]; then
echo "AUTH0_DOMAIN environment variable must be set"
exit 1
if [[ -z "${AUTH_AUTHORITY}" ]]; then
if [[ -z "${AUTH0_DOMAIN}" ]]; then
echo "AUTH_AUTHORITY or AUTH0_DOMAIN environment variable must be set"
exit 1
fi
fi
if [[ -z "${AUTH0_CLIENT_ID}" ]]; then
echo "AUTH0_CLIENT_ID environment variable must be set"
exit 1
if [[ -z "${AUTH_CLIENT_ID}" ]]; then
if [[ -z "${AUTH0_CLIENT_ID}" ]]; then
echo "AUTH_CLIENT_ID or AUTH0_CLIENT_ID environment variable must be set"
exit 1
fi
fi
if [[ -z "${AUTH0_AUDIENCE}" ]]; then
echo "AUTH0_AUDIENCE environment variable must be set"
exit 1
if [[ -z "${AUTH_AUDIENCE}" ]]; then
if [[ -z "${AUTH0_AUDIENCE}" ]]; then
echo "AUTH_AUDIENCE or AUTH0_AUDIENCE environment variable must be set"
exit 1
fi
fi
if [[ -z "${AUTH_SUPPORTED_SCOPES}" ]]; then
if [[ -z "${AUTH0_DOMAIN}" ]]; then
echo "AUTH_SUPPORTED_SCOPES environment variable must be set"
exit 1
fi
fi
if [[ -z "${USE_AUTH0}" ]]; then
if [[ -z "${AUTH0_DOMAIN}" ]]; then
echo "USE_AUTH0 environment variable must be set"
exit 1
fi
fi
if [[ -z "${NETBIRD_MGMT_API_ENDPOINT}" ]]; then
@@ -21,11 +41,14 @@ if [[ -z "${NETBIRD_MGMT_API_ENDPOINT}" ]]; then
exit 1
fi
AUTH0_DOMAIN=${AUTH0_DOMAIN}
AUTH0_CLIENT_ID=${AUTH0_CLIENT_ID}
AUTH0_AUDIENCE=${AUTH0_AUDIENCE}
NETBIRD_MGMT_API_ENDPOINT=$(echo $NETBIRD_MGMT_API_ENDPOINT | sed -E 's/(:80|:443)$//')
NETBIRD_MGMT_GRPC_API_ENDPOINT=${NETBIRD_MGMT_GRPC_API_ENDPOINT}
export AUTH_AUTHORITY=${AUTH_AUTHORITY:-https://$AUTH0_DOMAIN}
export AUTH_CLIENT_ID=${AUTH_CLIENT_ID:-$AUTH0_CLIENT_ID}
export AUTH_AUDIENCE=${AUTH_AUDIENCE:-$AUTH0_AUDIENCE}
export USE_AUTH0=${USE_AUTH0:-true}
export AUTH_SUPPORTED_SCOPES=${AUTH_SUPPORTED_SCOPES:-openid profile email api offline_access email_verified}
export NETBIRD_MGMT_API_ENDPOINT=$(echo $NETBIRD_MGMT_API_ENDPOINT | sed -E 's/(:80|:443)$//')
export NETBIRD_MGMT_GRPC_API_ENDPOINT=${NETBIRD_MGMT_GRPC_API_ENDPOINT}
REPO="https://github.com/netbirdio/netbird/"
# this command will fetch the latest release e.g. v0.6.3
@@ -33,7 +56,7 @@ export NETBIRD_LATEST_VERSION=$(basename $(curl -fs -o/dev/null -w %{redirect_ur
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
# replace ENVs in the config
ENV_STR="\$\$AUTH0_DOMAIN \$\$AUTH0_CLIENT_ID \$\$AUTH0_AUDIENCE \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_LATEST_VERSION"
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_LATEST_VERSION"
MAIN_JS=$(find /usr/share/nginx/html/static/js/main.*js)
OIDC_TRUSTED_DOMAINS="/usr/share/nginx/html/OidcTrustedDomains.js"
cp "$MAIN_JS" "$MAIN_JS".copy

553
package-lock.json generated
View File

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

View File

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

18
run-local-keycloak.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
MGMT_PORT=$1
npm run build
docker build -f docker/Dockerfile -t netbird/dashboard-local:latest .
docker rm -f netbird-dashboard
docker run -d --name netbird-dashboard \
-p 3000:80 -p 443:443 \
-e AUTH_AUDIENCE=netbird-client \
-e AUTH_AUTHORITY=http://localhost:8080/realms/netbird \
-e AUTH_CLIENT_ID=netbird-client \
-e USE_AUTH0=false \
-e AUTH_SUPPORTED_SCOPES='openid profile email api offline_access' \
-e NETBIRD_MGMT_API_ENDPOINT=http://localhost:$MGMT_PORT \
-e NETBIRD_MGMT_GRPC_API_ENDPOINT=http://localhost:$MGMT_PORT \
netbird/dashboard-local:latest

16
run-local-legacy.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/bash
MGMT_PORT=$1
npm run build
docker build -f docker/Dockerfile -t netbird/dashboard-local:latest .
docker rm -f netbird-dashboard
docker run -d --name netbird-dashboard \
-p 3000:80 -p 443:443 \
-e AUTH0_AUDIENCE=http://localhost:3000/ \
-e AUTH0_DOMAIN=netbird-localdev.eu.auth0.com \
-e AUTH0_CLIENT_ID=kBRMAOqIZ7hvpVCaypQLCJvTzkYYIXVt \
-e NETBIRD_MGMT_API_ENDPOINT=http://localhost:$MGMT_PORT \
-e NETBIRD_MGMT_GRPC_API_ENDPOINT=http://localhost:$MGMT_PORT \
netbird/dashboard-local:latest

18
run-local.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
MGMT_PORT=$1
npm run build
docker build -f docker/Dockerfile -t netbird/dashboard-local:latest .
docker rm -f netbird-dashboard
docker run -d --name netbird-dashboard \
-p 3000:80 -p 443:443 \
-e AUTH_AUDIENCE=http://localhost:3000/ \
-e AUTH_AUTHORITY=https://netbird-localdev.eu.auth0.com \
-e AUTH_CLIENT_ID=kBRMAOqIZ7hvpVCaypQLCJvTzkYYIXVt \
-e USE_AUTH0=true \
-e AUTH_SUPPORTED_SCOPES='openid profile email api offline_access email_verified' \
-e NETBIRD_MGMT_API_ENDPOINT=http://localhost:$MGMT_PORT \
-e NETBIRD_MGMT_GRPC_API_ENDPOINT=http://localhost:$MGMT_PORT \
netbird/dashboard-local:latest

View File

@@ -8,6 +8,7 @@ import SetupKeys from "./views/SetupKeys";
import AddPeer from "./views/AddPeer";
import Users from './views/Users';
import AccessControl from './views/AccessControl';
import Routes from './views/Routes';
import Banner from "./components/Banner";
import {store} from "./store";
import { Col, Layout, Row} from 'antd';
@@ -38,7 +39,7 @@ function App() {
return (
<Provider store={store}>
<Layout>
{/*<Banner/>*/}
<Banner/>
<Header className="header" style={{
display: "flex",
flexDirection: "column",
@@ -68,6 +69,7 @@ function App() {
<Route path="/add-peer" component={withOidcSecure(AddPeer)}/>
<Route path="/setup-keys" component={withOidcSecure(SetupKeys)}/>
<Route path="/acls" component={withOidcSecure(AccessControl)}/>
<Route path="/routes" component={withOidcSecure(Routes)}/>
<Route path="/users" component={withOidcSecure(Users)}/>
</Switch>
</Content>

View File

@@ -308,14 +308,7 @@ const AccessControlNew = () => {
<Form.Item
name="disabled"
label="Status"
//valuePropName="checked"
>
{/*<Switch
checkedChildren={<CheckOutlined />}
unCheckedChildren={<CloseOutlined />}
onChange={handleChangeDisabled}
/>*/}
<Radio.Group
options={optionsDisabledEnabled}
@@ -330,7 +323,6 @@ const AccessControlNew = () => {
name="tagSourceGroups"
label="Source groups"
rules={[{ validator: selectValidator }]}
style={{display: 'flex'}}
>
<Select mode="tags"
style={{ width: '100%' }}
@@ -352,7 +344,6 @@ const AccessControlNew = () => {
name="tagDestinationGroups"
label="Destination groups"
rules={[{ validator: selectValidator }]}
style={{display: 'flex'}}
>
<Select
mode="tags" style={{ width: '100%' }}

View File

@@ -14,7 +14,7 @@ const Banner = () => {
const linkLearnMore = () => {
return (
<a
href="https://netbird.io/blog/introducing-access-control"
href="https://netbird.io/docs/how-to-guides/network-routes"
className="font-bold underline"
target="_blank"
rel="noreferrer"
@@ -27,7 +27,7 @@ const Banner = () => {
<Row>
<Col xs={24} sm={0} lg={0}>
<Text className="ant-col-md-0" style={{color: "#ffffff"}}>
Big news! Introducing NetBird Access Control.
New Release! Access private networks with the Network Routes feature.
</Text>
</Col>
<Col xs={24} sm={0} lg={0}>
@@ -38,7 +38,7 @@ const Banner = () => {
<Col xs={0} sm={24}>
<Space align="center" style={{display: "flex", justifyContent: "center"}}>
<Text style={{color: "#ffffff"}}>
Big news! Introducing NetBird Access Control.
New Release! Access private networks with the Network Routes feature.
</Text>
<span>
{linkLearnMore()}

View File

@@ -34,10 +34,11 @@ 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 = () =>
logout("",{client_id:config.clientId});
logout("/",{client_id:config.clientId});
useEffect(() => {
const fs = menuItems.filter(m => m?.key !== userEmailKey && m?.key !== userLogoutKey && m?.key !== userDividerKey)
if (screens.xs === true) {

View File

@@ -295,7 +295,7 @@ const PeerUpdate = () => {
) : (
<Form.Item
name="name"
label="Update Name"
label="Name"
rules={[{
required: true,
message: 'Please add a new name for this peer',

View File

@@ -0,0 +1,373 @@
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, Radio, 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';
import {
masqueradeDisabledMSG,
peerToPeerIP,
initPeerMaps,
routePeerSeparator,
transformGroupedDataTable
} from '../utils/routes'
const { Paragraph } = Typography;
interface FormRoute extends Route {
}
const RouteUpdate = () => {
const {accessToken} = useOidcAccessToken()
const dispatch = useDispatch()
const setupNewRouteVisible = useSelector((state: RootState) => state.route.setupNewRouteVisible)
const setupNewRouteHA = useSelector((state: RootState) => state.route.setupNewRouteHA)
const peers = useSelector((state: RootState) => state.peer.data)
const route = useSelector((state: RootState) => state.route.route)
const routes = useSelector((state: RootState) => state.route.data)
const savedRoute = useSelector((state: RootState) => state.route.savedRoute)
// const [groupedDataTable, setGroupedDataTable] = useState([] as GroupedDataTable[]);
const [previousRouteKey, setPreviousRouteKey] = useState("")
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 defaultRoutingPeerMSG = "Routing Peer"
const [routingPeerMSG, setRoutingPeerMSG] = useState(defaultRoutingPeerMSG)
const defaultMasqueradeMSG = "Masquerade"
const [masqueradeMSG, setMasqueradeMSG] = useState(defaultMasqueradeMSG)
const defaultStatusMSG = "Status"
const [statusMSG, setStatusMSG] = useState(defaultStatusMSG)
const [peerNameToIP, peerIPToName] = initPeerMaps(peers);
const optionsDisabledEnabled = [{label: 'Enabled', value: true}, {label: 'Disabled', value: false}]
useEffect(() => {
if (setupNewRouteHA) {
setRoutingPeerMSG("Add additional routing peer")
setMasqueradeMSG("Update Masquerade")
setStatusMSG("Update Status")
} else {
setRoutingPeerMSG(defaultRoutingPeerMSG)
setMasqueradeMSG(defaultMasqueradeMSG)
setStatusMSG(defaultStatusMSG)
setPreviousRouteKey("")
}
}, [setupNewRouteHA])
useEffect(() => {
if (editName) inputNameRef.current!.focus({
cursor: 'end',
});
}, [editName]);
useEffect(() => {
if (editDescription) inputDescriptionRef.current!.focus({
cursor: 'end',
});
}, [editDescription]);
useEffect(() => {
if (!route) return
const fRoute = {
...route,
} as FormRoute
setFormRoute(fRoute)
setPreviousRouteKey(fRoute.network_id+fRoute.network)
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: peerToPeerIP(p.name,p.ip),
value: peerToPeerIP(p.name,p.ip),
disabled: false
})
}
})
const createRouteToSave = (inputRoute:FormRoute):Route => {
let peerIDList = inputRoute.peer.split(routePeerSeparator)
let peerID:string
if (peerIDList[1]) {
peerID = peerIDList[1]
} else {
peerID = peerNameToIP[inputRoute.peer]
}
return {
id: inputRoute.id,
network: inputRoute.network,
network_id: inputRoute.network_id,
description: inputRoute.description,
peer: peerID,
enabled: inputRoute.enabled,
masquerade: inputRoute.masquerade,
metric: inputRoute.metric
} as Route
}
const handleFormSubmit = () => {
form.validateFields()
.then(() => {
if (!setupNewRouteHA || formRoute.peer != '') {
const routeToSave = createRouteToSave(formRoute)
dispatch(routeActions.saveRoute.request({getAccessTokenSilently:accessToken, payload: routeToSave}))
} else {
let groupedDataTable = transformGroupedDataTable(routes,peerIPToName)
groupedDataTable.forEach((group) => {
if (group.key == previousRouteKey) {
group.groupedRoutes.forEach((route) => {
let updateRoute:FormRoute = {
...formRoute,
id: route.id,
peer: route.peer,
metric: route.metric,
enabled: (formRoute.enabled != group.enabled) ? formRoute.enabled : route.enabled
}
const routeToSave = createRouteToSave(updateRoute)
dispatch(routeActions.saveRoute.request({getAccessTokenSilently:accessToken, payload: routeToSave}))
})
}
})
}
})
.catch((errorInfo) => {
console.log('errorInfo', errorInfo)
});
};
const setVisibleNewRoute = (status:boolean) => {
dispatch(routeActions.setSetupNewRouteVisible(status));
}
const setSetupNewRouteHA = (status:boolean) => {
dispatch(routeActions.setSetupNewRouteHA(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)
setSetupNewRouteHA(false)
setPreviousRouteKey("")
}
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.network_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} disabled={!setupNewRouteHA} 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} disabled={!setupNewRouteHA} 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 Range"
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" disabled={!setupNewRouteHA} autoComplete="off" minLength={9} maxLength={43}/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="enabled"
label={statusMSG}
>
<Radio.Group
options={optionsDisabledEnabled}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="peer"
label={routingPeerMSG}
tooltip="Assign a peer as a routing peer for the Network CIDR"
>
<Select
showSearch
style={{ width: '100%' }}
placeholder="Select Peer"
dropdownRender={dropDownRender}
options={options}
allowClear={true}
/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="masquerade"
label={masqueradeMSG}
tooltip={masqueradeDisabledMSG}
>
<Switch size={"small"} disabled={!setupNewRouteHA} 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://netbird.io/docs/how-to-guides/network-routes" style={{color: 'rgb(07, 114, 128)'}}>Learn
more about network routes</Button>
</Col>
</Row>
</Form>
</Drawer>
}
</>
)
}
export default RouteUpdate

View File

@@ -1,139 +1,424 @@
import React, {useEffect, useState} from 'react';
import React, {useEffect, useRef, useState} from 'react';
import {useDispatch, useSelector} from "react-redux";
import { actions as setupKeyActions } from '../store/setup-key';
import {actions as setupKeyActions} from '../store/setup-key';
import {
Button,
Col,
Row,
Typography,
DatePicker,
DatePickerProps,
Divider,
Drawer,
Form,
Input,
Space,
List,
Radio,
Button, Drawer, Form, List, Divider
Row,
Select,
Space,
Tag,
Typography
} from "antd";
import {RootState} from "typesafe-actions";
import {QuestionCircleFilled} from "@ant-design/icons";
import {SetupKey} from "../store/setup-key/types";
import {CloseOutlined, EditOutlined, QuestionCircleFilled} from "@ant-design/icons";
import {SetupKey, SetupKeyToSave} from "../store/setup-key/types";
import {useOidcAccessToken} from "@axa-fr/react-oidc";
const { Text } = Typography;
import {Header} from "antd/es/layout/layout";
import {formatDate, timeAgo} from "../utils/common";
import {RuleObject} from "antd/lib/form";
import {CustomTagProps} from "rc-select/lib/BaseSelect";
import {Group} from "../store/group/types";
const {Option} = Select;
const {Text} = Typography;
const customExpiresFormat: DatePickerProps['format'] = value => {
return formatDate(value)
}
const customLastUsedFormat: DatePickerProps['format'] = value => {
if (value.toString().startsWith("0001")) {
return "never"
}
let ago = timeAgo(value.toString())
if (!ago) {
return "unused"
}
return ago
}
interface FormSetupKey extends SetupKey {
autoGroupNames: string[]
}
const SetupKeyNew = () => {
const {accessToken} = useOidcAccessToken()
const dispatch = useDispatch()
const setupNewKeyVisible = useSelector((state: RootState) => state.setupKey.setupNewKeyVisible)
const setupKey = useSelector((state: RootState) => state.setupKey.setupKey)
const createdSetupKey = useSelector((state: RootState) => state.setupKey.createdSetupKey)
const setupKey = useSelector((state: RootState) => state.setupKey.setupKey)
const savedSetupKey = useSelector((state: RootState) => state.setupKey.savedSetupKey)
const groups = useSelector((state: RootState) => state.group.data)
const [editName, setEditName] = useState(false)
const inputNameRef = useRef<any>(null)
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[])
const [tagGroups, setTagGroups] = useState([] as string[])
const [formSetupKey, setFormSetupKey] = useState({} as SetupKey)
const [formSetupKey, setFormSetupKey] = useState({} as FormSetupKey)
const [form] = Form.useForm()
useEffect(() => {
setFormSetupKey({ ...setupKey } as SetupKey)
form.setFieldsValue(setupKey)
if (editName) inputNameRef.current!.focus({
cursor: 'end',
});
}, [editName]);
useEffect(() => {
setTagGroups(groups?.filter(g => g.name != "All").map(g => g.name) || [])
}, [groups])
useEffect(() => {
if (!setupKey) return
let allGroups = new Map<string, Group>();
groups.forEach(g => {
allGroups.set(g.id!, g)
})
let formKeyGroups :string[] = []
if (setupKey.auto_groups) {
formKeyGroups = setupKey.auto_groups.filter(g => allGroups.get(g)).map(g => allGroups.get(g)!.name)
}
const fSetupKey = {
...setupKey,
autoGroupNames: setupKey.auto_groups ? formKeyGroups : [],
} as FormSetupKey
setFormSetupKey(fSetupKey)
form.setFieldsValue(fSetupKey)
}, [setupKey])
const createSetupKeyToSave = (): SetupKeyToSave => {
const autoGroups = groups?.filter(g => formSetupKey.autoGroupNames.includes(g.name)).map(g => g.id || '') || []
// find groups that do not yet exist (newly added by the user)
const allGroupsNames : string[] = groups?.map(g => g.name);
const groupsToCreate = formSetupKey.autoGroupNames.filter(s => !allGroupsNames.includes(s))
return {
id: formSetupKey.id,
name: formSetupKey.name,
type: formSetupKey.type,
auto_groups: autoGroups,
revoked: formSetupKey.revoked,
groupsToCreate: groupsToCreate
} as SetupKeyToSave
}
const handleFormSubmit = () => {
form.validateFields()
.then((values) => {
dispatch(setupKeyActions.createSetupKey.request({getAccessTokenSilently:accessToken, payload: formSetupKey}))
let setupKeyToSave = createSetupKeyToSave()
dispatch(setupKeyActions.saveSetupKey.request({
getAccessTokenSilently: accessToken,
payload: setupKeyToSave
}))
})
.catch((errorInfo) => {
console.log('errorInfo', errorInfo)
});
};
const setVisibleNewSetupKey = (status:boolean) => {
const setVisibleNewSetupKey = (status: boolean) => {
dispatch(setupKeyActions.setSetupNewKeyVisible(status));
}
const onCancel = () => {
if (createdSetupKey.loading) return
if (savedSetupKey.loading) return
dispatch(setupKeyActions.setSetupKey({
name: '',
type: 'reusable'
name: "",
type: "reusable",
key: "",
last_used: "",
expires: "",
state: "valid",
auto_groups: new Array()
} as SetupKey))
setFormSetupKey({} as FormSetupKey)
setVisibleNewSetupKey(false)
}
const onChange = (data:any) => {
const onChange = (data: any) => {
setFormSetupKey({...formSetupKey, ...data})
}
const toggleEditName = (status: boolean) => {
setEditName(status);
}
const selectValidator = (_: RuleObject, value: string[]) => {
let hasSpaceNamed = []
value.forEach(function (v: string) {
if (!v.trim().length) {
hasSpaceNamed.push(v)
}
})
if (hasSpaceNamed.length) {
return Promise.reject(new Error("Group names with just spaces are not allowed"))
}
return Promise.resolve()
}
const tagRender = (props: CustomTagProps) => {
const {label, value, closable, onClose} = props;
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
event.preventDefault();
event.stopPropagation();
};
return (
<Tag
color="blue"
onMouseDown={onPreventMouseDown}
closable={closable}
onClose={onClose}
style={{marginRight: 3}}
>
<strong>{value}</strong>
</Tag>
);
}
const optionRender = (label: string) => {
let peersCount = ''
const g = groups.find(_g => _g.name === label)
if (g) peersCount = ` - ${g.peers_count || 0} ${(!g.peers_count || parseInt(g.peers_count) !== 1) ? 'peers' : 'peer'} `
return (
<>
<Tag
color="blue"
style={{marginRight: 3}}
>
<strong>{label}</strong>
</Tag>
<span style={{fontSize: ".85em"}}>{peersCount}</span>
</>
)
}
const dropDownRender = (menu: React.ReactElement) => (
<>
{menu}
<Divider style={{margin: '8px 0'}}/>
<Row style={{padding: '0 8px 4px'}}>
<Col flex="auto">
<span style={{color: "#9CA3AF"}}>Add new group by pressing "Enter"</span>
</Col>
<Col flex="none">
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
fill="#9CA3AF"/>
</svg>
</Col>
</Row>
</>
)
const handleChangeTags = (value: string[]) => {
let validatedValues: string[] = []
value.forEach(function (v) {
if (v.trim().length) {
validatedValues.push(v)
}
})
setSelectedTagGroups(validatedValues)
};
const inputLabel = (text: any) => (
<>
<span>{text}</span>
<Tag color="red">{formSetupKey.state}</Tag>
</>
)
return (
<>
{setupKey &&
<Drawer
title="New setup key"
forceRender={true}
// width={512}
visible={setupNewKeyVisible}
bodyStyle={{paddingBottom: 80}}
onClose={onCancel}
footer={
<Space style={{display: 'flex', justifyContent: 'end'}}>
<Button disabled={createdSetupKey.loading} onClick={onCancel}>Cancel</Button>
<Button disabled={createdSetupKey.loading} type="primary" onClick={handleFormSubmit}>Create</Button>
</Space>
}
>
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}>
<Row gutter={16}>
<Col span={24}>
<Form.Item
name="Name"
label="Name"
rules={[{required: true, message: 'Please enter key name'}]}
>
<Input placeholder="Please enter key name" autoComplete="off"/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="Type"
label="Type"
rules={[{required: true, message: 'Please enter key type'}]}
style={{display: 'flex'}}
>
<Radio.Group style={{display: 'flex'}}>
<Space direction="vertical" style={{flex: 1}}>
<List
size="large"
bordered
{setupKey &&
<Drawer
forceRender={true}
headerStyle={{display: "none"}}
visible={setupNewKeyVisible}
bodyStyle={{paddingBottom: 80}}
onClose={onCancel}
footer={
<Space style={{display: 'flex', justifyContent: 'end'}}>
<Button disabled={savedSetupKey.loading} onClick={onCancel}>Cancel</Button>
<Button type="primary" disabled={savedSetupKey.loading}
onClick={handleFormSubmit}>{`${formSetupKey.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 && setupKey.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 && setupKey.id && formSetupKey.name ? (
<div className={"access-control input-text ant-drawer-title"}
onClick={() => toggleEditName(true)}>{formSetupKey.name ? formSetupKey.name : setupKey.name}
<EditOutlined/></div>
) : (
<Form.Item
name="name"
label="Name"
rules={[{
required: true,
message: 'Please add a new name for this peer',
whitespace: true
}]}
style={{display: 'flex'}}
>
<Input
placeholder={setupKey.name}
ref={inputNameRef}
onPressEnter={() => toggleEditName(false)}
onBlur={() => toggleEditName(false)}
autoComplete="off"/>
</Form.Item>)}
</Col>
</Row>
</Header>
</Col>
{setupKey.id && formSetupKey.name &&
<Col span={24}>
<Form.Item
name="key"
label={<>
<span style={{
marginRight: "5px",
}}>Key</span>
<Tag
color={formSetupKey.state === "valid" ? "green" : "red"}>{formSetupKey.state}</Tag>
</>}
>
<List.Item>
<Radio value={"reusable"}>
<Space direction="vertical" size="small">
<Text strong>Reusable</Text>
<Text>This type of a setup key allows to setup multiple
machine</Text>
</Space>
</Radio>
</List.Item>
<List.Item>
<Radio value={"one-off"}>
<Space direction="vertical" size="small">
<Text strong>One-off</Text>
<Text>This key can be used only once</Text>
</Space>
</Radio>
</List.Item>
</List>
<Input
disabled={true}
autoComplete="off"/>
</Form.Item>
</Col>
}
</Space>
</Radio.Group>
</Form.Item>
</Col>
<Col span={24}>
<Divider></Divider>
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
href="https://docs.netbird.io/docs/overview/setup-keys" style={{color: 'rgb(07, 114, 128)'}}>Learn
more about setup keys</Button>
</Col>
</Row>
</Form>
{setupKey.id && formSetupKey.name &&
<Col span={12}>
<Form.Item
name="expires"
label="Expires"
tooltip="The expiration date of the key"
>
<DatePicker disabled={true} style={{width: '100%'}}
format={customExpiresFormat}/>
</Form.Item>
</Col>
}
{setupKey.id && formSetupKey.name &&
<Col span={12}>
<Form.Item
name="last_used"
label="Last Used"
tooltip="The last time the key was used"
>
<DatePicker disabled={true} style={{width: '100%'}}
format={customLastUsedFormat}/>
</Form.Item>
</Col>
}
<Col span={24}>
<Form.Item
name="type"
label="Type"
rules={[{required: true, message: 'Please enter key type'}]}
style={{display: 'flex'}}
>
<Radio.Group style={{display: 'flex'}} disabled={setupKey.id}>
<Space direction="vertical" style={{flex: 1}}>
<List
size="large"
bordered
>
<List.Item>
<Radio value={"reusable"}>
<Space direction="vertical" size="small">
<Text strong>Reusable</Text>
<Text>This type of a setup key allows to enroll multiple
machines</Text>
</Space>
</Radio>
</List.Item>
<List.Item>
<Radio value={"one-off"}>
<Space direction="vertical" size="small">
<Text strong>One-off</Text>
<Text>This key can be used only once</Text>
</Space>
</Radio>
</List.Item>
</List>
</Drawer>
}
</Space>
</Radio.Group>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="autoGroupNames"
label="Auto-assigned groups"
tooltip="Every peer enrolled with this key will be automatically added to these groups"
rules={[{validator: selectValidator}]}
>
<Select mode="tags"
style={{width: '100%'}}
placeholder="Associate groups with the key"
tagRender={tagRender}
onChange={handleChangeTags}
dropdownRender={dropDownRender}
// enabled only when we have a new key !setupkey.id or when the key is valid
disabled={!(!setupKey.id || setupKey.valid)}
>
{
tagGroups.map(m =>
<Option key={m}>{optionRender(m)}</Option>
)
}
</Select>
</Form.Item>
</Col>
<Col span={24}>
<Divider></Divider>
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
href="https://netbird.io/docs/overview/setup-keys"
style={{color: 'rgb(07, 114, 128)'}}>Learn
more about setup keys</Button>
</Col>
</Row>
</Form>
</Drawer>
}
</>
)
}

View File

@@ -1,7 +1,10 @@
{
"domain": "$AUTH0_DOMAIN",
"clientId": "$AUTH0_CLIENT_ID",
"audience": "$AUTH0_AUDIENCE",
"auth0Auth": "$USE_AUTH0",
"authAuthority": "$AUTH_AUTHORITY",
"authClientId": "$AUTH_CLIENT_ID",
"authScopesSupported": "$AUTH_SUPPORTED_SCOPES",
"authAudience": "$AUTH_AUDIENCE",
"apiOrigin": "$NETBIRD_MGMT_API_ENDPOINT",
"grpcApiOrigin": "$NETBIRD_MGMT_GRPC_API_ENDPOINT",
"latestVersion": "$NETBIRD_LATEST_VERSION"

View File

@@ -8,23 +8,15 @@ if (process.env.NODE_ENV !== 'production') {
}
export function getConfig() {
// Configure the audience here. By default, it will take whatever is in the config
// (specified by the `audience` key) unless it's the default value of "YOUR_API_IDENTIFIER" (which
// is what you get sometimes by using the Auth0 sample download tool from the quickstart page, if you
// don't have an API).
// If this resolves to `null`, the API page changes to show some helpful info about what to do
// with the audience.
const audience =
configJson.audience && configJson.audience !== "YOUR_API_IDENTIFIER"
? configJson.audience
: null;
return {
domain: configJson.domain,
clientId: configJson.clientId,
auth0Auth: configJson.auth0Auth == "true", //due to substitution we can't use boolean in the config
authority: configJson.authAuthority,
clientId: configJson.authClientId,
scopesSupported: configJson.authScopesSupported,
apiOrigin: configJson.apiOrigin,
grpcApiOrigin: configJson.grpcApiOrigin,
latestVersion: configJson.latestVersion,
...(audience ? { audience } : null),
audience: configJson.authAudience,
};
}

View File

@@ -4,33 +4,37 @@ import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import history from "./utils/history";
import { getConfig } from "./config";
import {OidcProvider, useOidc} from '@axa-fr/react-oidc';
import {getConfig} from "./config";
import {OidcProvider} from '@axa-fr/react-oidc';
import {BrowserRouter} from "react-router-dom";
import Loading from "./components/Loading";
import LoginError from "./components/LoginError";
import {AuthorityConfiguration} from "@axa-fr/react-oidc/dist/vanilla/oidc";
const config = getConfig();
const authority = 'https://' + config.domain
// Unfortunately Auth0 https://<DOMAIN>/.well-known/openid-configuration doesn't contain end_session_endpoint that
// is required for doing logout. Therefore, we need to hardcode the config for auth
const auth0AuthorityConfig: AuthorityConfiguration = {
authorization_endpoint: new URL("authorize", config.authority).href,
token_endpoint: new URL("oauth/token", config.authority).href,
revocation_endpoint: new URL("oauth/revoke", config.authority).href,
end_session_endpoint: new URL("v2/logout", config.authority).href,
userinfo_endpoint: new URL("userinfo", config.authority).href,
} as AuthorityConfiguration
const providerConfig = {
authority: authority,
authority: config.authority,
client_id: config.clientId,
redirect_uri: window.location.origin+'#callback',
redirect_uri: window.location.origin + '/#callback',
refresh_time_before_tokens_expiration_in_second: 30,
silent_redirect_uri: window.location.origin + '#silent-callback',
scope: 'openid profile email api offline_access email_verified',
silent_redirect_uri: window.location.origin + '/#silent-callback',
scope: config.scopesSupported,
// disabling service worker
// service_worker_relative_url:'/OidcServiceWorker.js',
service_worker_only: false,
authority_configuration: {
authorization_endpoint: authority + "/authorize",
token_endpoint: authority + "/oauth/token",
revocation_endpoint: authority + "/oauth/revoke",
end_session_endpoint: authority + "/v2/logout",
userinfo_endpoint: authority + "/userinfo"
},
...(config.audience ? {extras:{ audience: config.audience}} : null)
authority_configuration: config.auth0Auth ? auth0AuthorityConfig : undefined,
...(config.audience ? {extras: {audience: config.audience}} : null)
};
const root = ReactDOM.createRoot(
@@ -40,23 +44,21 @@ const root = ReactDOM.createRoot(
const loadingComponent = () => <Loading padding="3em" width="50px" height="50px"/>
root.render(
<OidcProvider
configuration={providerConfig}
callbackSuccessComponent={loadingComponent}
authenticatingErrorComponent={LoginError}
authenticatingComponent={loadingComponent}
sessionLostComponent={loadingComponent}
loadingComponent={loadingComponent}
onSessionLost={()=>{
history.push("/peers")
}}
>
<BrowserRouter>
<App/>
</BrowserRouter>
</OidcProvider>
<OidcProvider
configuration={providerConfig}
callbackSuccessComponent={loadingComponent}
authenticatingErrorComponent={LoginError}
authenticatingComponent={loadingComponent}
sessionLostComponent={loadingComponent}
loadingComponent={loadingComponent}
onSessionLost={() => {
history.push("/peers")
}}
>
<BrowserRouter>
<App/>
</BrowserRouter>
</OidcProvider>
);
// If you want to start measuring performance in your app, pass a function

View File

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

View File

@@ -21,4 +21,12 @@ export interface PeerGroupsToSave {
groupsToRemove: string[];
groupsToAdd: string[];
groupsNoId: string[];
}
}
export interface PeerNameToIP {
[key: string]: string;
}
export interface PeerIPToName {
[key: string]: string;
}

View File

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

View File

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

View File

@@ -0,0 +1,35 @@
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>(),
setSetupNewRouteHA: createAction('SET_SETUP_NEW_ROUTE_HA')<boolean>()
};
export type ActionTypes = ActionType<typeof actions>;
export default actions;

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

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

View File

@@ -0,0 +1,95 @@
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;
setupNewRouteHA: 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,
setupNewRouteHA: 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)
const setupNewRouteHA = createReducer<boolean, ActionTypes>(initialState.setupNewRouteHA)
.handleAction(actions.setSetupNewRouteHA, (store, action) => action.payload)
export default combineReducers({
data,
route,
loading,
failed,
saving,
deletedRoute,
savedRoute,
setupNewRouteVisible,
setupNewRouteHA
});

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

@@ -0,0 +1,132 @@
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
import {ApiError, ApiResponse, CreateResponse, DeleteResponse} from '../../services/api-client/types';
import {Route} from './types'
import service from './service';
import actions from './actions';
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))
}
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(actions.getRoutes.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
} catch (err) {
yield put(actions.saveRoute.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null
} as CreateResponse<Route | null>));
}
}
export function* setDeleteRoute(action: ReturnType<typeof actions.setDeletedRoute>): Generator {
yield put(actions.setDeletedRoute(action.payload))
}
export function* deleteRoute(action: ReturnType<typeof actions.deleteRoute.request>): Generator {
try {
yield call(actions.setDeletedRoute,{
loading: true,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>)
const effect = yield call(service.deletedRoute, action.payload);
const response = effect as ApiResponse<any>;
yield put(actions.deleteRoute.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as DeleteResponse<string | null>));
const routes = (yield select(state => state.route.data)) as Route[]
yield put(actions.getRoutes.success(routes.filter((p:Route) => p.id !== action.payload.payload)))
} catch (err) {
yield put(actions.deleteRoute.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null
} as DeleteResponse<string | null>));
}
}
export default function* sagas(): Generator {
yield all([
takeLatest(actions.getRoutes.request, getRoutes),
takeLatest(actions.saveRoute.request, saveRoute),
takeLatest(actions.deleteRoute.request, deleteRoute)
]);
}

View File

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

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

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

View File

@@ -53,7 +53,6 @@ export function* saveRule(action: ReturnType<typeof actions.saveRule.request>):
})
))
const resGroups = (responsesGroup as ApiResponse<Rule>[]).filter(r => r.statusCode === 200).map(r => (r.body as Group))
const currentGroups = [...(yield select(state => state.group.data)) as Rule[]]

View File

@@ -1,8 +1,7 @@
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
import {SetupKey, SetupKeyRevoke} from './types';
import {SetupKey, SetupKeyToSave} from './types';
import {
ApiError,
ChangeResponse,
CreateResponse,
DeleteResponse,
RequestPayload
@@ -15,12 +14,13 @@ const actions = {
'GET_SETUP_KEYS_FAILURE',
)<RequestPayload<null>, SetupKey[], ApiError>(),
createSetupKey: createAsyncAction(
'CREATE_SETUP_KEY_REQUEST',
'CREATE_SETUP_KEY_SUCCESS',
'CREATE_SETUP_KEY_FAILURE',
)<RequestPayload<SetupKey>, CreateResponse<SetupKey | null>, CreateResponse<SetupKey | null>>(),
setCreateSetupKey: createAction('SET_CREATE_SETUP_KEY')<CreateResponse<SetupKey | null>>(),
saveSetupKey: createAsyncAction(
'SAVE_SETUP_KEY_REQUEST',
'SAVE_SETUP_KEY_SUCCESS',
'SAVE_SETUP_KEY_FAILURE',
)<RequestPayload<SetupKeyToSave>, CreateResponse<SetupKey | null>, CreateResponse<SetupKey | null>>(),
setSavedSetupKey: createAction('SET_SAVE_SETUP_KEY')<CreateResponse<SetupKey | null>>(),
resetSavedSetupKey: createAction('RESET_SAVE_SETUP_KEY')<null>(),
deleteSetupKey: createAsyncAction(
'DELETE_SETUP_KEY_REQUEST',
@@ -30,15 +30,6 @@ const actions = {
setDeleteSetupKey: createAction('SET_DELETE_SETUP_KEY')<DeleteResponse<string | null>>(),
resetDeletedSetupKey: createAction('RESET_DELETE_SETUP_KEY')<null>(),
revokeSetupKey: createAsyncAction(
'REVOKE_SETUP_KEY_REQUEST',
'REVOKE_SETUP_KEY_SUCCESS',
'REVOKE_SETUP_KEY_FAILURE'
)<RequestPayload<SetupKeyRevoke>, ChangeResponse<SetupKey | null>, ChangeResponse<SetupKey | null>>(),
setRevokeSetupKey: createAction('SET_REVOKED_SETUP_KEY')<ChangeResponse<SetupKey | null>>(),
resetRevokedSetupKey: createAction('RESET_REVOKED_SETUP_KEY')<null>(),
removeSetupKey: createAction('REMOVE_SETUP_KEY')<string>(),
setSetupKey: createAction('SET_SETUP_KEY')<SetupKey>(),
setSetupNewKeyVisible: createAction('SET_SETUP_NEW_KEY_VISIBLE')<boolean>()

View File

@@ -12,7 +12,7 @@ type StateType = Readonly<{
saving: boolean;
deletedSetupKey: DeleteResponse<string | null>;
revokedSetupKey: ChangeResponse<SetupKey | null>;
createdSetupKey: CreateResponse<SetupKey | null>;
savedSetupKey: CreateResponse<SetupKey | null>;
setupNewKeyVisible: boolean
}>;
@@ -36,7 +36,7 @@ const initialState: StateType = {
error: null,
data : null
},
createdSetupKey: <CreateResponse<SetupKey | null>>{
savedSetupKey: <CreateResponse<SetupKey | null>>{
loading: false,
success: false,
failure: false,
@@ -75,18 +75,12 @@ const deletedSetupKey = createReducer<DeleteResponse<string | null>, ActionTypes
.handleAction(actions.setDeleteSetupKey, (store, action) => action.payload)
.handleAction(actions.resetDeletedSetupKey, (store, action) => initialState.deletedSetupKey);
const revokedSetupKey = createReducer<ChangeResponse<SetupKey | null>, ActionTypes>(initialState.revokedSetupKey)
.handleAction(actions.revokeSetupKey.request, () => initialState.revokedSetupKey)
.handleAction(actions.revokeSetupKey.success, (store, action) => action.payload)
.handleAction(actions.revokeSetupKey.failure, (store, action) => action.payload)
.handleAction(actions.setRevokeSetupKey, (store, action) => action.payload)
.handleAction(actions.resetRevokedSetupKey, () => initialState.revokedSetupKey)
const createdSetupKey = createReducer<CreateResponse<SetupKey | null>, ActionTypes>(initialState.createdSetupKey)
.handleAction(actions.createSetupKey.request, () => initialState.createdSetupKey)
.handleAction(actions.createSetupKey.success, (store, action) => action.payload)
.handleAction(actions.createSetupKey.failure, (store, action) => action.payload)
.handleAction(actions.setCreateSetupKey, (store, action) => action.payload)
const savedSetupKey = createReducer<CreateResponse<SetupKey | null>, ActionTypes>(initialState.savedSetupKey)
.handleAction(actions.saveSetupKey.request, () => initialState.savedSetupKey)
.handleAction(actions.saveSetupKey.success, (store, action) => action.payload)
.handleAction(actions.saveSetupKey.failure, (store, action) => action.payload)
.handleAction(actions.setSavedSetupKey, (store, action) => action.payload)
.handleAction(actions.resetSavedSetupKey, () => initialState.savedSetupKey)
const setupNewKeyVisible = createReducer<boolean, ActionTypes>(initialState.setupNewKeyVisible)
.handleAction(actions.setSetupNewKeyVisible, (store, action) => action.payload)
@@ -98,7 +92,6 @@ export default combineReducers({
failed,
saving,
deletedSetupKey,
revokedSetupKey,
createdSetupKey,
savedSetupKey: savedSetupKey,
setupNewKeyVisible
});

View File

@@ -1,27 +1,36 @@
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
import {ApiError, ApiResponse, ChangeResponse, CreateResponse, DeleteResponse} from '../../services/api-client/types';
import {SetupKey, SetupKeyRevoke} from './types'
import {ApiError, ApiResponse, CreateResponse, DeleteResponse} from '../../services/api-client/types';
import {SetupKey, SetupKeyToSave} from './types'
import service from './service';
import actions from './actions';
import serviceGroup from "../group/service";
import {Group} from "../group/types";
import {actions as groupActions} from "../group";
export function* getSetupKeys(action: ReturnType<typeof actions.getSetupKeys.request>): Generator {
try {
const effect = yield call(service.getSetupKeys, action.payload);
const response = effect as ApiResponse<SetupKey[]>;
yield put(actions.getSetupKeys.success(response.body));
yield put(actions.getSetupKeys.success(response.body.map(k => {
// always set auto_groups even if absent (avoid null)
if (k.auto_groups) {
return k
}
return {...k, auto_groups: []}
})));
} catch (err) {
yield put(actions.getSetupKeys.failure(err as ApiError));
}
}
export function* setCreateSetupKey(action: ReturnType<typeof actions.setCreateSetupKey>): Generator {
yield put(actions.setCreateSetupKey(action.payload))
export function* setCreateSetupKey(action: ReturnType<typeof actions.setSavedSetupKey>): Generator {
yield put(actions.setSavedSetupKey(action.payload))
}
export function* createSetupKey(action: ReturnType<typeof actions.createSetupKey.request>): Generator {
export function* saveSetupKey(action: ReturnType<typeof actions.saveSetupKey.request>): Generator {
try {
yield put(actions.setCreateSetupKey({
yield put(actions.setSavedSetupKey({
loading: true,
success: false,
failure: false,
@@ -29,10 +38,46 @@ export function* createSetupKey(action: ReturnType<typeof actions.createSetupKey
data: null
} as CreateResponse<SetupKey | null>))
const effect = yield call(service.createSetupKey, action.payload);
const keyToSave = action.payload.payload
let groupsToCreate = keyToSave.groupsToCreate
if (!groupsToCreate) {
groupsToCreate = []
}
// first, create groups that were newly added by user
const responsesGroup = yield all(groupsToCreate.map(g => call(serviceGroup.createGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: { name: g }
})
))
const resGroups = (responsesGroup as ApiResponse<Group>[]).filter(r => r.statusCode === 200).map(g => (g.body as Group)).map(g => g.id)
const newGroups = [...keyToSave.auto_groups, ...resGroups]
let effect
if (!keyToSave.id) {
effect = yield call(service.createSetupKey, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: {
name: keyToSave.name,
auto_groups: newGroups,
type: keyToSave.type
} as SetupKeyToSave
});
} else {
effect = yield call(service.editSetupKey, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: {
id: keyToSave.id,
name: keyToSave.name,
revoked: keyToSave.revoked,
auto_groups: newGroups,
} as SetupKeyToSave
});
}
const response = effect as ApiResponse<SetupKey>;
yield put(actions.createSetupKey.success({
yield put(actions.saveSetupKey.success({
loading: false,
success: true,
failure: false,
@@ -40,11 +85,10 @@ export function* createSetupKey(action: ReturnType<typeof actions.createSetupKey
data: response.body
} as CreateResponse<SetupKey | null>));
const setupKeys = [...(yield select(state => state.setupKey.data)) as SetupKey[]]
setupKeys.unshift(response.body)
yield put(actions.getSetupKeys.success(setupKeys));
yield put(groupActions.getGroups.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
yield put(actions.getSetupKeys.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
} catch (err) {
yield put(actions.createSetupKey.failure({
yield put(actions.saveSetupKey.failure({
loading: false,
success: false,
failure: false,
@@ -92,53 +136,11 @@ export function* deleteSetupKey(action: ReturnType<typeof actions.deleteSetupKey
}
}
export function* revokeSetupKey(action: ReturnType<typeof actions.revokeSetupKey.request>): Generator {
try {
yield put(actions.setRevokeSetupKey({
loading: true,
success: false,
failure: false,
error: null,
data: null
} as ChangeResponse<SetupKey | null>))
const effect = yield call(service.revokeSetupKey, action.payload);
const response = effect as ApiResponse<SetupKey>;
yield put(actions.revokeSetupKey.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as ChangeResponse<SetupKey | null>));
const setupKeys = [...(yield select(state => state.setupKey.data)) as SetupKey[]]
let setupKey = setupKeys.find(s => s.id === response.body.id) as SetupKey
if (setupKey) {
setupKey.revoked = response.body.revoked
setupKey.valid = response.body.valid
setupKey.state = response.body.state
setupKey.expires = response.body.expires
}
yield put(actions.getSetupKeys.success(setupKeys));
} catch (err) {
yield put(actions.createSetupKey.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null
} as CreateResponse<SetupKey | null>));
}
}
export default function* sagas(): Generator {
yield all([
takeLatest(actions.getSetupKeys.request, getSetupKeys),
takeLatest(actions.createSetupKey.request, createSetupKey),
takeLatest(actions.deleteSetupKey.request, deleteSetupKey),
takeLatest(actions.revokeSetupKey.request, revokeSetupKey)
takeLatest(actions.saveSetupKey.request, saveSetupKey),
takeLatest(actions.deleteSetupKey.request, deleteSetupKey)
]);
}

View File

@@ -1,6 +1,6 @@
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
import { apiClient } from '../../services/api-client';
import {SetupKey, SetupKeyNew, SetupKeyRevoke} from './types';
import {SetupKey, SetupKeyToSave} from './types';
export default {
async getSetupKeys(payload:RequestPayload<null>): Promise<ApiResponse<SetupKey[]>> {
@@ -15,22 +15,19 @@ export default {
payload
);
},
async revokeSetupKey(payload:RequestPayload<SetupKeyRevoke>): Promise<ApiResponse<SetupKey>> {
return apiClient.put<SetupKey>(
`/api/setup-keys/` + payload.payload.id,
payload
);
},
async renameSetupKey(payload:RequestPayload<any>): Promise<ApiResponse<SetupKey>> {
return apiClient.put<SetupKey>(
`/api/setup-keys/` + payload.payload.id,
payload
);
},
async createSetupKey(payload:RequestPayload<SetupKey>): Promise<ApiResponse<SetupKey>> {
async createSetupKey(payload:RequestPayload<SetupKeyToSave>): Promise<ApiResponse<SetupKey>> {
return apiClient.post<SetupKey>(
`/api/setup-keys`,
payload
);
},
async editSetupKey(payload:RequestPayload<SetupKeyToSave>): Promise<ApiResponse<SetupKey>> {
const id = payload.payload.id
// @ts-ignore
delete payload.payload.id
return apiClient.put<SetupKey>(
`/api/setup-keys/${id}`,
payload
);
},
};

View File

@@ -1,3 +1,5 @@
import {Group} from "../group/types";
export interface SetupKey {
expires: string;
id: string;
@@ -9,15 +11,10 @@ export interface SetupKey {
type: string;
used_times: number;
valid: boolean;
auto_groups: string[]
}
export interface SetupKeyNew {
id: string;
name: string;
type: string;
}
export interface SetupKeyRevoke {
id: string;
revoked: boolean;
export interface SetupKeyToSave extends SetupKey
{
groupsToCreate: string[]
}

81
src/utils/routes.ts Normal file
View File

@@ -0,0 +1,81 @@
import {Peer, PeerNameToIP, PeerIPToName} from "../store/peer/types";
import {Route} from "../store/route/types";
export const routePeerSeparator = " - "
export const masqueradeDisabledMSG = "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."
export const masqueradeEnabledMSG = "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."
export const peerToPeerIP = (name:string,ip:string):string => {
return name + routePeerSeparator + ip
}
export const initPeerMaps = (peers:Peer[]): [PeerNameToIP, PeerIPToName] => {
let peerNameToIP = {} as PeerNameToIP
let peerIPToName = {} as PeerIPToName
peers.forEach((p) =>{
peerNameToIP[p.name] = p.ip
peerIPToName[p.ip] = p.name
})
return [ peerNameToIP, peerIPToName]
}
export interface RouteDataTable extends Route {
key: string;
}
export interface GroupedDataTable {
key: string
network_id: string
network: string
enabled: boolean
masquerade: boolean
description: string
routesCount: number
groupedRoutes: RouteDataTable[]
}
export const transformDataTable = (d:Route[],peerIPToName:PeerIPToName):RouteDataTable[] => {
return d.map(p => {
return {
key: p.id,
...p,
peer: peerIPToName[p.peer] ? peerIPToName[p.peer] : p.peer,
} as RouteDataTable
})
}
export const transformGroupedDataTable = (routes:Route[],peerIPToName:PeerIPToName):GroupedDataTable[] => {
let keySet = new Set(routes.map(r => {
return r.network_id + r.network
}))
let groupedRoutes:GroupedDataTable[] = []
keySet.forEach((p) => {
let hasEnabled = false
let lastRoute:Route
let listedRoutes:Route[] = []
routes.forEach((r) => {
if ( p === r.network_id + r.network ) {
lastRoute = r
if (r.enabled) {
hasEnabled = true
}
listedRoutes.push(r)
}
})
let groupDataTableRoutes = transformDataTable(listedRoutes,peerIPToName)
groupedRoutes.push({
key: p.toString(),
network_id: lastRoute!.network_id,
network: lastRoute!.network,
masquerade: lastRoute!.masquerade,
description: lastRoute!.description,
enabled: hasEnabled,
routesCount: groupDataTableRoutes.length,
groupedRoutes: groupDataTableRoutes,
})
})
return groupedRoutes
}

View File

@@ -126,7 +126,7 @@ export const AccessControl = () => {
if (savedRule.loading) {
message.loading({ content: 'Saving...', key: saveKey, duration: 0, style: styleNotification })
} else if (savedRule.success) {
message.success({ content: 'Rule has been successfully updated.', key: saveKey, duration: 2, style: styleNotification });
message.success({ content: 'Rule has been successfully saved.', key: saveKey, duration: 2, style: styleNotification });
dispatch(ruleActions.setSetupNewRuleVisible(false))
dispatch(ruleActions.setSavedRule({ ...savedRule, success: false }))
dispatch(ruleActions.resetSavedRule(null))

View File

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

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

@@ -0,0 +1,432 @@
import React, {useEffect, useState} from 'react';
import {
Alert,
Button, Card,
Col, Dropdown, Input, Menu, message, Modal, Radio, RadioChangeEvent,
Row, Select, Space, Switch, Table, Tag, Tooltip, Typography, Divider
} 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';
import {
masqueradeDisabledMSG,masqueradeEnabledMSG,
peerToPeerIP,initPeerMaps,
RouteDataTable,GroupedDataTable,
transformGroupedDataTable,transformDataTable
} from '../utils/routes'
const { Title, Paragraph } = Typography;
const { Column } = Table;
const { confirm } = Modal;
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 loadingPeer = useSelector((state: RootState) => state.peer.loading);
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 [groupedDataTable, setGroupedDataTable] = useState([] as GroupedDataTable[]);
const [expandRowsOnClick,setExpandRowsOnClick] = useState(true)
const [peerNameToIP, peerIPToName] = initPeerMaps(peers);
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"))
}
useEffect(() => {
dispatch(routeActions.getRoutes.request({getAccessTokenSilently:accessToken, payload: null}));
}, [peers])
useEffect(() => {
dispatch(peerActions.getPeers.request({getAccessTokenSilently:accessToken, payload: null}));
}, [])
const filterGroupedDataTable = (routes:GroupedDataTable[]):GroupedDataTable[] => {
const t = textToSearch.toLowerCase().trim()
let f:GroupedDataTable[] = filter(routes, (f) =>
(f.network_id.toLowerCase().includes(t) ||f.network.toLowerCase().includes(t) || f.description.toLowerCase().includes(t) || t === "")
) as GroupedDataTable[]
if (optionAllEnable !== "all") {
f = filter(f, (f) => f.enabled)
}
return f
}
useEffect(() =>{
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes,peerIPToName)))
},[dataTable])
useEffect(() => {
setShowTutorial(isShowTutorial(routes))
setDataTable(sortBy(transformDataTable(routes,peerIPToName), "network_id"))
}, [routes])
useEffect(() => {
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes,peerIPToName)))
}, [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 = () => {
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes,peerIPToName)))
}
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 onClickAddNewRoute = () => {
dispatch(routeActions.setSetupNewRouteHA(true));
dispatch(routeActions.setSetupNewRouteVisible(true));
dispatch(routeActions.setRoute({
network: '',
network_id: '',
description: '',
peer: '',
masquerade: true,
metric: 9999,
enabled: true
} as Route))
}
const onClickViewRoute = () => {
dispatch(routeActions.setSetupNewRouteHA(false));
dispatch(routeActions.setSetupNewRouteVisible(true));
dispatch(routeActions.setRoute({
id: routeToAction?.id || null,
network: routeToAction?.network,
network_id: routeToAction?.network_id,
description: routeToAction?.description,
peer: peerToPeerIP(routeToAction!.peer,peerNameToIP[routeToAction!.peer]),
metric: routeToAction?.metric,
masquerade: routeToAction?.masquerade,
enabled: routeToAction?.enabled
} as Route))
}
const setRouteAndView = (route: RouteDataTable) => {
if (!route.id) {
dispatch(routeActions.setSetupNewRouteHA(true));
}
dispatch(routeActions.setRoute({
id: route.id || null,
network: route.network,
network_id: route.network_id,
description: route.description,
peer: route.peer? peerToPeerIP(route.peer,peerNameToIP[route.peer]) : '',
metric: route.metric? route.metric : 9999,
masquerade: route.masquerade,
enabled: route.enabled
} as Route))
dispatch(routeActions.setSetupNewRouteVisible(true));
}
const showConfirmEnableMasquerade = (record: GroupedDataTable, checked: boolean) => {
let label = record.network_id ? record.network_id : record.network
let tittle = "Enable Masquerade for \"" + label + "\"?"
let content = masqueradeDisabledMSG
if (!checked) {
tittle = "Disable Masquerade for \"" + label + "\"?"
content = masqueradeEnabledMSG
}
confirm({
icon: <ExclamationCircleOutlined />,
title: tittle,
width: 600,
content: content,
okType: 'danger',
onOk() {
handleSwitchMasquerade(record, checked)
},
onCancel() {
},
});
}
function handleSwitchMasquerade(routeGroup: GroupedDataTable, checked: boolean) {
routeGroup.groupedRoutes.forEach((record) => {
const route = {
...record,
peer: peerNameToIP[record.peer],
masquerade: checked,
} as Route
dispatch(routeActions.saveRoute.request({getAccessTokenSilently:accessToken, payload: route}));
})
}
const expandedRowRender = (record: GroupedDataTable) => {
return <Table
dataSource={record.groupedRoutes}
rowKey="id"
pagination={false}
showHeader={true}
tableLayout="fixed"
size="small"
bordered={true}
>
<Column title="Routing Peer" dataIndex="peer" align="center"
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={(text, record) => {
return <span onClick={() => setRouteAndView(record as RouteDataTable)} className="tooltip-label">{text}</span>
}}
/>
<Column title="Metric" dataIndex="metric" align="center"
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="Status" dataIndex="enabled" align="center"
render={(text:Boolean) => {
return text ? <Tag color="green">enabled</Tag> : <Tag color="red">disabled</Tag>
}}
/>
<Column title="" align="center"
render={(text, record) => {
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>
};
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) => {
setCurrentPage(page)
}
}}
className={`access-control-table ${showTutorial ? "card-table card-table-no-placeholder" : "card-table"}`}
showSorterTooltip={false}
scroll={{x: true}}
loading={tableSpin(loading || loadingPeer)}
dataSource={groupedDataTable}
expandable={{
expandedRowRender,
expandRowByClick: expandRowsOnClick,
onExpandedRowsChange: (r) => {setExpandRowsOnClick((!r.length))},
}}
>
<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' align="center"
sorter={(a, b) => ((a as any).network_id.localeCompare((b as any).network_id))}
render={(text, record) => {
const desc = (record as RouteDataTable).description.trim()
return <Tooltip title={desc !== "" ? desc : "no description"} arrowPointAtCenter>{text}</Tooltip>
}}
/>
<Column title="Network Range" dataIndex="network" align="center"
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="Status" dataIndex="enabled" align="center"
render={(text:Boolean) => {
return text ? <Tag color="green">enabled</Tag> : <Tag color="red">disabled</Tag>
}}
/>
<Column title="Masquerade Traffic" dataIndex="masquerade" align="center"
render={(e, record: GroupedDataTable) => {
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="High Availability" align="center" dataIndex="routesCount"
render={(count, record: RouteDataTable) => {
let tag = <Tag color="red">off</Tag>
if (count > 1) {
tag = <Tag color="green">on</Tag>
}
return <div>{tag}<Divider type="vertical" /><Button type="link" onClick={() => setRouteAndView(record)}>Configure</Button></div>
}}
/>
</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;

View File

@@ -1,38 +1,46 @@
import React, {useEffect, useState} from 'react';
import {useDispatch, useSelector} from "react-redux";
import { RootState } from "typesafe-actions";
import { actions as setupKeyActions } from '../store/setup-key';
import {RootState} from "typesafe-actions";
import {actions as setupKeyActions} from '../store/setup-key';
import {Container} from "../components/Container";
import {useOidcAccessToken} from '@axa-fr/react-oidc';
import {
Col,
Row,
Typography,
Table,
Alert,
Button,
Card,
Tag,
Col,
Dropdown,
Input,
Space,
Menu,
message,
Modal, Popover,
Radio,
RadioChangeEvent,
Dropdown,
Menu,
Alert, Select, Modal, Button, message, Drawer, Form, List
Row,
Select,
Space,
Table,
Tag,
Typography
} from "antd";
import {SetupKey, SetupKeyRevoke} from "../store/setup-key/types";
import {SetupKey, SetupKeyToSave} from "../store/setup-key/types";
import {filter} from "lodash"
import {formatDate, timeAgo} from "../utils/common";
import {ExclamationCircleOutlined} from "@ant-design/icons";
import SetupKeyNew from "../components/SetupKeyNew";
import ButtonCopyMessage from "../components/ButtonCopyMessage";
import tableSpin from "../components/Spin";
import {actions as groupActions} from "../store/group";
import {Group} from "../store/group/types";
import {TooltipPlacement} from "antd/es/tooltip";
const { Title, Text, Paragraph } = Typography;
const { Column } = Table;
const { confirm } = Modal;
const {Title, Text, Paragraph} = Typography;
const {Column} = Table;
const {confirm} = Modal;
interface SetupKeyDataTable extends SetupKey {
key: string
groupsCount: number
}
export const SetupKeys = () => {
@@ -43,16 +51,16 @@ export const SetupKeys = () => {
const failed = useSelector((state: RootState) => state.setupKey.failed);
const loading = useSelector((state: RootState) => state.setupKey.loading);
const deletedSetupKey = useSelector((state: RootState) => state.setupKey.deletedSetupKey);
const revokedSetupKey = useSelector((state: RootState) => state.setupKey.revokedSetupKey);
const createdSetupKey = useSelector((state: RootState) => state.setupKey.createdSetupKey);
const savedSetupKey = useSelector((state: RootState) => state.setupKey.savedSetupKey);
const groups = useSelector((state: RootState) => state.group.data)
const [textToSearch, setTextToSearch] = useState('');
const [optionValidAll, setOptionValidAll] = useState('valid');
const [pageSize, setPageSize] = useState(5);
const [pageSize, setPageSize] = useState(10);
const [dataTable, setDataTable] = useState([] as SetupKeyDataTable[]);
const [setupKeyToAction, setSetupKeyToAction] = useState(null as SetupKeyDataTable | null);
const styleNotification = { marginTop: 85 }
const styleNotification = {marginTop: 85}
const pageSizeOptions = [
{label: "5", value: "5"},
@@ -67,19 +75,21 @@ export const SetupKeys = () => {
key: "revoke",
label: (<Button type="text" onClick={() => showConfirmRevoke()}>Revoke</Button>)
},
/*{
key: "delete",
label: (<Button type="text" onClick={() => showConfirmDelete()}>Delete</Button>)
}*/
]
const actionsMenu = (<Menu items={itemsMenuAction} ></Menu>)
{
key: "edit",
label: (<Button type="text" onClick={() => onClickEditSetupKey()}>View</Button>)
},
const transformDataTable = (d:SetupKey[]):SetupKeyDataTable[] => {
return d.map(p => ({ ...p } as SetupKeyDataTable))
]
const actionsMenu = (<Menu items={itemsMenuAction}></Menu>)
const transformDataTable = (d: SetupKey[]): SetupKeyDataTable[] => {
return d.map(p => ({...p, groupsCount: p.auto_groups ? p.auto_groups.length : 0} as SetupKeyDataTable))
}
useEffect(() => {
dispatch(setupKeyActions.getSetupKeys.request({getAccessTokenSilently:accessToken, payload: null}));
dispatch(setupKeyActions.getSetupKeys.request({getAccessTokenSilently: accessToken, payload: null}));
dispatch(groupActions.getGroups.request({getAccessTokenSilently: accessToken, payload: null}));
}, [])
useEffect(() => {
@@ -93,52 +103,61 @@ export const SetupKeys = () => {
const deleteKey = 'deleting';
useEffect(() => {
if (deletedSetupKey.loading) {
message.loading({ content: 'Deleting...', key: deleteKey, style: styleNotification });
message.loading({content: 'Deleting...', key: deleteKey, style: styleNotification});
} else if (deletedSetupKey.success) {
message.success({ content: 'Setup key has been successfully removed.', key: deleteKey, duration: 2, style: styleNotification });
dispatch(setupKeyActions.setDeleteSetupKey({ ...deletedSetupKey, success: false }))
message.success({
content: 'Setup key has been successfully removed.',
key: deleteKey,
duration: 2,
style: styleNotification
});
dispatch(setupKeyActions.setDeleteSetupKey({...deletedSetupKey, success: false}))
dispatch(setupKeyActions.resetDeletedSetupKey(null))
} else if (deletedSetupKey.error) {
message.error({ content: 'Failed to delete setup key. You might not have enough permissions.', key: deleteKey, duration: 2, style: styleNotification });
dispatch(setupKeyActions.setDeleteSetupKey({ ...deletedSetupKey, error: null }))
message.error({
content: 'Failed to delete setup key. You might not have enough permissions.',
key: deleteKey,
duration: 2,
style: styleNotification
});
dispatch(setupKeyActions.setDeleteSetupKey({...deletedSetupKey, error: null}))
dispatch(setupKeyActions.resetDeletedSetupKey(null))
}
}, [deletedSetupKey])
const revokeKey = 'revoking';
const createKey = 'saving';
useEffect(() => {
if (revokedSetupKey.loading) {
message.loading({ content: 'Revoking...', key: revokeKey, duration: 0, style: styleNotification })
} else if (revokedSetupKey.success) {
message.success({ content: 'Setup key has been successfully revoked.', key: revokeKey, duration: 2, style: styleNotification });
dispatch(setupKeyActions.resetRevokedSetupKey(null))
} else if (revokedSetupKey.error) {
message.error({ content: 'Failed to revoke setup key. You might not have enough permissions.', key: revokeKey, duration: 2, style: styleNotification });
dispatch(setupKeyActions.resetRevokedSetupKey(null))
}
}, [revokedSetupKey])
const createKey = 'creating';
useEffect(() => {
if (createdSetupKey.loading) {
message.loading({ content: 'Creating...', key: createKey, duration: 0, style: styleNotification });
} else if (createdSetupKey.success) {
message.success({ content: 'Setup key has been successfully created.', key: createKey, duration: 2, style: styleNotification });
if (savedSetupKey.loading) {
message.loading({content: 'Saving...', key: createKey, duration: 0, style: styleNotification});
} else if (savedSetupKey.success) {
message.success({
content: 'Setup key has been successfully saved.',
key: createKey,
duration: 2,
style: styleNotification
});
dispatch(setupKeyActions.setSetupNewKeyVisible(false));
dispatch(setupKeyActions.setCreateSetupKey({ ...createdSetupKey, success: false }));
} else if (createdSetupKey.error) {
message.error({ content: 'Failed to create setup key. You might not have enough permissions.', key: createKey, duration: 2, style: styleNotification });
dispatch(setupKeyActions.setCreateSetupKey({ ...createdSetupKey, error: null }));
dispatch(setupKeyActions.setSavedSetupKey({...savedSetupKey, success: false}));
dispatch(setupKeyActions.resetSavedSetupKey(null))
} else if (savedSetupKey.error) {
message.error({
content: 'Failed to update setup key. You might not have enough permissions.',
key: createKey,
duration: 2,
style: styleNotification
});
dispatch(setupKeyActions.setSavedSetupKey({...savedSetupKey, error: null}));
dispatch(setupKeyActions.resetSavedSetupKey(null))
}
}, [createdSetupKey])
}, [savedSetupKey])
const filterDataTable = ():SetupKey[] => {
const filterDataTable = (): SetupKey[] => {
const t = textToSearch.toLowerCase().trim()
let f:SetupKey[] = [...setupKeys]
let f: SetupKey[] = [...setupKeys]
if (optionValidAll === "valid") {
f = filter(setupKeys, (_f:SetupKey) => _f.valid && !_f.revoked)
f = filter(setupKeys, (_f: SetupKey) => _f.valid && !_f.revoked)
}
f = filter(f, (_f:SetupKey) =>
f = filter(f, (_f: SetupKey) =>
(_f.name.toLowerCase().includes(t) || _f.state.includes(t) || _f.type.toLowerCase().includes(t) || _f.key.toLowerCase().includes(t) || t === "")
) as SetupKey[]
return f
@@ -153,7 +172,7 @@ export const SetupKeys = () => {
setDataTable(transformDataTable(data))
}
const onChangeValidAll = ({ target: { value } }: RadioChangeEvent) => {
const onChangeValidAll = ({target: {value}}: RadioChangeEvent) => {
setOptionValidAll(value)
}
@@ -163,7 +182,7 @@ export const SetupKeys = () => {
const showConfirmDelete = () => {
confirm({
icon: <ExclamationCircleOutlined />,
icon: <ExclamationCircleOutlined/>,
width: 600,
content: <Space direction="vertical" size="small">
{setupKeyToAction &&
@@ -175,7 +194,10 @@ export const SetupKeys = () => {
</Space>,
okType: 'danger',
onOk() {
dispatch(setupKeyActions.deleteSetupKey.request({getAccessTokenSilently:accessToken, payload: setupKeyToAction ? setupKeyToAction.id : ''}));
dispatch(setupKeyActions.deleteSetupKey.request({
getAccessTokenSilently: accessToken,
payload: setupKeyToAction ? setupKeyToAction.id : ''
}));
},
onCancel() {
setSetupKeyToAction(null);
@@ -185,7 +207,7 @@ export const SetupKeys = () => {
const showConfirmRevoke = () => {
confirm({
icon: <ExclamationCircleOutlined />,
icon: <ExclamationCircleOutlined/>,
width: 600,
content: <Space direction="vertical" size="small">
{setupKeyToAction &&
@@ -197,7 +219,15 @@ export const SetupKeys = () => {
</Space>,
okType: 'danger',
onOk() {
dispatch(setupKeyActions.revokeSetupKey.request({getAccessTokenSilently:accessToken, payload: { id: setupKeyToAction ? setupKeyToAction.id : null,revoked: true } as SetupKeyRevoke}));
dispatch(setupKeyActions.saveSetupKey.request({
getAccessTokenSilently: accessToken,
payload: {
id: setupKeyToAction ? setupKeyToAction.id : null,
revoked: true,
name: setupKeyToAction ? setupKeyToAction.name : null,
auto_groups: setupKeyToAction && setupKeyToAction.auto_groups ? setupKeyToAction.auto_groups : [],
} as SetupKeyToSave
}));
},
onCancel() {
setSetupKeyToAction(null);
@@ -206,25 +236,124 @@ export const SetupKeys = () => {
}
const onClickAddNewSetupKey = () => {
const autoGroups : string[] = []
dispatch(setupKeyActions.setSetupNewKeyVisible(true));
dispatch(setupKeyActions.setSetupKey({
name: '',
type: 'reusable'
name: "",
type: "reusable",
auto_groups: autoGroups
} as SetupKey))
}
const setKeyAndView = (key: SetupKeyDataTable) => {
dispatch(setupKeyActions.setSetupNewKeyVisible(true));
dispatch(setupKeyActions.setSetupKey({
id: key?.id || null,
key: key?.key,
name: key?.name,
revoked: key?.revoked,
expires: key?.expires,
state: key?.state,
type: key?.type,
used_times: key?.used_times,
valid: key?.valid,
auto_groups: key?.auto_groups,
last_used: key?.last_used,
} as SetupKey))
}
const onClickEditSetupKey = () => {
dispatch(setupKeyActions.setSetupNewKeyVisible(true));
dispatch(setupKeyActions.setSetupKey({
id: setupKeyToAction?.id || null,
key: setupKeyToAction?.key,
name: setupKeyToAction?.name,
revoked: setupKeyToAction?.revoked,
expires: setupKeyToAction?.expires,
state: setupKeyToAction?.state,
type: setupKeyToAction?.type,
used_times: setupKeyToAction?.used_times,
valid: setupKeyToAction?.valid,
auto_groups: setupKeyToAction?.auto_groups,
last_used: setupKeyToAction?.last_used,
} as SetupKey))
}
const renderPopoverGroups = (label: string, rowGroups: string[] | string[] | null, setupKeyToAction: SetupKeyDataTable) => {
let groupsMap = new Map<string, Group>();
groups.forEach(g => {
groupsMap.set(g.id!, g)
})
let displayGroups :Group[] = []
if (rowGroups) {
displayGroups = rowGroups.filter(g => groupsMap.get(g)).map(g => groupsMap.get(g)!)
}
let btn = <Button type="link" onClick={() => setUpdateGroupsVisible(setupKeyToAction, true)}>{displayGroups.length}</Button>
if (!displayGroups || displayGroups!.length < 1) {
return btn
}
const content = displayGroups?.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}}
>
<strong>{_g.name}</strong>
</Tag>
<span style={{fontSize: ".85em"}}>{peersCount}</span>
</div>
)
})
const mainContent = (<Space direction="vertical">{content}</Space>)
let popoverPlacement = "top"
if (content && content.length > 5) {
popoverPlacement = "rightTop"
}
return (
<Popover placement={popoverPlacement as TooltipPlacement} key={setupKeyToAction.key} content={mainContent}
title={null}>
{btn}
</Popover>
)
}
const setUpdateGroupsVisible = (setupKeyToAction: SetupKey, status: boolean) => {
if (status) {
dispatch(setupKeyActions.setSetupKey({...setupKeyToAction}))
dispatch(setupKeyActions.setSetupNewKeyVisible(true))
return
}
const autoGroups : string[] = []
dispatch(setupKeyActions.setSetupKey({
name: "",
type: "reusable",
auto_groups: autoGroups
} as SetupKey))
dispatch(setupKeyActions.setSetupNewKeyVisible(false))
}
return (
<>
<Container style={{paddingTop: "40px"}}>
<Row>
<Col span={24}>
<Title level={4}>Setup Keys</Title>
<Paragraph>A list of all the setup keys in your account including their name, state, type and expiration.</Paragraph>
<Space direction="vertical" size="large" style={{ display: 'flex' }}>
<Paragraph>A list of all the setup keys in your account including their name, state, type and
expiration.</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">
@@ -235,7 +364,8 @@ export const SetupKeys = () => {
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}
@@ -252,11 +382,16 @@ export const SetupKeys = () => {
</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/>
}
<Card bodyStyle={{padding: 0}}>
<Table
pagination={{pageSize, showSizeChanger: false, showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} setup keys`)}}
pagination={{
pageSize,
showSizeChanger: false,
showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} setup keys`)
}}
className="card-table"
showSorterTooltip={false}
scroll={{x: true}}
@@ -265,12 +400,18 @@ export const SetupKeys = () => {
<Column title="Name" dataIndex="name"
onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
sorter={(a, b) => ((a as any).name.localeCompare((b as any).name))}
render={(text, record, index) => {
return <Button type="text"
onClick={() => setKeyAndView(record as SetupKeyDataTable)}
className="tooltip-label">{text}</Button>
}}
defaultSortOrder='ascend'
/>
<Column title="State" dataIndex="state"
render={(text, record, index) => {
return (text === 'valid') ? <Tag color="green">{text}</Tag> : <Tag color="red">{text}</Tag>
return (text === 'valid') ? <Tag color="green">{text}</Tag> :
<Tag color="red">{text}</Tag>
}}
sorter={(a, b) => ((a as any).state.localeCompare((b as any).state))}
/>
@@ -279,19 +420,25 @@ export const SetupKeys = () => {
onFilter={(value: string | number | boolean, record) => (record as any).type.includes(value)}
sorter={(a, b) => ((a as any).type.localeCompare((b as any).type))}
/>
<Column title="Groups" dataIndex="groupsCount" align="center"
render={(text, record: SetupKeyDataTable, index) => {
return renderPopoverGroups(text, record.auto_groups, record)
}}
/>
<Column title="Key" dataIndex="key"
onFilter={(value: string | number | boolean, record) => (record as any).key.includes(value)}
sorter={(a, b) => ((a as any).key.localeCompare((b as any).key))}
render={(text, record, index) => {
return <ButtonCopyMessage keyMessage={(record as SetupKeyDataTable).key} text={text} messageText={`Key copied!`} styleNotification={{}}/>
return <ButtonCopyMessage keyMessage={(record as SetupKeyDataTable).key}
text={text} messageText={`Key copied!`}
styleNotification={{}}/>
}}
/>
<Column title="Last Used" dataIndex="last_used"
sorter={(a, b) => ((a as any).last_used.localeCompare((b as any).last_used))}
render={(text, record, index) => {
return !(record as SetupKey).used_times ? 'unused' : timeAgo(text)
return !(record as SetupKey).used_times ? 'never' : timeAgo(text)
}}
/>
<Column title="Used Times" dataIndex="used_times"
@@ -307,10 +454,11 @@ export const SetupKeys = () => {
<Column title="" align="center"
render={(text, record, index) => {
return !(record as SetupKeyDataTable).revoked ? (
<Dropdown.Button type="text" overlay={actionsMenu} trigger={["click"]}
onVisibleChange={visible => {
if (visible) setSetupKeyToAction(record as SetupKeyDataTable)
}}></Dropdown.Button>) : <></>
<Dropdown.Button type="text" overlay={actionsMenu}
trigger={["click"]}
onVisibleChange={visible => {
if (visible) setSetupKeyToAction(record as SetupKeyDataTable)
}}></Dropdown.Button>) : <></>
}}
/>
</Table>