Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
835bb37ab9 | ||
|
|
a944dc8ab0 | ||
|
|
b2c51533fb | ||
|
|
fd24536926 | ||
|
|
8e8484cd45 | ||
|
|
6c87f53195 | ||
|
|
9bbbff7dc0 | ||
|
|
04a20fa31f | ||
|
|
3797db93f0 | ||
|
|
2e81765e85 | ||
|
|
cb9f76c0fc | ||
|
|
54accb665c | ||
|
|
cfea3bd489 | ||
|
|
a44a1c5424 | ||
|
|
c2c044421f | ||
|
|
e8d57c3445 | ||
|
|
0b892c0056 | ||
|
|
14d9b80029 | ||
|
|
dedbe55308 | ||
|
|
796a06cf27 | ||
|
|
2443c6332d | ||
|
|
3b5193ae4e | ||
|
|
cf42dd52fc | ||
|
|
bc6842e5b5 | ||
|
|
a8ed755dda | ||
|
|
a87c06ef52 | ||
|
|
c0130d265c | ||
|
|
63ced3088a | ||
|
|
42b7a15466 | ||
|
|
c88bfa6476 | ||
|
|
c4138a8c45 | ||
|
|
3f5418bebc | ||
|
|
f60605e5e3 | ||
|
|
c1b2ededa7 | ||
|
|
31eaa4a241 | ||
|
|
ba365336ff | ||
|
|
06c238b013 | ||
|
|
ed56c240f3 | ||
|
|
1f3c7d87d7 | ||
|
|
9de2906fb2 | ||
|
|
359b443326 | ||
|
|
ecae39d94b | ||
|
|
30b858c1bc | ||
|
|
e82b269ff5 | ||
|
|
23a4b79f01 | ||
|
|
cc5a9b1033 | ||
|
|
09ae157be3 | ||
|
|
cb89eeb921 | ||
|
|
79446c0e77 | ||
|
|
1f258e4e2c | ||
|
|
bae95d2e11 | ||
|
|
3d95a826e7 | ||
|
|
d8d13aff01 | ||
|
|
695b571a50 | ||
|
|
ddfb6a6179 | ||
|
|
8c94119c6a | ||
|
|
439e803ef2 | ||
|
|
440c51a35c | ||
|
|
800e94de85 | ||
|
|
ba7d138156 | ||
|
|
a66fb3bf8f | ||
|
|
fcc51243e0 | ||
|
|
312c60dd45 | ||
|
|
09e6de74ee | ||
|
|
addd348456 | ||
|
|
a8190bfe5b | ||
|
|
9e3d9f245d | ||
|
|
e7a7a75906 | ||
|
|
67efd47f22 | ||
|
|
813cd851ca | ||
|
|
f44ccf3ef7 | ||
|
|
899f56acdc | ||
|
|
dc2760d5ff | ||
|
|
5ae4fd31f1 | ||
|
|
54d9d7c768 | ||
|
|
3a73781fca | ||
|
|
f3c5d4df6a | ||
|
|
b4c9135c58 | ||
|
|
a280d9c67c | ||
|
|
5caf06b086 | ||
|
|
95af1c4e32 | ||
|
|
d247e7f3ed | ||
|
|
391e534253 | ||
|
|
a7f64d4a15 | ||
|
|
53ed514803 | ||
|
|
6cadce1598 | ||
|
|
c03d4b8a4b | ||
|
|
bc1053fbb4 | ||
|
|
d154bfb799 | ||
|
|
67fd2fcb2e | ||
|
|
f4933e45ee |
2
.github/workflows/build_and_push.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependecies
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Build
|
||||
|
||||
42
.github/workflows/e2e-tests.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: run e2e tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
e2e_tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: setup-node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: install playwright
|
||||
run: npx playwright install
|
||||
|
||||
- name: install playwright deps
|
||||
run: npx playwright install-deps
|
||||
|
||||
- name: create test environment
|
||||
run: bash ./e2e-tests/create-test-env.sh
|
||||
|
||||
- name: run e2e tests
|
||||
run: npx playwright test --workers 2
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: |
|
||||
playwright-report/
|
||||
test-results/
|
||||
retention-days: 3
|
||||
14
.gitignore
vendored
@@ -26,6 +26,18 @@ yarn-error.log*
|
||||
src/auth_config.json
|
||||
.idea
|
||||
.eslintcache
|
||||
src/.local-config.json
|
||||
src/.local-config*.json
|
||||
/public/OidcServiceWorker.js
|
||||
/public/OidcTrustedDomains.js
|
||||
/e2e-tests/node_modules/
|
||||
/e2e-tests/playwright-report/
|
||||
/e2e-tests/test-results/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
.env
|
||||
Caddyfile
|
||||
docker-compose.yml
|
||||
machinekey/
|
||||
management.json
|
||||
turnserver.conf
|
||||
zitadel.env
|
||||
|
||||
@@ -22,6 +22,10 @@ if [[ -z "${AUTH_AUDIENCE}" ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${AUTH_AUDIENCE}" == "none" ]]; then
|
||||
unset AUTH_AUDIENCE
|
||||
fi
|
||||
|
||||
if [[ -z "${AUTH_SUPPORTED_SCOPES}" ]]; then
|
||||
if [[ -z "${AUTH0_DOMAIN}" ]]; then
|
||||
echo "AUTH_SUPPORTED_SCOPES environment variable must be set"
|
||||
@@ -43,6 +47,7 @@ fi
|
||||
|
||||
export AUTH_AUTHORITY=${AUTH_AUTHORITY:-https://$AUTH0_DOMAIN}
|
||||
export AUTH_CLIENT_ID=${AUTH_CLIENT_ID:-$AUTH0_CLIENT_ID}
|
||||
export AUTH_CLIENT_SECRET=${AUTH_CLIENT_SECRET}
|
||||
export AUTH_AUDIENCE=${AUTH_AUDIENCE:-$AUTH0_AUDIENCE}
|
||||
export AUTH_REDIRECT_URI=${AUTH_REDIRECT_URI}
|
||||
export AUTH_SILENT_REDIRECT_URI=${AUTH_SILENT_REDIRECT_URI}
|
||||
@@ -53,11 +58,12 @@ export NETBIRD_MGMT_API_ENDPOINT=$(echo $NETBIRD_MGMT_API_ENDPOINT | sed -E 's/(
|
||||
export NETBIRD_MGMT_GRPC_API_ENDPOINT=${NETBIRD_MGMT_GRPC_API_ENDPOINT}
|
||||
export NETBIRD_HOTJAR_TRACK_ID=${NETBIRD_HOTJAR_TRACK_ID}
|
||||
export NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken}
|
||||
export NETBIRD_DRAG_QUERY_PARAMS=${NETBIRD_DRAG_QUERY_PARAMS:-false}
|
||||
|
||||
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
|
||||
|
||||
# replace ENVs in the config
|
||||
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE"
|
||||
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_CLIENT_SECRET \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE \$\$NETBIRD_DRAG_QUERY_PARAMS"
|
||||
|
||||
MAIN_JS=$(find /usr/share/nginx/html/static/js/main.*js)
|
||||
OIDC_TRUSTED_DOMAINS="/usr/share/nginx/html/OidcTrustedDomains.js"
|
||||
|
||||
3
e2e-tests/clean-test-env.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
docker-compose down --volumes
|
||||
rm -f docker-compose.yml Caddyfile zitadel.env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json
|
||||
697
e2e-tests/create-test-env.sh
Normal file
@@ -0,0 +1,697 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
handle_request_command_status() {
|
||||
PARSED_RESPONSE=$1
|
||||
FUNCTION_NAME=$2
|
||||
RESPONSE=$3
|
||||
if [[ $PARSED_RESPONSE -ne 0 ]]; then
|
||||
echo "ERROR calling $FUNCTION_NAME:" $(echo "$RESPONSE" | jq -r '.message') > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
handle_zitadel_request_response() {
|
||||
PARSED_RESPONSE=$1
|
||||
FUNCTION_NAME=$2
|
||||
RESPONSE=$3
|
||||
if [[ $PARSED_RESPONSE == "null" ]]; then
|
||||
echo "ERROR calling $FUNCTION_NAME:" $(echo "$RESPONSE" | jq -r '.message') > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
}
|
||||
|
||||
check_docker_compose() {
|
||||
if command -v docker-compose &> /dev/null
|
||||
then
|
||||
echo "docker-compose"
|
||||
return
|
||||
fi
|
||||
if docker compose --help &> /dev/null
|
||||
then
|
||||
echo "docker compose"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "docker-compose is not installed or not in PATH. Please follow the steps from the official guide: https://docs.docker.com/engine/install/" > /dev/stderr
|
||||
exit 1
|
||||
}
|
||||
|
||||
check_jq() {
|
||||
if ! command -v jq &> /dev/null
|
||||
then
|
||||
echo "jq is not installed or not in PATH, please install with your package manager. e.g. sudo apt install jq" > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
wait_crdb() {
|
||||
set +e
|
||||
while true; do
|
||||
if $DOCKER_COMPOSE_COMMAND exec -T crdb curl -sf -o /dev/null 'http://localhost:8080/health?ready=1'; then
|
||||
break
|
||||
fi
|
||||
echo -n " ."
|
||||
sleep 5
|
||||
done
|
||||
echo " done"
|
||||
set -e
|
||||
}
|
||||
|
||||
init_crdb() {
|
||||
echo -e "\nInitializing Zitadel's CockroachDB\n\n"
|
||||
$DOCKER_COMPOSE_COMMAND up -d crdb
|
||||
echo ""
|
||||
# shellcheck disable=SC2028
|
||||
echo -n "Waiting cockroachDB to become ready "
|
||||
wait_crdb
|
||||
$DOCKER_COMPOSE_COMMAND exec -T crdb /bin/bash -c "cp /cockroach/certs/* /zitadel-certs/ && cockroach cert create-client --overwrite --certs-dir /zitadel-certs/ --ca-key /zitadel-certs/ca.key zitadel_user && chown -R 1000:1000 /zitadel-certs/"
|
||||
handle_request_command_status $? "init_crdb failed" ""
|
||||
}
|
||||
|
||||
get_main_ip_address() {
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
interface=$(route -n get default | grep 'interface:' | awk '{print $2}')
|
||||
ip_address=$(ifconfig "$interface" | grep 'inet ' | awk '{print $2}')
|
||||
else
|
||||
interface=$(ip route | grep default | awk '{print $5}' | head -n 1)
|
||||
ip_address=$(ip addr show "$interface" | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1)
|
||||
fi
|
||||
|
||||
echo "$ip_address"
|
||||
}
|
||||
|
||||
wait_pat() {
|
||||
PAT_PATH=$1
|
||||
set +e
|
||||
while true; do
|
||||
if [[ -f "$PAT_PATH" ]]; then
|
||||
break
|
||||
fi
|
||||
echo -n " ."
|
||||
sleep 1
|
||||
done
|
||||
echo " done"
|
||||
set -e
|
||||
}
|
||||
|
||||
wait_api() {
|
||||
INSTANCE_URL=$1
|
||||
PAT=$2
|
||||
set +e
|
||||
while true; do
|
||||
curl -s --fail -o /dev/null "$INSTANCE_URL/auth/v1/users/me" -H "Authorization: Bearer $PAT"
|
||||
if [[ $? -eq 0 ]]; then
|
||||
break
|
||||
fi
|
||||
echo -n " ."
|
||||
sleep 1
|
||||
done
|
||||
echo " done"
|
||||
set -e
|
||||
}
|
||||
|
||||
create_new_project() {
|
||||
INSTANCE_URL=$1
|
||||
PAT=$2
|
||||
PROJECT_NAME="NETBIRD"
|
||||
|
||||
RESPONSE=$(
|
||||
curl -sS -X POST "$INSTANCE_URL/management/v1/projects" \
|
||||
-H "Authorization: Bearer $PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "'"$PROJECT_NAME"'"}'
|
||||
)
|
||||
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.id')
|
||||
handle_zitadel_request_response "$PARSED_RESPONSE" "create_new_project" "$RESPONSE"
|
||||
echo "$PARSED_RESPONSE"
|
||||
}
|
||||
|
||||
create_new_application() {
|
||||
INSTANCE_URL=$1
|
||||
PAT=$2
|
||||
APPLICATION_NAME=$3
|
||||
BASE_REDIRECT_URL1=$4
|
||||
BASE_REDIRECT_URL2=$5
|
||||
LOGOUT_URL=$6
|
||||
ZITADEL_DEV_MODE=$7
|
||||
|
||||
RESPONSE=$(
|
||||
curl -sS -X POST "$INSTANCE_URL/management/v1/projects/$PROJECT_ID/apps/oidc" \
|
||||
-H "Authorization: Bearer $PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "'"$APPLICATION_NAME"'",
|
||||
"redirectUris": [
|
||||
"'"$BASE_REDIRECT_URL1"'",
|
||||
"'"$BASE_REDIRECT_URL2"'"
|
||||
],
|
||||
"postLogoutRedirectUris": [
|
||||
"'"$LOGOUT_URL"'"
|
||||
],
|
||||
"RESPONSETypes": [
|
||||
"OIDC_RESPONSE_TYPE_CODE"
|
||||
],
|
||||
"grantTypes": [
|
||||
"OIDC_GRANT_TYPE_AUTHORIZATION_CODE",
|
||||
"OIDC_GRANT_TYPE_REFRESH_TOKEN"
|
||||
],
|
||||
"appType": "OIDC_APP_TYPE_USER_AGENT",
|
||||
"authMethodType": "OIDC_AUTH_METHOD_TYPE_NONE",
|
||||
"version": "OIDC_VERSION_1_0",
|
||||
"devMode": '"$ZITADEL_DEV_MODE"',
|
||||
"accessTokenType": "OIDC_TOKEN_TYPE_JWT",
|
||||
"accessTokenRoleAssertion": true,
|
||||
"skipNativeAppSuccessPage": true
|
||||
}'
|
||||
)
|
||||
|
||||
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.clientId')
|
||||
handle_zitadel_request_response "$PARSED_RESPONSE" "create_new_application" "$RESPONSE"
|
||||
echo "$PARSED_RESPONSE"
|
||||
}
|
||||
|
||||
create_service_user() {
|
||||
INSTANCE_URL=$1
|
||||
PAT=$2
|
||||
|
||||
RESPONSE=$(
|
||||
curl -sS -X POST "$INSTANCE_URL/management/v1/users/machine" \
|
||||
-H "Authorization: Bearer $PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"userName": "netbird-service-account",
|
||||
"name": "Netbird Service Account",
|
||||
"description": "Netbird Service Account for IDP management",
|
||||
"accessTokenType": "ACCESS_TOKEN_TYPE_JWT"
|
||||
}'
|
||||
)
|
||||
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.userId')
|
||||
handle_zitadel_request_response "$PARSED_RESPONSE" "create_service_user" "$RESPONSE"
|
||||
echo "$PARSED_RESPONSE"
|
||||
}
|
||||
|
||||
create_service_user_secret() {
|
||||
INSTANCE_URL=$1
|
||||
PAT=$2
|
||||
USER_ID=$3
|
||||
|
||||
RESPONSE=$(
|
||||
curl -sS -X PUT "$INSTANCE_URL/management/v1/users/$USER_ID/secret" \
|
||||
-H "Authorization: Bearer $PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
)
|
||||
SERVICE_USER_CLIENT_ID=$(echo "$RESPONSE" | jq -r '.clientId')
|
||||
handle_zitadel_request_response "$SERVICE_USER_CLIENT_ID" "create_service_user_secret_id" "$RESPONSE"
|
||||
SERVICE_USER_CLIENT_SECRET=$(echo "$RESPONSE" | jq -r '.clientSecret')
|
||||
handle_zitadel_request_response "$SERVICE_USER_CLIENT_SECRET" "create_service_user_secret" "$RESPONSE"
|
||||
}
|
||||
|
||||
add_organization_user_manager() {
|
||||
INSTANCE_URL=$1
|
||||
PAT=$2
|
||||
USER_ID=$3
|
||||
|
||||
RESPONSE=$(
|
||||
curl -sS -X POST "$INSTANCE_URL/management/v1/orgs/me/members" \
|
||||
-H "Authorization: Bearer $PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"userId": "'"$USER_ID"'",
|
||||
"roles": [
|
||||
"ORG_USER_MANAGER"
|
||||
]
|
||||
}'
|
||||
)
|
||||
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.creationDate')
|
||||
handle_zitadel_request_response "$PARSED_RESPONSE" "add_organization_user_manager" "$RESPONSE"
|
||||
echo "$PARSED_RESPONSE"
|
||||
}
|
||||
|
||||
create_admin_user() {
|
||||
INSTANCE_URL=$1
|
||||
PAT=$2
|
||||
USERNAME=$3
|
||||
PASSWORD=$4
|
||||
RESPONSE=$(
|
||||
curl -sS -X POST "$INSTANCE_URL/management/v1/users/human/_import" \
|
||||
-H "Authorization: Bearer $PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"userName": "'"$USERNAME"'",
|
||||
"profile": {
|
||||
"firstName": "Zitadel",
|
||||
"lastName": "Admin"
|
||||
},
|
||||
"email": {
|
||||
"email": "'"$USERNAME"'",
|
||||
"isEmailVerified": true
|
||||
},
|
||||
"password": "'"$PASSWORD"'",
|
||||
"passwordChangeRequired": false
|
||||
}'
|
||||
)
|
||||
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.userId')
|
||||
handle_zitadel_request_response "$PARSED_RESPONSE" "create_admin_user" "$RESPONSE"
|
||||
echo "$PARSED_RESPONSE"
|
||||
}
|
||||
|
||||
add_instance_admin() {
|
||||
INSTANCE_URL=$1
|
||||
PAT=$2
|
||||
USER_ID=$3
|
||||
|
||||
RESPONSE=$(
|
||||
curl -sS -X POST "$INSTANCE_URL/admin/v1/members" \
|
||||
-H "Authorization: Bearer $PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"userId": "'"$USER_ID"'",
|
||||
"roles": [
|
||||
"IAM_OWNER"
|
||||
]
|
||||
}'
|
||||
)
|
||||
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.creationDate')
|
||||
handle_zitadel_request_response "$PARSED_RESPONSE" "add_instance_admin" "$RESPONSE"
|
||||
echo "$PARSED_RESPONSE"
|
||||
}
|
||||
|
||||
delete_auto_service_user() {
|
||||
INSTANCE_URL=$1
|
||||
PAT=$2
|
||||
|
||||
RESPONSE=$(
|
||||
curl -sS -X GET "$INSTANCE_URL/auth/v1/users/me" \
|
||||
-H "Authorization: Bearer $PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
)
|
||||
USER_ID=$(echo "$RESPONSE" | jq -r '.user.id')
|
||||
handle_zitadel_request_response "$USER_ID" "delete_auto_service_user_get_user" "$RESPONSE"
|
||||
|
||||
RESPONSE=$(
|
||||
curl -sS -X DELETE "$INSTANCE_URL/admin/v1/members/$USER_ID" \
|
||||
-H "Authorization: Bearer $PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
)
|
||||
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.changeDate')
|
||||
handle_zitadel_request_response "$PARSED_RESPONSE" "delete_auto_service_user_remove_instance_permissions" "$RESPONSE"
|
||||
|
||||
RESPONSE=$(
|
||||
curl -sS -X DELETE "$INSTANCE_URL/management/v1/orgs/me/members/$USER_ID" \
|
||||
-H "Authorization: Bearer $PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
)
|
||||
PARSED_RESPONSE=$(echo "$RESPONSE" | jq -r '.details.changeDate')
|
||||
handle_zitadel_request_response "$PARSED_RESPONSE" "delete_auto_service_user_remove_org_permissions" "$RESPONSE"
|
||||
echo "$PARSED_RESPONSE"
|
||||
}
|
||||
|
||||
init_zitadel() {
|
||||
echo -e "\nInitializing Zitadel with NetBird's applications\n"
|
||||
INSTANCE_URL="$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT"
|
||||
|
||||
TOKEN_PATH=./machinekey/zitadel-admin-sa.token
|
||||
|
||||
echo -n "Waiting for Zitadel's PAT to be created "
|
||||
wait_pat "$TOKEN_PATH"
|
||||
echo "Reading Zitadel PAT"
|
||||
PAT=$(cat $TOKEN_PATH)
|
||||
if [ "$PAT" = "null" ]; then
|
||||
echo "Failed requesting getting Zitadel PAT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -n "Waiting for Zitadel to become ready "
|
||||
wait_api "$INSTANCE_URL" "$PAT"
|
||||
|
||||
# create the zitadel project
|
||||
echo "Creating new zitadel project"
|
||||
PROJECT_ID=$(create_new_project "$INSTANCE_URL" "$PAT")
|
||||
|
||||
ZITADEL_DEV_MODE=false
|
||||
BASE_REDIRECT_URL=$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN
|
||||
if [[ $NETBIRD_HTTP_PROTOCOL == "http" ]]; then
|
||||
ZITADEL_DEV_MODE=true
|
||||
fi
|
||||
|
||||
# create zitadel spa applications
|
||||
echo "Creating new Zitadel SPA Dashboard application"
|
||||
DASHBOARD_APPLICATION_CLIENT_ID=$(create_new_application "$INSTANCE_URL" "$PAT" "Dashboard" "http://localhost:3000/nb-auth" "http://localhost:3000/nb-silent-auth" "http://localhost:3000/" "true")
|
||||
|
||||
echo "Creating new Zitadel SPA Cli application"
|
||||
CLI_APPLICATION_CLIENT_ID=$(create_new_application "$INSTANCE_URL" "$PAT" "Cli" "http://localhost:53000/" "http://localhost:54000/" "http://localhost:53000/" "true")
|
||||
|
||||
MACHINE_USER_ID=$(create_service_user "$INSTANCE_URL" "$PAT")
|
||||
|
||||
SERVICE_USER_CLIENT_ID="null"
|
||||
SERVICE_USER_CLIENT_SECRET="null"
|
||||
|
||||
create_service_user_secret "$INSTANCE_URL" "$PAT" "$MACHINE_USER_ID"
|
||||
|
||||
DATE=$(add_organization_user_manager "$INSTANCE_URL" "$PAT" "$MACHINE_USER_ID")
|
||||
|
||||
ZITADEL_ADMIN_USERNAME="admin@localhost"
|
||||
ZITADEL_ADMIN_PASSWORD="testMe123@"
|
||||
|
||||
HUMAN_USER_ID=$(create_admin_user "$INSTANCE_URL" "$PAT" "$ZITADEL_ADMIN_USERNAME" "$ZITADEL_ADMIN_PASSWORD")
|
||||
|
||||
DATE="null"
|
||||
|
||||
DATE=$(add_instance_admin "$INSTANCE_URL" "$PAT" "$HUMAN_USER_ID")
|
||||
|
||||
DATE="null"
|
||||
DATE=$(delete_auto_service_user "$INSTANCE_URL" "$PAT")
|
||||
if [ "$DATE" = "null" ]; then
|
||||
echo "Failed deleting auto service user"
|
||||
echo "Please remove it manually"
|
||||
fi
|
||||
|
||||
export NETBIRD_AUTH_CLIENT_ID=$DASHBOARD_APPLICATION_CLIENT_ID
|
||||
export NETBIRD_AUTH_CLIENT_ID_CLI=$CLI_APPLICATION_CLIENT_ID
|
||||
export NETBIRD_IDP_MGMT_CLIENT_ID=$SERVICE_USER_CLIENT_ID
|
||||
export NETBIRD_IDP_MGMT_CLIENT_SECRET=$SERVICE_USER_CLIENT_SECRET
|
||||
export ZITADEL_ADMIN_USERNAME
|
||||
export ZITADEL_ADMIN_PASSWORD
|
||||
}
|
||||
|
||||
check_nb_domain() {
|
||||
DOMAIN=$1
|
||||
if [ "$DOMAIN-x" == "-x" ]; then
|
||||
echo "The NETBIRD_DOMAIN variable cannot be empty." > /dev/stderr
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$DOMAIN" == "netbird.example.com" ]; then
|
||||
echo "The NETBIRD_DOMAIN cannot be netbird.example.com" > /dev/stderr
|
||||
retrun 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
read_nb_domain() {
|
||||
READ_NETBIRD_DOMAIN=""
|
||||
echo -n "Enter the domain you want to use for NetBird (e.g. netbird.my-domain.com): " > /dev/stderr
|
||||
read -r READ_NETBIRD_DOMAIN < /dev/tty
|
||||
if ! check_nb_domain "$READ_NETBIRD_DOMAIN"; then
|
||||
read_nb_domain
|
||||
fi
|
||||
echo "$READ_NETBIRD_DOMAIN"
|
||||
}
|
||||
|
||||
initEnvironment() {
|
||||
CADDY_SECURE_DOMAIN=""
|
||||
ZITADEL_EXTERNALSECURE="false"
|
||||
ZITADEL_TLS_MODE="disabled"
|
||||
ZITADEL_MASTERKEY="$(openssl rand -base64 32 | head -c 32)"
|
||||
NETBIRD_PORT=80
|
||||
NETBIRD_HTTP_PROTOCOL="http"
|
||||
TURN_USER="self"
|
||||
TURN_PASSWORD=$(openssl rand -base64 32 | sed 's/=//g')
|
||||
TURN_MIN_PORT=49152
|
||||
TURN_MAX_PORT=65535
|
||||
|
||||
NETBIRD_DOMAIN=$(get_main_ip_address)
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
ZIDATE_TOKEN_EXPIRATION_DATE=$(date -u -v+30M "+%Y-%m-%dT%H:%M:%SZ")
|
||||
else
|
||||
ZIDATE_TOKEN_EXPIRATION_DATE=$(date -u -d "+30 minutes" "+%Y-%m-%dT%H:%M:%SZ")
|
||||
fi
|
||||
|
||||
check_jq
|
||||
|
||||
DOCKER_COMPOSE_COMMAND=$(check_docker_compose)
|
||||
|
||||
if [ -f zitadel.env ]; then
|
||||
echo "Generated files already exist, if you want to reinitialize the environment, please remove them first."
|
||||
echo "You can use the following commands:"
|
||||
echo " $DOCKER_COMPOSE_COMMAND down --volumes # to remove all containers and volumes"
|
||||
echo " rm -f docker-compose.yml Caddyfile zitadel.env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json"
|
||||
echo "Be aware that this will remove all data from the database, and you will have to reconfigure the dashboard."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo Rendering initial files...
|
||||
renderDockerCompose > docker-compose.yml
|
||||
renderCaddyfile > Caddyfile
|
||||
renderZitadelEnv > zitadel.env
|
||||
echo "" > turnserver.conf
|
||||
echo "" > management.json
|
||||
|
||||
mkdir -p machinekey
|
||||
chmod 777 machinekey
|
||||
|
||||
init_crdb
|
||||
|
||||
echo -e "\nStarting Zidatel IDP for user management\n\n"
|
||||
$DOCKER_COMPOSE_COMMAND up -d caddy zitadel
|
||||
init_zitadel
|
||||
|
||||
echo -e "\nRendering NetBird files...\n"
|
||||
renderTurnServerConf > turnserver.conf
|
||||
renderManagementJson > management.json
|
||||
renderDashboardEnv > src/.local-config.json
|
||||
|
||||
echo -e "\nStarting NetBird services\n"
|
||||
$DOCKER_COMPOSE_COMMAND up -d
|
||||
echo -e "\nDone!\n"
|
||||
echo "You can access the NetBird dashboard at $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT"
|
||||
echo "Login with the following credentials:"
|
||||
echo "Username: $ZITADEL_ADMIN_USERNAME" | tee .env
|
||||
echo "Password: $ZITADEL_ADMIN_PASSWORD" | tee -a .env
|
||||
}
|
||||
|
||||
renderCaddyfile() {
|
||||
cat <<EOF
|
||||
{
|
||||
debug
|
||||
servers :80,:443 {
|
||||
protocols h1 h2c
|
||||
}
|
||||
}
|
||||
|
||||
:80${CADDY_SECURE_DOMAIN} {
|
||||
# Signal
|
||||
reverse_proxy /signalexchange.SignalExchange/* h2c://signal:10000
|
||||
# Management
|
||||
reverse_proxy /api/* management:80
|
||||
reverse_proxy /management.ManagementService/* h2c://management:80
|
||||
# Zitadel
|
||||
reverse_proxy /zitadel.admin.v1.AdminService/* h2c://zitadel:8080
|
||||
reverse_proxy /admin/v1/* h2c://zitadel:8080
|
||||
reverse_proxy /zitadel.auth.v1.AuthService/* h2c://zitadel:8080
|
||||
reverse_proxy /auth/v1/* h2c://zitadel:8080
|
||||
reverse_proxy /zitadel.management.v1.ManagementService/* h2c://zitadel:8080
|
||||
reverse_proxy /management/v1/* h2c://zitadel:8080
|
||||
reverse_proxy /zitadel.system.v1.SystemService/* h2c://zitadel:8080
|
||||
reverse_proxy /system/v1/* h2c://zitadel:8080
|
||||
reverse_proxy /assets/v1/* h2c://zitadel:8080
|
||||
reverse_proxy /ui/* h2c://zitadel:8080
|
||||
reverse_proxy /oidc/v1/* h2c://zitadel:8080
|
||||
reverse_proxy /saml/v2/* h2c://zitadel:8080
|
||||
reverse_proxy /oauth/v2/* h2c://zitadel:8080
|
||||
reverse_proxy /.well-known/openid-configuration h2c://zitadel:8080
|
||||
reverse_proxy /openapi/* h2c://zitadel:8080
|
||||
reverse_proxy /debug/* h2c://zitadel:8080
|
||||
# Dashboard
|
||||
reverse_proxy /* dashboard:80
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
renderTurnServerConf() {
|
||||
cat <<EOF
|
||||
listening-port=3478
|
||||
tls-listening-port=5349
|
||||
min-port=$TURN_MIN_PORT
|
||||
max-port=$TURN_MAX_PORT
|
||||
fingerprint
|
||||
lt-cred-mech
|
||||
user=$TURN_USER:$TURN_PASSWORD
|
||||
realm=wiretrustee.com
|
||||
cert=/etc/coturn/certs/cert.pem
|
||||
pkey=/etc/coturn/private/privkey.pem
|
||||
log-file=stdout
|
||||
no-software-attribute
|
||||
pidfile="/var/tmp/turnserver.pid"
|
||||
no-cli
|
||||
EOF
|
||||
}
|
||||
|
||||
renderManagementJson() {
|
||||
cat <<EOF
|
||||
{
|
||||
"Stuns": [
|
||||
{
|
||||
"Proto": "udp",
|
||||
"URI": "stun:$NETBIRD_DOMAIN:3478"
|
||||
}
|
||||
],
|
||||
"TURNConfig": {
|
||||
"Turns": [
|
||||
{
|
||||
"Proto": "udp",
|
||||
"URI": "turn:$NETBIRD_DOMAIN:3478",
|
||||
"Username": "$TURN_USER",
|
||||
"Password": "$TURN_PASSWORD"
|
||||
}
|
||||
],
|
||||
"TimeBasedCredentials": false
|
||||
},
|
||||
"Signal": {
|
||||
"Proto": "$NETBIRD_HTTP_PROTOCOL",
|
||||
"URI": "$NETBIRD_DOMAIN:$NETBIRD_PORT"
|
||||
},
|
||||
"HttpConfig": {
|
||||
"AuthIssuer": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN",
|
||||
"AuthAudience": "$NETBIRD_AUTH_CLIENT_ID",
|
||||
"OIDCConfigEndpoint":"$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/.well-known/openid-configuration"
|
||||
},
|
||||
"IdpManagerConfig": {
|
||||
"ManagerType": "zitadel",
|
||||
"ClientConfig": {
|
||||
"Issuer": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT",
|
||||
"TokenEndpoint": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT/oauth/v2/token",
|
||||
"ClientID": "$NETBIRD_IDP_MGMT_CLIENT_ID",
|
||||
"ClientSecret": "$NETBIRD_IDP_MGMT_CLIENT_SECRET",
|
||||
"GrantType": "client_credentials"
|
||||
},
|
||||
"ExtraConfig": {
|
||||
"ManagementEndpoint": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT/management/v1"
|
||||
}
|
||||
},
|
||||
"PKCEAuthorizationFlow": {
|
||||
"ProviderConfig": {
|
||||
"Audience": "$NETBIRD_AUTH_CLIENT_ID_CLI",
|
||||
"ClientID": "$NETBIRD_AUTH_CLIENT_ID_CLI",
|
||||
"Scope": "openid profile email offline_access",
|
||||
"RedirectURLs": ["http://localhost:53000/","http://localhost:54000/"]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
renderDashboardEnv() {
|
||||
cat <<EOF
|
||||
{
|
||||
"auth0Auth": "false",
|
||||
"authAuthority": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT",
|
||||
"authClientId": "$NETBIRD_AUTH_CLIENT_ID",
|
||||
"authScopesSupported": "openid profile email offline_access",
|
||||
"authAudience": "$NETBIRD_AUTH_CLIENT_ID",
|
||||
"apiOrigin": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT",
|
||||
"grpcApiOrigin": "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN:$NETBIRD_PORT",
|
||||
"redirectURI": "/nb-auth",
|
||||
"silentRedirectURI": "/nb-silent-auth"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
renderZitadelEnv() {
|
||||
cat <<EOF
|
||||
ZITADEL_LOG_LEVEL=debug
|
||||
ZITADEL_MASTERKEY=$ZITADEL_MASTERKEY
|
||||
ZITADEL_DATABASE_COCKROACH_HOST=crdb
|
||||
ZITADEL_DATABASE_COCKROACH_USER_USERNAME=zitadel_user
|
||||
ZITADEL_DATABASE_COCKROACH_USER_SSL_MODE=verify-full
|
||||
ZITADEL_DATABASE_COCKROACH_USER_SSL_ROOTCERT="/crdb-certs/ca.crt"
|
||||
ZITADEL_DATABASE_COCKROACH_USER_SSL_CERT="/crdb-certs/client.zitadel_user.crt"
|
||||
ZITADEL_DATABASE_COCKROACH_USER_SSL_KEY="/crdb-certs/client.zitadel_user.key"
|
||||
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_MODE=verify-full
|
||||
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_ROOTCERT="/crdb-certs/ca.crt"
|
||||
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_CERT="/crdb-certs/client.root.crt"
|
||||
ZITADEL_DATABASE_COCKROACH_ADMIN_SSL_KEY="/crdb-certs/client.root.key"
|
||||
ZITADEL_EXTERNALSECURE=$ZITADEL_EXTERNALSECURE
|
||||
ZITADEL_TLS_ENABLED="false"
|
||||
ZITADEL_EXTERNALPORT=$NETBIRD_PORT
|
||||
ZITADEL_EXTERNALDOMAIN=$NETBIRD_DOMAIN
|
||||
ZITADEL_FIRSTINSTANCE_PATPATH=/machinekey/zitadel-admin-sa.token
|
||||
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME=zitadel-admin-sa
|
||||
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME=Admin
|
||||
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_SCOPES=openid
|
||||
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE=$ZIDATE_TOKEN_EXPIRATION_DATE
|
||||
EOF
|
||||
}
|
||||
|
||||
renderDockerCompose() {
|
||||
cat <<EOF
|
||||
version: "3.4"
|
||||
services:
|
||||
# Caddy reverse proxy
|
||||
caddy:
|
||||
image: caddy
|
||||
restart: unless-stopped
|
||||
networks: [ netbird ]
|
||||
ports:
|
||||
- '443:443'
|
||||
- '80:80'
|
||||
- '8080:8080'
|
||||
volumes:
|
||||
- netbird_caddy_data:/data
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
# Management
|
||||
management:
|
||||
image: netbirdio/management:latest
|
||||
restart: unless-stopped
|
||||
networks: [netbird]
|
||||
volumes:
|
||||
- netbird_management:/var/lib/netbird
|
||||
- ./management.json:/etc/netbird/management.json
|
||||
command: [
|
||||
"--port", "80",
|
||||
"--log-file", "console",
|
||||
"--log-level", "info",
|
||||
"--disable-anonymous-metrics=false",
|
||||
"--single-account-mode-domain=netbird.selfhosted",
|
||||
"--dns-domain=netbird.selfhosted",
|
||||
"--idp-sign-key-refresh-enabled",
|
||||
]
|
||||
# Zitadel - identity provider
|
||||
zitadel:
|
||||
restart: 'always'
|
||||
networks: [netbird]
|
||||
image: 'ghcr.io/zitadel/zitadel:v2.31.3'
|
||||
command: 'start-from-init --masterkeyFromEnv --tlsMode $ZITADEL_TLS_MODE'
|
||||
env_file:
|
||||
- ./zitadel.env
|
||||
depends_on:
|
||||
crdb:
|
||||
condition: 'service_healthy'
|
||||
volumes:
|
||||
- ./machinekey:/machinekey
|
||||
- netbird_zitadel_certs:/crdb-certs:ro
|
||||
# CockroachDB for zitadel
|
||||
crdb:
|
||||
restart: 'always'
|
||||
networks: [netbird]
|
||||
image: 'cockroachdb/cockroach:v22.2.2'
|
||||
command: 'start-single-node --advertise-addr crdb'
|
||||
volumes:
|
||||
- netbird_crdb_data:/cockroach/cockroach-data
|
||||
- netbird_crdb_certs:/cockroach/certs
|
||||
- netbird_zitadel_certs:/zitadel-certs
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-f", "http://localhost:8080/health?ready=1" ]
|
||||
interval: '10s'
|
||||
timeout: '30s'
|
||||
retries: 5
|
||||
start_period: '20s'
|
||||
|
||||
volumes:
|
||||
netbird_management:
|
||||
netbird_caddy_data:
|
||||
netbird_crdb_data:
|
||||
netbird_crdb_certs:
|
||||
netbird_zitadel_certs:
|
||||
|
||||
networks:
|
||||
netbird:
|
||||
EOF
|
||||
}
|
||||
|
||||
initEnvironment
|
||||
56
e2e-tests/pages/access-control-page.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Page, test, expect } from "@playwright/test";
|
||||
|
||||
export class AccessControlPage {
|
||||
private readonly accessControlUrl = 'http://localhost:3000/acls'
|
||||
private readonly defaulAccessControl = this.page.getByRole('cell', { name: 'Default' })
|
||||
private readonly deleteButton = this.page.getByRole('button', { name: 'Delete' })
|
||||
private readonly deleteModal = this.page.getByTestId('confirm-delete-modal-title')
|
||||
private readonly confirmButton = this.page.getByRole('button', { name: 'OK' })
|
||||
private readonly addRulesButton = this.page.getByTestId('add-rule-empty-state-button')
|
||||
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
async openAccessControlPage() {
|
||||
await test.step('Open Access Control page', async () => {
|
||||
await this.page.goto(this.accessControlUrl);
|
||||
})
|
||||
}
|
||||
|
||||
async assertDefaultAccessCotrolIsCreated() {
|
||||
await test.step('Assert that default cotrol access is created', async () => {
|
||||
await expect(this.defaulAccessControl).toBeVisible();
|
||||
})
|
||||
}
|
||||
|
||||
async pressDeleteButton() {
|
||||
await test.step('Press delete button', async () => {
|
||||
await this.deleteButton.click();
|
||||
})
|
||||
}
|
||||
|
||||
async assertDeleteModalIsVisibile() {
|
||||
await test.step('Assert access control deletion modal is visible', async () => {
|
||||
await expect(this.deleteModal).toBeVisible();
|
||||
})
|
||||
}
|
||||
|
||||
async pressConfirmButton() {
|
||||
await test.step('Press confirm button on access control deletion modal', async () => {
|
||||
await this.confirmButton.click();
|
||||
})
|
||||
}
|
||||
|
||||
async assertDefaultAccessCotrolIsDeleted() {
|
||||
await test.step('Assert default access control should be deleted', async () => {
|
||||
await expect(this.defaulAccessControl).not.toBeVisible();
|
||||
})
|
||||
}
|
||||
|
||||
async assertAddRuleButtonIsVisile() {
|
||||
await test.step('Assert Add Rules button is visible', async () => {
|
||||
await expect(this.addRulesButton).toBeVisible();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default AccessControlPage;
|
||||
34
e2e-tests/pages/login-page.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Page, test, expect} from "@playwright/test";
|
||||
|
||||
export class LoginPage {
|
||||
private readonly localUrl = 'http://localhost:3000/'
|
||||
private readonly usernameField = this.page.getByPlaceholder('username@domain')
|
||||
private readonly nextButton = this.page.getByRole('button', { name: 'next' })
|
||||
private readonly passwordField = this.page.getByLabel('Password')
|
||||
private readonly skipButton = this.page.getByRole('button', { name: 'skip' });
|
||||
private readonly netBirdLogo = this.page.getByRole('link', { name: 'logo' })
|
||||
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
async doLogin() {
|
||||
await test.step('Login to local enviroment', async () => {
|
||||
await this.page.goto(this.localUrl);
|
||||
await this.usernameField.fill('admin@localhost');
|
||||
await this.pressNextButton();
|
||||
await this.passwordField.fill('testMe123@');
|
||||
await this.pressNextButton();
|
||||
if (await this.skipButton.isVisible({ timeout: 300 })) {
|
||||
await this.skipButton.click();
|
||||
}
|
||||
await expect(this.netBirdLogo).toBeVisible();
|
||||
})
|
||||
}
|
||||
|
||||
async pressNextButton() {
|
||||
await test.step('Press next button', async () => {
|
||||
await this.nextButton.click();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default LoginPage;
|
||||
111
e2e-tests/pages/modals/add-peer-modal.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { Page, test, expect } from "@playwright/test";
|
||||
|
||||
export class AddPeerModal {
|
||||
private readonly addPeerModal = this.page.getByTestId('add-peer-modal').locator('div').nth(2)
|
||||
private readonly linuxTab = this.page.getByTestId('add-peer-modal-linux-tab')
|
||||
private readonly windowsTab = this.page.getByTestId('add-peer-modal-windows-tab')
|
||||
private readonly macTab = this.page.getByTestId('add-peer-modal-mac-tab')
|
||||
private readonly androidTab = this.page.getByTestId('add-peer-modal-android-tab')
|
||||
private readonly dockerTab = this.page.getByTestId('add-peer-modal-docker-tab')
|
||||
private readonly linuxTabText = this.page.locator('pre').filter({ hasText: 'curl -fsSL https://pkgs.netbird.io/install.sh | sh' })
|
||||
private readonly windowsDownloadButton = this.page.getByTestId('download-windows-button')
|
||||
private readonly intelDownloadButton = this.page.getByTestId('download-intel-button')
|
||||
private readonly m1M2DownloadButton = this.page.getByTestId('download-m1-m2-button')
|
||||
private readonly androidDownloadButton = this.page.getByTestId('download-android-button')
|
||||
private readonly dockerDownloadButton = this.page.getByTestId('download-docker-button')
|
||||
private readonly closeButton = this.page.getByLabel('Close', { exact: true })
|
||||
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
async assertPeerModalIsVisible() {
|
||||
await test.step('Assert that add peer modal is visible', async () => {
|
||||
await expect(this.addPeerModal).toBeVisible();
|
||||
})
|
||||
}
|
||||
|
||||
async assertPeerModalIsNotVisible() {
|
||||
await test.step('Assert that add peer modal is not visible', async () => {
|
||||
await expect(this.addPeerModal).not.toBeVisible();
|
||||
})
|
||||
}
|
||||
|
||||
async openLinuxTab() {
|
||||
await test.step('Open Linux tab on add peer modal', async () => {
|
||||
await this.linuxTab.click();
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
async openWindowsTab() {
|
||||
await test.step('Open Windows tab on add peer modal', async () => {
|
||||
await this.windowsTab.click();
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
async openMacTab() {
|
||||
await test.step('Open MacOS tab on add peer modal', async () => {
|
||||
await this.macTab.click();
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
async openAndroidTab() {
|
||||
await test.step('Open Android tab on add peer modal', async () => {
|
||||
await this.androidTab.click();
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
async openDockerTab() {
|
||||
await test.step('Open Docker tab on add peer modal', async () => {
|
||||
await this.dockerTab.click();
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
async assertLinuxTabHasCorrectText() {
|
||||
await test.step('Assert Linux tab has correct installation text', async () => {
|
||||
await expect(this.linuxTabText).toBeVisible();
|
||||
})
|
||||
}
|
||||
|
||||
async assertWindowsDownloadButtonHasCorrectLink() {
|
||||
await test.step('Assert Windows download button has a correct link', async () => {
|
||||
await expect(this.windowsDownloadButton).toHaveAttribute('href', 'https://pkgs.netbird.io/windows/x64');
|
||||
})
|
||||
}
|
||||
|
||||
async assertIntelDownloadButtonHasCorrectLink() {
|
||||
await test.step('Assert Intel download button has a correct link', async () => {
|
||||
await expect(this.intelDownloadButton).toHaveAttribute('href', 'https://pkgs.netbird.io/macos/amd64');
|
||||
})
|
||||
}
|
||||
|
||||
async assertM1M2DownloadButtonHasCorrectLink() {
|
||||
await test.step('Assert M1 & M2 download button has a correct link', async () => {
|
||||
await expect(this.m1M2DownloadButton).toHaveAttribute('href', 'https://pkgs.netbird.io/macos/arm64');
|
||||
})
|
||||
}
|
||||
|
||||
async assertAndroidDownloadButtonHasCorrectLink() {
|
||||
await test.step('Assert Android download button has a correct link', async () => {
|
||||
await expect(this.androidDownloadButton).toHaveAttribute('href', 'https://play.google.com/store/apps/details?id=io.netbird.client');
|
||||
})
|
||||
}
|
||||
|
||||
async assertDockerDownloadButtonHasCorrectLink() {
|
||||
await test.step('Assert Docker download button has a correct link', async () => {
|
||||
await expect(this.dockerDownloadButton).toHaveAttribute('href', 'https://docs.docker.com/engine/install/');
|
||||
})
|
||||
}
|
||||
|
||||
async closeAddPeerModal() {
|
||||
await test.step('Close Add peer modal', async () => {
|
||||
await this.closeButton.click();
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default AddPeerModal;
|
||||
15
e2e-tests/pages/peers-page.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Page, test } from "@playwright/test";
|
||||
|
||||
export class PeersPage {
|
||||
private readonly addNewPeerButton = this.page.getByTestId('add-new-peer-button')
|
||||
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
async clickOnAddNewPeerButton() {
|
||||
await test.step('Click on Add new peer Button to open Add peer modal', async () => {
|
||||
await this.addNewPeerButton.click();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default PeersPage;
|
||||
15
e2e-tests/pages/top-menu.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Page, test} from "@playwright/test";
|
||||
|
||||
export class TopMenu {
|
||||
private readonly accessControlButton = this.page.getByTestId('access-control-page')
|
||||
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
async clickOnAccessControlOnTopMenu() {
|
||||
await test.step('Click on Access Control page on a top menu', async () => {
|
||||
await this.accessControlButton.click();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default TopMenu;
|
||||
22
e2e-tests/tests/access-control.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { test } from '@playwright/test'
|
||||
import {LoginPage} from '../pages/login-page'
|
||||
import {AccessControlPage} from '../pages/access-control-page'
|
||||
|
||||
let loginPage: LoginPage
|
||||
let accessControlPage: AccessControlPage
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
await loginPage.doLogin();
|
||||
});
|
||||
|
||||
test('Confirm that new user has Default access', async ({ page }) => {
|
||||
accessControlPage = new AccessControlPage(page);
|
||||
await accessControlPage.openAccessControlPage();
|
||||
await accessControlPage.assertDefaultAccessCotrolIsCreated();
|
||||
await accessControlPage.pressDeleteButton();
|
||||
await accessControlPage.assertDeleteModalIsVisibile();
|
||||
await accessControlPage.pressConfirmButton();
|
||||
await accessControlPage.assertDefaultAccessCotrolIsDeleted();
|
||||
await accessControlPage.assertAddRuleButtonIsVisile();
|
||||
});
|
||||
49
e2e-tests/tests/peers.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { test } from '@playwright/test'
|
||||
import {AddPeerModal} from '../pages/modals/add-peer-modal'
|
||||
import {PeersPage} from '../pages/peers-page'
|
||||
import {LoginPage} from '../pages/login-page'
|
||||
|
||||
let addPeerModal: AddPeerModal
|
||||
let peersPage: PeersPage
|
||||
let loginPage: LoginPage
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
addPeerModal = new AddPeerModal(page);
|
||||
loginPage = new LoginPage(page);
|
||||
await loginPage.doLogin();
|
||||
await addPeerModal.assertPeerModalIsVisible();
|
||||
});
|
||||
|
||||
test('Test Linux tab on a first access add peer modal / @bc', async function () {
|
||||
await addPeerModal.openLinuxTab();
|
||||
await addPeerModal.assertLinuxTabHasCorrectText();
|
||||
});
|
||||
|
||||
test('Test Windows tab on a first access add peer modal / @bc', async () => {
|
||||
await addPeerModal.openWindowsTab();
|
||||
await addPeerModal.assertWindowsDownloadButtonHasCorrectLink();
|
||||
});
|
||||
|
||||
test('Test MacOS tab on a first access add peer modal / @bc', async () => {
|
||||
await addPeerModal.openMacTab();
|
||||
await addPeerModal.assertIntelDownloadButtonHasCorrectLink();
|
||||
await addPeerModal.assertM1M2DownloadButtonHasCorrectLink();
|
||||
});
|
||||
|
||||
test('Test Android tab on a first access add peer modal', async () => {
|
||||
await addPeerModal.openAndroidTab();
|
||||
await addPeerModal.assertAndroidDownloadButtonHasCorrectLink();
|
||||
});
|
||||
|
||||
test('Test Docker tab on a first access add peer modal', async () => {
|
||||
await addPeerModal.openDockerTab();
|
||||
await addPeerModal.assertDockerDownloadButtonHasCorrectLink();
|
||||
});
|
||||
|
||||
test('Close and open Add peer modal', async ({ page }) => {
|
||||
peersPage = new PeersPage(page);
|
||||
await addPeerModal.closeAddPeerModal();
|
||||
await addPeerModal.assertPeerModalIsNotVisible();
|
||||
await peersPage.clickOnAddNewPeerButton();
|
||||
await addPeerModal.assertPeerModalIsVisible();
|
||||
});
|
||||
664
package-lock.json
generated
@@ -39,6 +39,7 @@
|
||||
"react-redux": "^8.0.2",
|
||||
"react-router-dom": "^5.3.3",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-select": "^5.7.3",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-table": "^7.7.0",
|
||||
"redux": "^4.2.0",
|
||||
@@ -78,6 +79,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react-syntax-highlighter": "^15.5.3"
|
||||
"@types/react-syntax-highlighter": "^15.5.3",
|
||||
"@playwright/test": "^1.36.2"
|
||||
}
|
||||
}
|
||||
|
||||
80
playwright.config.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './e2e-tests/tests',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://127.0.0.1:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { channel: 'chrome', },
|
||||
},
|
||||
|
||||
// {
|
||||
// name: 'firefox',
|
||||
// use: { browserName: 'firefox', },
|
||||
// },
|
||||
//
|
||||
// {
|
||||
// name: 'webkit',
|
||||
// use: { browserName: 'webkit', },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'npm run start',
|
||||
url: 'http://127.0.0.1:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 180 * 1000,
|
||||
},
|
||||
});
|
||||
13
src/App.tsx
@@ -70,16 +70,7 @@ function App() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
borderRadius: 4,
|
||||
colorPrimary: "#1890ff",
|
||||
fontFamily: "Arial"
|
||||
},
|
||||
components: {Badge: {fontSizeSM: 20}},
|
||||
}}
|
||||
>
|
||||
|
||||
<Provider store={store}>
|
||||
{!show && <SecureLoading padding="3em" width={50} height={50}/>}
|
||||
{show &&
|
||||
@@ -124,8 +115,6 @@ function App() {
|
||||
</Layout>
|
||||
}
|
||||
</Provider>
|
||||
|
||||
</ConfigProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="29.435" height="7.636" viewBox="0 0 29.435 7.636">
|
||||
<path id="direct_bi" d="M3.64,8.158-.178,4.34,3.64.522,4.3,1.17l-2.7,2.7h25.89l-2.7-2.7.656-.648L29.257,4.34,27.213,6.384,25.439,8.158,24.783,7.5l2.7-2.693H1.595L4.3,7.5Z" transform="translate(0.178 -0.522)" fill="#1e429f"/>
|
||||
<svg width="39" height="6" viewBox="0 0 39 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.451731 2.66267C0.315048 2.79935 0.315048 3.02096 0.451731 3.15764L2.67912 5.38503C2.8158 5.52171 3.03741 5.52171 3.17409 5.38503C3.31078 5.24835 3.31078 5.02674 3.17409 4.89006L1.19419 2.91016L3.17409 0.930257C3.31078 0.793574 3.31078 0.571966 3.17409 0.435282C3.03741 0.298599 2.8158 0.298599 2.67912 0.435282L0.451731 2.66267ZM38.3807 3.15764C38.5174 3.02096 38.5174 2.79935 38.3807 2.66267L36.1533 0.435282C36.0166 0.298599 35.795 0.298599 35.6583 0.435282C35.5216 0.571966 35.5216 0.793574 35.6583 0.930257L37.6382 2.91016L35.6583 4.89006C35.5216 5.02674 35.5216 5.24835 35.6583 5.38503C35.795 5.52171 36.0166 5.52171 36.1533 5.38503L38.3807 3.15764ZM0.699219 3.26016H38.1332V2.56016H0.699219V3.26016Z" fill="#03543F"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 332 B After Width: | Height: | Size: 837 B |
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="29.497" height="7.636" viewBox="0 0 29.497 7.636">
|
||||
<path id="direct_out" d="M26.589,8l-.656-.656,2.7-2.693H.91V3.713H28.635l-2.7-2.7.656-.648,3.818,3.818Z" transform="translate(-0.91 -0.364)" fill="#03543f"/>
|
||||
<svg width="39" height="6" viewBox="0 0 39 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M38.3807 3.15764C38.5174 3.02096 38.5174 2.79935 38.3807 2.66267L36.1533 0.435282C36.0166 0.298599 35.795 0.298599 35.6583 0.435282C35.5216 0.571966 35.5216 0.793574 35.6583 0.930257L37.6382 2.91016L35.6583 4.89006C35.5216 5.02674 35.5216 5.24835 35.6583 5.38503C35.795 5.52171 36.0166 5.52171 36.1533 5.38503L38.3807 3.15764ZM0.699219 3.26016H38.1332V2.56016H0.699219V3.26016Z" fill="#1E429F"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 265 B After Width: | Height: | Size: 506 B |
3
src/assets/forward_default.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="39" height="7" viewBox="0 0 39 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M38.3846 3.68889C38.5213 3.55221 38.5213 3.3306 38.3846 3.19392L36.1572 0.966532C36.0205 0.829849 35.7989 0.829849 35.6622 0.966532C35.5255 1.10322 35.5255 1.32482 35.6622 1.46151L37.6421 3.44141L35.6622 5.42131C35.5255 5.55799 35.5255 5.7796 35.6622 5.91628C35.7989 6.05296 36.0205 6.05296 36.1572 5.91628L38.3846 3.68889ZM0.703125 3.79141H38.1371V3.09141H0.703125V3.79141Z" fill="#D9D9D9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 503 B |
BIN
src/assets/google-play-badge.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
3
src/assets/in_bound.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="39" height="7" viewBox="0 0 39 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.615425 3.3111C0.478741 3.44779 0.478741 3.66939 0.615425 3.80608L2.84281 6.03346C2.97949 6.17015 3.2011 6.17015 3.33778 6.03346C3.47447 5.89678 3.47447 5.67517 3.33778 5.53849L1.35789 3.55859L3.33778 1.57869C3.47447 1.44201 3.47447 1.2204 3.33778 1.08372C3.2011 0.947033 2.97949 0.947033 2.84281 1.08372L0.615425 3.3111ZM38.2969 3.20859L0.862911 3.20859L0.862911 3.90859L38.2969 3.90859L38.2969 3.20859Z" fill="#2B4DA5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 535 B |
3
src/assets/out_bound_blue.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="39" height="7" viewBox="0 0 39 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M38.3807 3.68889C38.5174 3.55221 38.5174 3.3306 38.3807 3.19392L36.1533 0.966532C36.0166 0.829849 35.795 0.829849 35.6583 0.966532C35.5216 1.10322 35.5216 1.32482 35.6583 1.46151L37.6382 3.44141L35.6583 5.42131C35.5216 5.55799 35.5216 5.7796 35.6583 5.91628C35.795 6.05296 36.0166 6.05296 36.1533 5.91628L38.3807 3.68889ZM0.699219 3.79141L38.1332 3.79141V3.09141L0.699219 3.09141V3.79141Z" fill="#2B4DA5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 517 B |
3
src/assets/out_bound_green.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="39" height="7" viewBox="0 0 39 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M38.3846 3.68889C38.5213 3.55221 38.5213 3.3306 38.3846 3.19392L36.1572 0.966532C36.0205 0.829849 35.7989 0.829849 35.6622 0.966532C35.5255 1.10322 35.5255 1.32482 35.6622 1.46151L37.6421 3.44141L35.6622 5.42131C35.5255 5.55799 35.5255 5.7796 35.6622 5.91628C35.7989 6.05296 36.0205 6.05296 36.1572 5.91628L38.3846 3.68889ZM0.703125 3.79141H38.1371V3.09141H0.703125V3.79141Z" fill="#03543F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 503 B |
3
src/assets/reverse_default.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="39" height="7" viewBox="0 0 39 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.615425 3.3111C0.478741 3.44779 0.478741 3.66939 0.615425 3.80608L2.84281 6.03346C2.97949 6.17015 3.2011 6.17015 3.33778 6.03346C3.47447 5.89678 3.47447 5.67517 3.33778 5.53849L1.35789 3.55859L3.33778 1.57869C3.47447 1.44201 3.47447 1.2204 3.33778 1.08372C3.2011 0.947033 2.97949 0.947033 2.84281 1.08372L0.615425 3.3111ZM38.2969 3.20859L0.862911 3.20859L0.862911 3.90859L38.2969 3.90859L38.2969 3.20859Z" fill="#D9D9D9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 535 B |
3
src/assets/reverse_green.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="39" height="7" viewBox="0 0 39 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.615425 3.3111C0.478741 3.44779 0.478741 3.66939 0.615425 3.80608L2.84281 6.03346C2.97949 6.17015 3.2011 6.17015 3.33778 6.03346C3.47447 5.89678 3.47447 5.67517 3.33778 5.53849L1.35789 3.55859L3.33778 1.57869C3.47447 1.44201 3.47447 1.2204 3.33778 1.08372C3.2011 0.947033 2.97949 0.947033 2.84281 1.08372L0.615425 3.3111ZM38.2969 3.20859L0.862911 3.20859L0.862911 3.90859L38.2969 3.90859L38.2969 3.20859Z" fill="#03543F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 535 B |
943
src/components/AccessControlEdit.tsx
Normal file
@@ -0,0 +1,943 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import { actions as policyActions } from "../store/policy";
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
Card,
|
||||
Row,
|
||||
Select,
|
||||
SelectProps,
|
||||
Switch,
|
||||
Tag,
|
||||
Typography,
|
||||
Breadcrumb,
|
||||
} from "antd";
|
||||
import inbound from "../assets/in_bound.svg";
|
||||
import outBoundGreen from "../assets/out_bound_green.svg";
|
||||
import outBoundblue from "../assets/out_bound_blue.svg";
|
||||
import reverseDefault from "../assets/reverse_default.svg";
|
||||
import forwardDefault from "../assets/forward_default.svg";
|
||||
import reverseGreen from "../assets/reverse_green.svg";
|
||||
import { Policy, PolicyToSave } from "../store/policy/types";
|
||||
import { uniq } from "lodash";
|
||||
import { Header } from "antd/es/layout/layout";
|
||||
import { RuleObject } from "antd/lib/form";
|
||||
import { useGetTokenSilently } from "../utils/token";
|
||||
import { Container } from "./Container";
|
||||
import { useGetGroupTagHelpers } from "../utils/groups";
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
const { Text } = Typography;
|
||||
|
||||
interface FormPolicy {
|
||||
id?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
query: string;
|
||||
bidirectional: boolean;
|
||||
protocol: string;
|
||||
ports: string[];
|
||||
action: string;
|
||||
tagSourceGroups: string[];
|
||||
tagDestinationGroups: string[];
|
||||
}
|
||||
|
||||
const AccessControlEdit = () => {
|
||||
const { optionRender, blueTagRender, tagGroups, grayTagRender } =
|
||||
useGetGroupTagHelpers();
|
||||
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
const setupEditPolicyVisible = useSelector(
|
||||
(state: RootState) => state.policy.setupEditPolicyVisible
|
||||
);
|
||||
const groups = useSelector((state: RootState) => state.group.data);
|
||||
const actions: SelectProps["options"] = [
|
||||
{ label: "Accept", value: "accept" },
|
||||
{ label: "Drop", value: "drop" },
|
||||
];
|
||||
const protocols: SelectProps["options"] = [
|
||||
{ label: "ALL", value: "all" },
|
||||
{ label: "TCP", value: "tcp" },
|
||||
{ label: "UDP", value: "udp" },
|
||||
{ label: "ICMP", value: "icmp" },
|
||||
];
|
||||
const formPolicyCopy: any = ["ALL"];
|
||||
|
||||
const policy = useSelector((state: RootState) => state.policy.policy);
|
||||
const savedPolicy = useSelector(
|
||||
(state: RootState) => state.policy.savedPolicy
|
||||
);
|
||||
const [editName, setEditName] = useState(false);
|
||||
const [editDescription, setEditDescription] = useState(false);
|
||||
const [direction, setDirection] = useState<any>({
|
||||
biDirectional: false,
|
||||
reverseDirectional: false,
|
||||
});
|
||||
// const [tagGroups, setTagGroups] = useState([] as string[]);
|
||||
const [formPolicy, setFormPolicy] = useState({} as FormPolicy);
|
||||
const [form] = Form.useForm();
|
||||
const inputNameRef = useRef<any>(null);
|
||||
const inputDescriptionRef = useRef<any>(null);
|
||||
useEffect(() => {
|
||||
//Unmounting component clean
|
||||
return () => {
|
||||
onCancel();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (editName) inputNameRef.current!.focus({ cursor: "end" });
|
||||
}, [editName]);
|
||||
useEffect(() => {
|
||||
if (editDescription) inputDescriptionRef.current!.focus({ cursor: "end" });
|
||||
}, [editDescription]);
|
||||
// useEffect(() => {
|
||||
// setTagGroups(groups?.map((g) => g.name) || []);
|
||||
// }, [groups]);
|
||||
useEffect(() => {
|
||||
if (!policy) return;
|
||||
const fPolicy = {
|
||||
id: policy.id,
|
||||
name: policy.name,
|
||||
description: policy.description,
|
||||
enabled: policy.enabled,
|
||||
query: "",
|
||||
bidirectional: policy.rules[0].bidirectional,
|
||||
protocol: policy.rules[0].protocol,
|
||||
ports: policy.rules[0].ports,
|
||||
action: policy.rules[0].action,
|
||||
tagSourceGroups: policy.rules[0].sources
|
||||
? policy.rules[0].sources?.map((t) => t.id || "")
|
||||
: [],
|
||||
tagDestinationGroups: policy.rules[0].destinations
|
||||
? policy.rules[0].destinations?.map((t) => t.id || "")
|
||||
: [],
|
||||
} as FormPolicy;
|
||||
setFormPolicy(fPolicy);
|
||||
form.setFieldsValue(fPolicy);
|
||||
if (fPolicy.bidirectional) {
|
||||
setDirection({
|
||||
biDirectional: true,
|
||||
reverseDirectional: true,
|
||||
});
|
||||
} else {
|
||||
setDirection({
|
||||
biDirectional: false,
|
||||
reverseDirectional: false,
|
||||
});
|
||||
}
|
||||
}, [policy, form]);
|
||||
|
||||
const createPolicyToSave = (): PolicyToSave => {
|
||||
const sources =
|
||||
groups
|
||||
?.filter((g) => formPolicy.tagSourceGroups.includes(g.id || ""))
|
||||
.map((g) => g.id || "") || [];
|
||||
|
||||
const destinations =
|
||||
groups
|
||||
?.filter((g) => formPolicy.tagDestinationGroups.includes(g.id || ""))
|
||||
.map((g) => g.id || "") || [];
|
||||
|
||||
const existingGroupsNames: any[] = groups?.map((g) => g.id);
|
||||
const sourcesNoId = formPolicy.tagSourceGroups.filter(
|
||||
(s) => !existingGroupsNames.includes(s)
|
||||
);
|
||||
|
||||
const destinationsNoId = formPolicy.tagDestinationGroups.filter(
|
||||
(s) => !existingGroupsNames.includes(s)
|
||||
);
|
||||
const groupsToSave = uniq([...sourcesNoId, ...destinationsNoId]);
|
||||
return {
|
||||
id: formPolicy.id,
|
||||
name: formPolicy.name,
|
||||
description: formPolicy.description,
|
||||
enabled: formPolicy.enabled,
|
||||
sourcesNoId,
|
||||
destinationsNoId,
|
||||
groupsToSave,
|
||||
rules: [
|
||||
{
|
||||
id: formPolicy.id,
|
||||
name: formPolicy.name,
|
||||
description: formPolicy.description,
|
||||
enabled: formPolicy.enabled,
|
||||
sources:
|
||||
direction.reverseDirectional && !direction.biDirectional
|
||||
? destinations
|
||||
: sources,
|
||||
destinations:
|
||||
direction.reverseDirectional && !direction.biDirectional
|
||||
? sources
|
||||
: destinations,
|
||||
bidirectional: formPolicy.bidirectional,
|
||||
protocol: formPolicy.protocol,
|
||||
ports: formPolicy.ports,
|
||||
action: "accept",
|
||||
},
|
||||
],
|
||||
} as PolicyToSave;
|
||||
};
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((_) => {
|
||||
const policyToSave = createPolicyToSave();
|
||||
dispatch(
|
||||
policyActions.savePolicy.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: policyToSave,
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log("errorInfo", errorInfo);
|
||||
});
|
||||
};
|
||||
|
||||
const setVisibleNewRule = (status: boolean) => {
|
||||
dispatch(policyActions.setSetupEditPolicyVisible(status));
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedPolicy.loading) return;
|
||||
setEditName(false);
|
||||
dispatch(
|
||||
policyActions.setPolicy({
|
||||
name: "",
|
||||
description: "",
|
||||
enabled: true,
|
||||
query: "",
|
||||
rules: [
|
||||
{
|
||||
name: "",
|
||||
description: "",
|
||||
enabled: true,
|
||||
sources: [],
|
||||
destinations: [],
|
||||
bidirectional: true,
|
||||
protocol: "all",
|
||||
ports: [],
|
||||
action: "accept",
|
||||
},
|
||||
],
|
||||
} as Policy)
|
||||
);
|
||||
setVisibleNewRule(false);
|
||||
};
|
||||
|
||||
const onChange = (data: any) => {
|
||||
setFormPolicy({ ...formPolicy, ...data });
|
||||
};
|
||||
|
||||
const handleChangeSource = (value: string[]) => {
|
||||
setFormPolicy({
|
||||
...formPolicy,
|
||||
tagSourceGroups: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeDestination = (value: string[]) => {
|
||||
setFormPolicy({
|
||||
...formPolicy,
|
||||
tagDestinationGroups: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeProtocol = (value: string) => {
|
||||
if (value === "all" || value === "icmp") {
|
||||
setDirection({
|
||||
biDirectional: true,
|
||||
reverseDirectional: true,
|
||||
});
|
||||
}
|
||||
setFormPolicy({
|
||||
...formPolicy,
|
||||
ports: value === "all" || value === "icmp" ? [] : formPolicy.ports,
|
||||
protocol: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangePorts = (value: string[]) => {
|
||||
setFormPolicy({
|
||||
...formPolicy,
|
||||
ports: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeDisabled = (checked: boolean) => {
|
||||
setFormPolicy({
|
||||
...formPolicy,
|
||||
enabled: checked,
|
||||
});
|
||||
};
|
||||
|
||||
const handleChangeBidirect = (checked: boolean) => {
|
||||
setFormPolicy({
|
||||
...formPolicy,
|
||||
bidirectional: checked,
|
||||
});
|
||||
};
|
||||
|
||||
const dropDownRenderGroups = (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 dropDownRenderPorts = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{ margin: "8px 0" }} />
|
||||
<Row style={{ padding: "0 8px 4px" }}>
|
||||
<Col flex="auto">
|
||||
<Text type={"secondary"}>Add new ports by pressing "Enter"</Text>
|
||||
</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 toggleEditDescription = (status: boolean) => {
|
||||
setEditDescription(status);
|
||||
};
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = [];
|
||||
if (!value.length) {
|
||||
return Promise.reject(new Error("Please enter at least one group"));
|
||||
}
|
||||
|
||||
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 selectPortRangeValidator = (_: RuleObject, value: string[]) => {
|
||||
if (value) {
|
||||
var failed = false;
|
||||
value.forEach(function (v: string) {
|
||||
let p = Number(v);
|
||||
if (Number.isNaN(p) || p < 1 || p > 65535 || !Number.isInteger(p)) {
|
||||
failed = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
if (failed) {
|
||||
return Promise.reject(
|
||||
new Error("Port value must be in 1..65535 range")
|
||||
);
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const selectPortProtocolValidator = (_: RuleObject, value: string[]) => {
|
||||
if (!formPolicy.bidirectional && value.length === 0) {
|
||||
return Promise.reject(new Error("Directional traffic require ports"));
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const handleDirection = (directionValue: string) => {
|
||||
if (
|
||||
directionValue === "forwardDirectional" &&
|
||||
!direction.reverseDirectional
|
||||
) {
|
||||
setDirection({
|
||||
biDirectional: false,
|
||||
reverseDirectional: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
directionValue === "forwardDirectional" &&
|
||||
direction.reverseDirectional
|
||||
) {
|
||||
setDirection({
|
||||
...direction,
|
||||
biDirectional: !direction.biDirectional,
|
||||
});
|
||||
}
|
||||
|
||||
if (directionValue === "reverseDirectional" && direction.biDirectional) {
|
||||
setDirection({
|
||||
biDirectional: false,
|
||||
reverseDirectional: !direction.reverseDirectional,
|
||||
});
|
||||
}
|
||||
|
||||
if (directionValue === "reverseDirectional" && !direction.biDirectional) {
|
||||
setDirection({
|
||||
biDirectional: true,
|
||||
reverseDirectional: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onBreadcrumbUsersClick = () => {
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const toggleEditName = (status: boolean) => {
|
||||
setEditName(status);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(formPolicy).length > 0) {
|
||||
setFormPolicy({
|
||||
...formPolicy,
|
||||
bidirectional: direction.biDirectional,
|
||||
});
|
||||
}
|
||||
}, [direction]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{policy && (
|
||||
<Container style={{ paddingTop: "40px" }}>
|
||||
<Breadcrumb
|
||||
style={{ marginBottom: "25px" }}
|
||||
items={[
|
||||
{
|
||||
title: <a onClick={onBreadcrumbUsersClick}>Access Control</a>,
|
||||
},
|
||||
{
|
||||
title: policy.name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Card bordered={true} style={{ marginBottom: "7px" }}>
|
||||
<div style={{ maxWidth: "550px" }}>
|
||||
<Form
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
form={form}
|
||||
onValuesChange={onChange}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Header
|
||||
style={{
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
{!editName && (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "22px",
|
||||
margin: "0px",
|
||||
marginBottom: "10px",
|
||||
cursor: "pointer",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
onDoubleClick={() => toggleEditName(true)}
|
||||
>
|
||||
{formPolicy.name}
|
||||
</Paragraph>
|
||||
)}
|
||||
<Row align="top">
|
||||
<Col flex="auto">
|
||||
{editName && (
|
||||
<>
|
||||
<Paragraph
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Rule name
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label=""
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message:
|
||||
"Please add a name for this access rule",
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder={'for example "UserAccessRule"'}
|
||||
ref={inputNameRef}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!editDescription ? (
|
||||
<div
|
||||
style={{
|
||||
margin: "0 0 39px",
|
||||
lineHeight: "22px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => toggleEditDescription(true)}
|
||||
>
|
||||
{formPolicy.description &&
|
||||
formPolicy.description.trim() !== "" ? (
|
||||
formPolicy.description
|
||||
) : (
|
||||
<span style={{ textDecoration: "underline" }}>
|
||||
Add description
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="Description"
|
||||
style={{ marginTop: 10, fontWeight: "500" }}
|
||||
>
|
||||
<Input
|
||||
placeholder="Add description..."
|
||||
ref={inputDescriptionRef}
|
||||
onPressEnter={() =>
|
||||
toggleEditDescription(false)
|
||||
}
|
||||
onBlur={() => toggleEditDescription(false)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Header>
|
||||
</Col>
|
||||
<Col span={24} style={{ marginBottom: "15px" }}>
|
||||
<Form.Item name="enabled" label="">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
onChange={handleChangeDisabled}
|
||||
defaultChecked={policy.enabled}
|
||||
size="small"
|
||||
/>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Enabled
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "0",
|
||||
}}
|
||||
>
|
||||
{formPolicy.enabled
|
||||
? "Disable this rule to apply it later"
|
||||
: "Enable this rule to apply it immediately"}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Row gutter={15}>
|
||||
<Col span={10}>
|
||||
<Form.Item
|
||||
name="tagSourceGroups"
|
||||
label="Source groups"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
style={{ fontWeight: "500" }}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%", fontWeight: "500" }}
|
||||
placeholder="Select groups"
|
||||
tagRender={blueTagRender}
|
||||
onChange={handleChangeSource}
|
||||
dropdownRender={dropDownRenderGroups}
|
||||
optionFilterProp="serchValue"
|
||||
>
|
||||
{tagGroups.map((m, index) => (
|
||||
<Option
|
||||
key={index}
|
||||
value={m.id}
|
||||
serchValue={m.name}
|
||||
>
|
||||
{optionRender(m.name, m.id)}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col
|
||||
span={4}
|
||||
style={{ padding: "0 2.5px", lineHeight: "16px" }}
|
||||
>
|
||||
<Button
|
||||
type={"ghost"}
|
||||
disabled={
|
||||
formPolicy.protocol === "all" ||
|
||||
formPolicy.protocol === "icmp"
|
||||
}
|
||||
onClick={() => handleDirection("forwardDirectional")}
|
||||
style={{
|
||||
padding: "0",
|
||||
width: "100%",
|
||||
marginTop: "30px",
|
||||
height: "13px",
|
||||
}}
|
||||
>
|
||||
<Tag
|
||||
style={{
|
||||
marginInlineEnd: "0",
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
height: "13px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
color={
|
||||
!direction.biDirectional &&
|
||||
!direction.reverseDirectional
|
||||
? "processing"
|
||||
: direction.biDirectional
|
||||
? "green"
|
||||
: "default"
|
||||
}
|
||||
>
|
||||
{!direction.biDirectional &&
|
||||
!direction.reverseDirectional ? (
|
||||
<img
|
||||
src={outBoundblue}
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "45px",
|
||||
}}
|
||||
alt="out icon"
|
||||
/>
|
||||
) : direction.biDirectional ? (
|
||||
<img
|
||||
src={outBoundGreen}
|
||||
alt="out icon"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "45px",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={forwardDefault}
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "45px",
|
||||
}}
|
||||
alt="out icon"
|
||||
/>
|
||||
)}
|
||||
</Tag>
|
||||
</Button>
|
||||
<Button
|
||||
type="ghost"
|
||||
disabled={
|
||||
formPolicy.protocol === "all" ||
|
||||
formPolicy.protocol === "icmp"
|
||||
}
|
||||
onClick={() => handleDirection("reverseDirectional")}
|
||||
style={{
|
||||
padding: "0",
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
height: "13px",
|
||||
marginTop: "0",
|
||||
}}
|
||||
>
|
||||
<Tag
|
||||
style={{
|
||||
marginInlineEnd: "0",
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
height: "13px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
color={
|
||||
direction.reverseDirectional &&
|
||||
direction.biDirectional
|
||||
? "green"
|
||||
: direction.reverseDirectional
|
||||
? "processing"
|
||||
: "default"
|
||||
}
|
||||
>
|
||||
{direction.reverseDirectional &&
|
||||
direction.biDirectional ? (
|
||||
<img
|
||||
src={reverseGreen}
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "45px",
|
||||
}}
|
||||
alt="out icon"
|
||||
/>
|
||||
) : direction.reverseDirectional ? (
|
||||
<img
|
||||
src={inbound}
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "45px",
|
||||
}}
|
||||
alt="out icon"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={reverseDefault}
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "45px",
|
||||
}}
|
||||
alt="out icon"
|
||||
/>
|
||||
)}
|
||||
</Tag>
|
||||
</Button>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Form.Item
|
||||
name="tagDestinationGroups"
|
||||
label="Destination groups"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
style={{ fontWeight: "500" }}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%", fontWeight: "500" }}
|
||||
placeholder="Select groups"
|
||||
tagRender={blueTagRender}
|
||||
onChange={handleChangeDestination}
|
||||
dropdownRender={dropDownRenderGroups}
|
||||
optionFilterProp="serchValue"
|
||||
>
|
||||
{tagGroups.map((m, index) => (
|
||||
<Option
|
||||
key={index}
|
||||
value={m.id}
|
||||
serchValue={m.name}
|
||||
>
|
||||
{optionRender(m.name, m.id)}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={24} style={{ marginBottom: "15px" }}>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{ marginTop: "-15px", marginBottom: "30px" }}
|
||||
>
|
||||
To change traffic direction and ports, select TCP or UDP
|
||||
protocol below
|
||||
</Paragraph>
|
||||
</Col>
|
||||
<Col span={24} style={{ marginBottom: "15px" }}>
|
||||
<Row>
|
||||
<Col span={10}>
|
||||
<Form.Item
|
||||
name="protocol"
|
||||
label="Protocol"
|
||||
style={{ fontWeight: "500" }}
|
||||
className="tag-box"
|
||||
>
|
||||
<Select
|
||||
style={{ width: "100%", maxWidth: "260px" }}
|
||||
options={protocols}
|
||||
onChange={handleChangeProtocol}
|
||||
className="menlo-font"
|
||||
defaultValue={"all"}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Row>
|
||||
<Col span={10}>
|
||||
{formPolicy.protocol === "all" ||
|
||||
formPolicy.protocol === "icmp" ? (
|
||||
<Form.Item
|
||||
label="Ports"
|
||||
style={{ fontWeight: "500" }}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "260px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
placeholder={
|
||||
<div
|
||||
color={"rgba(0,0,0,0.25)"}
|
||||
className="arimo-font"
|
||||
>
|
||||
Select ports
|
||||
</div>
|
||||
}
|
||||
className="menlo-font"
|
||||
value={formPolicyCopy}
|
||||
disabled={true}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="ports"
|
||||
label="Ports"
|
||||
style={{ fontWeight: "500" }}
|
||||
rules={[
|
||||
{
|
||||
message:
|
||||
"Directional traffic requires at least one port",
|
||||
validator: selectPortProtocolValidator,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
message: "Port value must be in 1..65535 range",
|
||||
validator: selectPortRangeValidator,
|
||||
required: false,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "260px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
placeholder={
|
||||
<div
|
||||
color={"rgba(0,0,0,0.25)"}
|
||||
className="arimo-font"
|
||||
>
|
||||
Select ports
|
||||
</div>
|
||||
}
|
||||
tagRender={grayTagRender}
|
||||
onChange={handleChangePorts}
|
||||
className="menlo-font"
|
||||
dropdownRender={dropDownRenderPorts}
|
||||
>
|
||||
{formPolicy &&
|
||||
formPolicy.ports?.map((m) => (
|
||||
<Option key={m}>
|
||||
<Tag style={{ marginRight: 3 }}>{m}</Tag>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</div>
|
||||
<Container
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "start",
|
||||
padding: 0,
|
||||
gap: "10px",
|
||||
marginTop: "12px",
|
||||
}}
|
||||
key={0}
|
||||
>
|
||||
<Button onClick={onCancel} disabled={savedPolicy.loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={savedPolicy.loading}
|
||||
onClick={handleFormSubmit}
|
||||
>{`${formPolicy.id ? "Save" : "Create"}`}</Button>
|
||||
</Container>
|
||||
</Card>
|
||||
</Container>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessControlEdit;
|
||||
@@ -22,7 +22,7 @@ const Banner = () => {
|
||||
const linkLearnMore = () => {
|
||||
return (
|
||||
<a
|
||||
href="https://netbird.io/docs/how-to-guides/nameservers"
|
||||
href="https://docs.netbird.io/how-to/manage-dns-in-your-network"
|
||||
className="font-bold underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
|
||||
@@ -6,7 +6,7 @@ const { Footer } = Layout
|
||||
export default () => {
|
||||
return (
|
||||
<Footer style={{ textAlign: 'center', bottom: "0"}}>
|
||||
Copyright © 2022 <a href="https://netbird.io">NetBird Authors</a>
|
||||
Copyright © 2023 <a href="https://netbird.io">NetBird Authors</a>
|
||||
</Footer>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
896
src/components/NameServerGroupAdd.tsx
Normal file
@@ -0,0 +1,896 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import { actions as nsGroupActions } from "../store/nameservers";
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Switch,
|
||||
Form,
|
||||
FormListFieldData,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {
|
||||
CloseOutlined,
|
||||
MinusCircleOutlined,
|
||||
PlusOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Header } from "antd/es/layout/layout";
|
||||
import { RuleObject } from "antd/lib/form";
|
||||
import cidrRegex from "cidr-regex";
|
||||
import {
|
||||
NameServer,
|
||||
NameServerGroup,
|
||||
NameServerGroupToSave,
|
||||
} from "../store/nameservers/types";
|
||||
import { useGetGroupTagHelpers } from "../utils/groups";
|
||||
import { useGetTokenSilently } from "../utils/token";
|
||||
import { domainValidator } from "../utils/common";
|
||||
|
||||
const { Paragraph, Text } = Typography;
|
||||
|
||||
interface formNSGroup extends NameServerGroup {}
|
||||
|
||||
const NameServerGroupAdd = () => {
|
||||
const {
|
||||
blueTagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator,
|
||||
} = useGetGroupTagHelpers();
|
||||
const dispatch = useDispatch();
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const { Option } = Select;
|
||||
const nsGroup = useSelector(
|
||||
(state: RootState) => state.nameserverGroup.nameserverGroup
|
||||
);
|
||||
const setupNewNameServerGroupVisible = useSelector(
|
||||
(state: RootState) => state.nameserverGroup.setupNewNameServerGroupVisible
|
||||
);
|
||||
const savedNSGroup = useSelector(
|
||||
(state: RootState) => state.nameserverGroup.savedNameServerGroup
|
||||
);
|
||||
const nsGroupData = useSelector(
|
||||
(state: RootState) => state.nameserverGroup.data
|
||||
);
|
||||
|
||||
const [formNSGroup, setFormNSGroup] = useState({} as formNSGroup);
|
||||
const [form] = Form.useForm();
|
||||
const [editName, setEditName] = useState(false);
|
||||
const [isPrimary, setIsPrimary] = useState(false);
|
||||
const [editDescription, setEditDescription] = useState(false);
|
||||
const inputNameRef = useRef<any>(null);
|
||||
const inputDescriptionRef = useRef<any>(null);
|
||||
const [selectCustom, setSelectCustom] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (editName)
|
||||
inputNameRef.current!.focus({
|
||||
cursor: "end",
|
||||
});
|
||||
}, [editName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editDescription)
|
||||
inputDescriptionRef.current!.focus({
|
||||
cursor: "end",
|
||||
});
|
||||
}, [editDescription]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!nsGroup) return;
|
||||
|
||||
let newFormGroup = {
|
||||
...nsGroup,
|
||||
groups: getGroupNamesFromIDs(nsGroup.groups),
|
||||
} as formNSGroup;
|
||||
setFormNSGroup(newFormGroup);
|
||||
form.setFieldsValue(newFormGroup);
|
||||
if (nsGroup.id) {
|
||||
setSelectCustom(true);
|
||||
}
|
||||
if (nsGroup.primary !== undefined) {
|
||||
setIsPrimary(nsGroup.primary);
|
||||
}
|
||||
}, [nsGroup]);
|
||||
|
||||
const onCancel = () => {
|
||||
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(false));
|
||||
dispatch(
|
||||
nsGroupActions.setNameServerGroup({
|
||||
id: "",
|
||||
name: "",
|
||||
description: "",
|
||||
primary: false,
|
||||
domains: [],
|
||||
nameservers: [] as NameServer[],
|
||||
groups: [],
|
||||
enabled: false,
|
||||
} as NameServerGroup)
|
||||
);
|
||||
setEditName(false);
|
||||
setSelectCustom(false);
|
||||
setIsPrimary(false);
|
||||
};
|
||||
|
||||
const onChange = (changedValues: any) => {
|
||||
if (changedValues.primary !== undefined) {
|
||||
setIsPrimary(changedValues.primary);
|
||||
}
|
||||
setFormNSGroup({ ...formNSGroup, ...changedValues });
|
||||
};
|
||||
|
||||
let googleChoice = "Google DNS";
|
||||
let cloudflareChoice = "Cloudflare DNS";
|
||||
let quad9Choice = "Quad9 DNS";
|
||||
let customChoice = "Add custom nameserver";
|
||||
|
||||
let defaultDNSOptions: NameServerGroup[] = [
|
||||
{
|
||||
name: googleChoice,
|
||||
description: "Google DNS servers",
|
||||
domains: [],
|
||||
primary: true,
|
||||
nameservers: [
|
||||
{
|
||||
ip: "8.8.8.8",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
{
|
||||
ip: "8.8.4.4",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
],
|
||||
groups: [],
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: cloudflareChoice,
|
||||
description: "Cloudflare DNS servers",
|
||||
domains: [],
|
||||
primary: true,
|
||||
nameservers: [
|
||||
{
|
||||
ip: "1.1.1.1",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
{
|
||||
ip: "1.0.0.1",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
],
|
||||
groups: [],
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: quad9Choice,
|
||||
description: "Quad9 DNS servers",
|
||||
domains: [],
|
||||
primary: true,
|
||||
nameservers: [
|
||||
{
|
||||
ip: "9.9.9.9",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
{
|
||||
ip: "149.112.112.112",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
],
|
||||
groups: [],
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
const handleSelectChange = (value: string) => {
|
||||
let nsGroupLocal = {} as NameServerGroup;
|
||||
if (value === customChoice) {
|
||||
nsGroupLocal = nsGroup;
|
||||
} else {
|
||||
defaultDNSOptions.forEach((nsg) => {
|
||||
if (value === nsg.name) {
|
||||
nsGroupLocal = nsg;
|
||||
}
|
||||
});
|
||||
}
|
||||
let newFormGroup = {
|
||||
...nsGroupLocal,
|
||||
groups: getGroupNamesFromIDs(nsGroupLocal.groups),
|
||||
} as formNSGroup;
|
||||
setFormNSGroup(newFormGroup);
|
||||
form.setFieldsValue(newFormGroup);
|
||||
setSelectCustom(true);
|
||||
};
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
const nsGroupToSave = createNSGroupToSave(values as NameServerGroup);
|
||||
dispatch(
|
||||
nsGroupActions.saveNameServerGroup.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: nsGroupToSave,
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => onCancel())
|
||||
.catch((errorInfo) => {
|
||||
let msg = "please check the fields and try again";
|
||||
if (errorInfo.errorFields) {
|
||||
msg = errorInfo.errorFields[0].errors[0];
|
||||
}
|
||||
message.error({
|
||||
content: msg,
|
||||
duration: 1,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const createNSGroupToSave = (
|
||||
values: NameServerGroup
|
||||
): NameServerGroupToSave => {
|
||||
let [existingGroups, newGroups] = getExistingAndToCreateGroupsLists(
|
||||
values.groups
|
||||
);
|
||||
return {
|
||||
id: formNSGroup.id || null,
|
||||
name: values.name ? values.name : formNSGroup.name,
|
||||
description: values.description
|
||||
? values.description
|
||||
: formNSGroup.description,
|
||||
primary: values.domains.length ? false : true,
|
||||
domains: values.primary ? [] : values.domains,
|
||||
nameservers: values.nameservers,
|
||||
groups: existingGroups,
|
||||
groupsToCreate: newGroups,
|
||||
enabled: values.enabled,
|
||||
} as NameServerGroupToSave;
|
||||
};
|
||||
|
||||
const toggleEditName = (status: boolean) => {
|
||||
setEditName(status);
|
||||
};
|
||||
|
||||
const toggleEditDescription = (status: boolean) => {
|
||||
setEditDescription(status);
|
||||
};
|
||||
|
||||
const nameValidator = (_: RuleObject, value: string) => {
|
||||
const found = nsGroupData.find(
|
||||
(u) => u.name == value && u.id !== formNSGroup.id
|
||||
);
|
||||
if (found) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
"Please enter a unique name for your nameserver configuration"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const ipValidator = (_: RuleObject, value: string) => {
|
||||
if (!cidrRegex().test(value + "/32")) {
|
||||
return Promise.reject(
|
||||
new Error("Please enter a valid IP, e.g. 192.168.1.1 or 8.8.8.8")
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const formListValidator = (_: RuleObject, names) => {
|
||||
if (names.length >= 3) {
|
||||
return Promise.reject(
|
||||
new Error("Exceeded maximum number of Nameservers. (Max is 2)")
|
||||
);
|
||||
}
|
||||
if (names.length < 1) {
|
||||
return Promise.reject(new Error("You should add at least 1 Nameserver"));
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const renderNSList = (
|
||||
fields: FormListFieldData[],
|
||||
{ add, remove }: any,
|
||||
{ errors }: any
|
||||
) => (
|
||||
<div style={{ width: "100%", maxWidth: "360px" }}>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
marginBottom: "10px",
|
||||
display: "block",
|
||||
}}
|
||||
>
|
||||
Nameservers
|
||||
</label>
|
||||
|
||||
{!!fields.length && (
|
||||
<Row align="middle">
|
||||
<Col span={4} style={{ textAlign: "left" }}>
|
||||
<Typography.Text
|
||||
style={{ color: "#818183", paddingLeft: "5px" }}
|
||||
></Typography.Text>
|
||||
</Col>
|
||||
<Col span={10} style={{ textAlign: "left" }}>
|
||||
<Typography.Text style={{ color: "#818183", paddingLeft: "5px" }}>
|
||||
Nameserver IP
|
||||
</Typography.Text>
|
||||
</Col>
|
||||
<Col span={4} style={{ textAlign: "left" }}>
|
||||
<Typography.Text style={{ color: "#818183", paddingLeft: "5px" }}>
|
||||
Port
|
||||
</Typography.Text>
|
||||
</Col>
|
||||
<Col span={4} />
|
||||
</Row>
|
||||
)}
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<Row key={index}>
|
||||
<Col span={4} style={{ textAlign: "left" }}>
|
||||
<Form.Item
|
||||
style={{ margin: "3px" }}
|
||||
name={[field.name, "ns_type"]}
|
||||
rules={[{ required: true, message: "Missing first protocol" }]}
|
||||
initialValue={"udp"}
|
||||
>
|
||||
<Select
|
||||
disabled
|
||||
style={{ width: "100%" }}
|
||||
className="style-like-text"
|
||||
>
|
||||
<Option value="udp">UDP</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={10} style={{ margin: "1px" }}>
|
||||
<Form.Item
|
||||
style={{ margin: "1px" }}
|
||||
name={[field.name, "ip"]}
|
||||
rules={[{ validator: ipValidator }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="e.g. X.X.X.X"
|
||||
style={{ width: "100%" }}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={4} style={{ textAlign: "center" }}>
|
||||
<Form.Item
|
||||
style={{ margin: "1px" }}
|
||||
name={[field.name, "port"]}
|
||||
rules={[{ required: true, message: "Missing port" }]}
|
||||
initialValue={53}
|
||||
>
|
||||
<InputNumber placeholder="Port" style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col
|
||||
span={2}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<MinusCircleOutlined onClick={() => remove(field.name)} />
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
|
||||
<Row>
|
||||
<Col span={20}>
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add()}
|
||||
block
|
||||
style={{
|
||||
maxWidth: "270px",
|
||||
marginTop: "5px",
|
||||
}}
|
||||
disabled={fields.length > 1}
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
Add nameserver
|
||||
</Button>
|
||||
<Form.ErrorList errors={errors} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
const renderDomains = (
|
||||
fields: FormListFieldData[],
|
||||
{ add, remove }: any,
|
||||
{ errors }: any
|
||||
) => (
|
||||
<>
|
||||
<Row>
|
||||
<Space>
|
||||
<Col>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Match domains
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-4px",
|
||||
fontWeight: "400",
|
||||
marginBottom: "4px",
|
||||
}}
|
||||
>
|
||||
Add domain if you want to have a specific one
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Space>
|
||||
</Row>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<Row key={index} style={{ marginBottom: "5px" }}>
|
||||
<Col span={22}>
|
||||
<Form.Item
|
||||
style={{ margin: "0" }}
|
||||
{...field}
|
||||
rules={[{ validator: domainValidator }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="e.g. example.com"
|
||||
style={{ width: "100%" }}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col
|
||||
span={2}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<MinusCircleOutlined
|
||||
className="dynamic-delete-button"
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
|
||||
<Row>
|
||||
<Col span={24} style={{ margin: "1px" }}>
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => add()}
|
||||
block
|
||||
icon={<PlusOutlined />}
|
||||
style={{ marginTop: "5px" }}
|
||||
>
|
||||
Add Domain
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.ErrorList errors={errors} />
|
||||
</>
|
||||
);
|
||||
|
||||
const handleChangeDisabled = (checked: boolean) => {
|
||||
setFormNSGroup({
|
||||
...formNSGroup,
|
||||
enabled: checked,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{nsGroup && (
|
||||
<Modal
|
||||
forceRender={true}
|
||||
footer={false}
|
||||
onCancel={onCancel}
|
||||
open={setupNewNameServerGroupVisible}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "18px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Add nameserver
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
marginTop: "-20px",
|
||||
fontSize: "14px",
|
||||
paddingBottom: "25px",
|
||||
marginBottom: "4px",
|
||||
}}
|
||||
>
|
||||
Use this nameserver to resolve domains in your network
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
{selectCustom ? (
|
||||
<Form
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
form={form}
|
||||
onValuesChange={onChange}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Header
|
||||
style={{
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
<Row align="top">
|
||||
<Col flex="none" style={{ display: "flex" }}>
|
||||
{!editName && !editDescription && formNSGroup.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 && formNSGroup.id ? (
|
||||
<div
|
||||
className={
|
||||
"access-control input-text ant-drawer-title"
|
||||
}
|
||||
onClick={() => toggleEditName(true)}
|
||||
>
|
||||
{formNSGroup.id
|
||||
? formNSGroup.name
|
||||
: "New nameserver group"}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ lineHeight: "15px" }}>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<Form.Item
|
||||
name="name"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message:
|
||||
"Please add an identifier for this nameserver group",
|
||||
whitespace: true,
|
||||
},
|
||||
{
|
||||
validator: nameValidator,
|
||||
},
|
||||
]}
|
||||
style={{
|
||||
marginBottom: "10px",
|
||||
marginTop: "10px",
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
placeholder="e.g. Public DNS"
|
||||
ref={inputNameRef}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
autoComplete="off"
|
||||
maxLength={40}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
{!editDescription ? (
|
||||
<div
|
||||
className={
|
||||
"access-control input-text ant-drawer-subtitle"
|
||||
}
|
||||
style={{ marginTop: "0" }}
|
||||
onClick={() => toggleEditDescription(true)}
|
||||
>
|
||||
{formNSGroup.description &&
|
||||
formNSGroup.description.trim() !== ""
|
||||
? formNSGroup.description
|
||||
: "Add description"}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{ lineHeight: "15px", marginTop: "24px" }}
|
||||
>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
<Form.Item
|
||||
name="description"
|
||||
style={{ marginTop: "8px" }}
|
||||
>
|
||||
<Input
|
||||
placeholder="Add description..."
|
||||
ref={inputDescriptionRef}
|
||||
onPressEnter={() =>
|
||||
toggleEditDescription(false)
|
||||
}
|
||||
onBlur={() => toggleEditDescription(false)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Header>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.List
|
||||
name="nameservers"
|
||||
rules={[{ validator: formListValidator }]}
|
||||
>
|
||||
{renderNSList}
|
||||
</Form.List>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.List name="domains">{renderDomains}</Form.List>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Distribution groups
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-4px",
|
||||
fontWeight: "400",
|
||||
marginBottom: "4px",
|
||||
}}
|
||||
>
|
||||
Advertise this route to peers that belong to the following
|
||||
groups
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="groups"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Associate groups with the NS group"
|
||||
tagRender={blueTagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
optionFilterProp="serchValue"
|
||||
>
|
||||
{tagGroups.map((m, index) => (
|
||||
<Option key={index} value={m.id} serchValue={m.name}>
|
||||
{optionRender(m.name, m.id)}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item name="enabled" label="">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
onChange={handleChangeDisabled}
|
||||
defaultChecked={formNSGroup.enabled}
|
||||
size="small"
|
||||
/>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Enabled
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "0",
|
||||
}}
|
||||
>
|
||||
{formNSGroup.enabled
|
||||
? "Disable this server if you don't want it to apply immediately"
|
||||
: " Enable this server if you want it to apply immediately"}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col
|
||||
span={24}
|
||||
style={{ marginTop: "10px", marginBottom: "24px" }}
|
||||
>
|
||||
<Text type={"secondary"}>
|
||||
Learn more about
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://docs.netbird.io/how-to/manage-dns-in-your-network"
|
||||
>
|
||||
{" "}
|
||||
DNS
|
||||
</a>
|
||||
</Text>
|
||||
</Col>
|
||||
<Col
|
||||
style={{
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Space
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "end",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Button onClick={onCancel} disabled={savedNSGroup.loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleFormSubmit}
|
||||
disabled={savedNSGroup.loading}
|
||||
>
|
||||
Create nameserver
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
) : (
|
||||
<>
|
||||
<Space direction={"vertical"} style={{ width: "100%" }}>
|
||||
<Row align="middle">
|
||||
<Col span={24} style={{ textAlign: "left" }}>
|
||||
<span className="ant-form-item font-500">
|
||||
Select a predefined one
|
||||
</span>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="middle">
|
||||
<Col span={24} style={{ textAlign: "center" }}>
|
||||
<Select
|
||||
style={{ width: "100%" }}
|
||||
onChange={handleSelectChange}
|
||||
options={[
|
||||
{
|
||||
value: googleChoice,
|
||||
label: googleChoice,
|
||||
},
|
||||
{
|
||||
value: cloudflareChoice,
|
||||
label: cloudflareChoice,
|
||||
},
|
||||
{
|
||||
value: quad9Choice,
|
||||
label: quad9Choice,
|
||||
},
|
||||
{
|
||||
value: customChoice,
|
||||
label: customChoice,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="middle">
|
||||
<Col span={24} style={{ textAlign: "left" }}>
|
||||
<Col span={24} style={{ textAlign: "left" }}>
|
||||
<span className="ant-form-item blue-color">
|
||||
<Typography.Link
|
||||
onClick={() => handleSelectChange(customChoice)}
|
||||
>
|
||||
or create custom
|
||||
</Typography.Link>
|
||||
</span>
|
||||
</Col>
|
||||
</Col>
|
||||
</Row>
|
||||
</Space>
|
||||
<Space
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "end",
|
||||
marginTop: "25px",
|
||||
}}
|
||||
>
|
||||
<Button onClick={onCancel} type="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NameServerGroupAdd;
|
||||
@@ -1,185 +1,244 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Link, useLocation} from 'react-router-dom';
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import logo from "../assets/logo.png";
|
||||
import {Avatar, Button, Col, Dropdown, Grid, Menu, Row} from 'antd'
|
||||
import {ItemType} from "antd/lib/menu/hooks/useItems";
|
||||
import {AvatarSize} from "antd/es/avatar/SizeContext";
|
||||
import {UserOutlined} from '@ant-design/icons';
|
||||
import {useOidc, useOidcIdToken, useOidcUser} from '@axa-fr/react-oidc';
|
||||
import {getConfig} from "../config";
|
||||
import {User} from "../store/user/types";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as userActions} from "../store/user";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {actions as personalAccessTokenActions} from "../store/personal-access-token";
|
||||
import { Avatar, Button, Col, Dropdown, Grid, Menu, Row } from "antd";
|
||||
import { ItemType } from "antd/lib/menu/hooks/useItems";
|
||||
import { AvatarSize } from "antd/es/avatar/SizeContext";
|
||||
import { UserOutlined } from "@ant-design/icons";
|
||||
import { useOidc, useOidcIdToken, useOidcUser } from "@axa-fr/react-oidc";
|
||||
import { getConfig } from "../config";
|
||||
import { User } from "../store/user/types";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import { actions as userActions } from "../store/user";
|
||||
import { useGetTokenSilently } from "../utils/token";
|
||||
import { actions as personalAccessTokenActions } from "../store/personal-access-token";
|
||||
|
||||
const {useBreakpoint} = Grid;
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
const Navbar = () => {
|
||||
let location = useLocation();
|
||||
const config = getConfig();
|
||||
const { logout } = useOidc();
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
let location = useLocation();
|
||||
const config = getConfig();
|
||||
const { logout } = useOidc();
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {oidcUser} = useOidcUser();
|
||||
const {idTokenPayload} = useOidcIdToken()
|
||||
const user = oidcUser;
|
||||
const [currentUser, setCurrentUser] = useState({} as User)
|
||||
const { oidcUser } = useOidcUser();
|
||||
const { idTokenPayload } = useOidcIdToken();
|
||||
const user = oidcUser;
|
||||
const [currentUser, setCurrentUser] = useState({} as User);
|
||||
|
||||
const screens = useBreakpoint();
|
||||
const screens = useBreakpoint();
|
||||
|
||||
const [hideMenuUser, setHideMenuUser] = useState(false)
|
||||
const users = useSelector((state: RootState) => state.user.data)
|
||||
const [isRefreshingUserState, setIsRefreshingUserState] = useState(false)
|
||||
const [hideMenuUser, setHideMenuUser] = useState(false);
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
const [isRefreshingUserState, setIsRefreshingUserState] = useState(false);
|
||||
|
||||
const items = [
|
||||
{label: (<Link to="/peers">Peers</Link>), key: '/peers'},
|
||||
{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="/dns">DNS</Link>), key: '/dns' },
|
||||
{label: (<Link to="/users">Users</Link>), key: '/users'},
|
||||
{label: (<Link to="/activity">Activity</Link>), key: '/activity'},
|
||||
{label: (<Link to="/settings">Settings</Link>), key: '/settings'}
|
||||
] as ItemType[]
|
||||
const items = [
|
||||
{ label: <Link data-testid="peers-page" to="/peers">Peers</Link>, key: "/peers" },
|
||||
{ label: <Link data-testid="setup-keys-page" to="/setup-keys">Setup Keys</Link>, key: "/setup-keys" },
|
||||
{ label: <Link data-testid="access-control-page" to="/acls">Access Control</Link>, key: "/acls" },
|
||||
{ label: <Link data-testid="network-routes-page" to="/routes">Network Routes</Link>, key: "/routes" },
|
||||
{ label: <Link data-testid="dns-page"to="/dns">DNS</Link>, key: "/dns" },
|
||||
{ label: <Link data-testid="usersf-page" to="/users">Users</Link>, key: "/users" },
|
||||
{ label: <Link to="/activity">Activity</Link>, key: "/activity" },
|
||||
{ label: <Link to="/settings">Settings</Link>, key: "/settings" },
|
||||
] as ItemType[];
|
||||
|
||||
const userEmailKey = 'user-email'
|
||||
const userLogoutKey = 'user-logout'
|
||||
const userDividerKey = 'user-divider'
|
||||
const adminOnlyTabs = ["/setup-keys", "/acls", "/routes", "/dns", "/activity", "/settings"]
|
||||
const [menuItems, setMenuItems] = useState(items)
|
||||
const logoutWithRedirect = () =>
|
||||
logout("/", {client_id: config.clientId});
|
||||
const userEmailKey = "user-email";
|
||||
const userLogoutKey = "user-logout";
|
||||
const userDividerKey = "user-divider";
|
||||
const adminOnlyTabs = [
|
||||
"/setup-keys",
|
||||
"/acls",
|
||||
"/routes",
|
||||
"/dns",
|
||||
"/users",
|
||||
"/activity",
|
||||
"/settings",
|
||||
];
|
||||
const [menuItems, setMenuItems] = useState(items);
|
||||
const logoutWithRedirect = () => {
|
||||
let lRemove = [
|
||||
"peerFilter",
|
||||
"setupKeysFilter",
|
||||
"accessControlFilter",
|
||||
"routesFilter",
|
||||
"nameServerFilter",
|
||||
"userFilter",
|
||||
"serviceUserFilter",
|
||||
"activityFilter",
|
||||
];
|
||||
lRemove.forEach((element) => {
|
||||
localStorage.removeItem(element);
|
||||
});
|
||||
logout("/", { client_id: config.clientId });
|
||||
};
|
||||
|
||||
const openPersonalUserPage = () => {
|
||||
dispatch(userActions.setUser({
|
||||
id: currentUser.id,
|
||||
email: currentUser.email,
|
||||
role: currentUser.role,
|
||||
auto_groups: currentUser.auto_groups ? currentUser.auto_groups : [],
|
||||
name: currentUser.name,
|
||||
is_current: currentUser.is_current,
|
||||
is_service_user: currentUser.is_service_user,
|
||||
} as User));
|
||||
dispatch(userActions.setUserTabOpen("Users"));
|
||||
dispatch(userActions.setEditUserPopupVisible(true));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const fs = items.filter(m => showTab(m?.key?.toString(), currentUser) && m?.key !== userEmailKey && m?.key !== userLogoutKey && m?.key !== userDividerKey)
|
||||
if (screens.xs === true) {
|
||||
setHideMenuUser(false)
|
||||
fs.push({type: 'divider', key: userDividerKey})
|
||||
fs.push({
|
||||
label: (
|
||||
<Link to="#">{user?.name}</Link>
|
||||
),
|
||||
icon: createAvatar("small"),
|
||||
key: userEmailKey
|
||||
})
|
||||
fs.push({
|
||||
label: (<Button type="link" block onClick={logoutWithRedirect}>Logout</Button>),
|
||||
key: userLogoutKey
|
||||
})
|
||||
setMenuItems([...fs])
|
||||
return
|
||||
}
|
||||
setMenuItems([...fs])
|
||||
setHideMenuUser(true)
|
||||
}, [screens, currentUser])
|
||||
|
||||
useEffect(() => {
|
||||
if (users.length === 0 && !isRefreshingUserState &&
|
||||
window.location.pathname !== '/peers' &&
|
||||
window.location.pathname !== '/users') {
|
||||
setIsRefreshingUserState(true)
|
||||
dispatch(userActions.getUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}))
|
||||
return
|
||||
}
|
||||
if (users.length === 0 && isRefreshingUserState) {
|
||||
return
|
||||
}
|
||||
let runUser = oidcUser
|
||||
if (!oidcUser) {
|
||||
runUser = idTokenPayload
|
||||
}
|
||||
setIsRefreshingUserState(false)
|
||||
if (runUser) {
|
||||
const found = users.find(u => u.is_current ? u.is_current : runUser.sub ? u.id == runUser.sub : false)
|
||||
if (found) {
|
||||
setCurrentUser(found)
|
||||
}
|
||||
}
|
||||
}, [users, oidcUser])
|
||||
|
||||
const showTab = (key: string | undefined, user: User | undefined) => {
|
||||
if (!user) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (user.role?.toLowerCase() === "admin") {
|
||||
return true
|
||||
}
|
||||
return !adminOnlyTabs.find(t => t === key)
|
||||
}
|
||||
|
||||
const menuUser = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
label: (<Link to="/users" onClick={openPersonalUserPage}>{user?.email}</Link>),
|
||||
key: '0',
|
||||
},
|
||||
{
|
||||
label: (<Link to="/logout" onClick={logoutWithRedirect}>Logout</Link>),
|
||||
key: '1',
|
||||
}
|
||||
]}
|
||||
/>
|
||||
const openPersonalUserPage = () => {
|
||||
dispatch(
|
||||
userActions.setUser({
|
||||
id: currentUser.id,
|
||||
email: currentUser.email,
|
||||
role: currentUser.role,
|
||||
auto_groups: currentUser.auto_groups ? currentUser.auto_groups : [],
|
||||
name: currentUser.name,
|
||||
is_current: currentUser.is_current,
|
||||
is_service_user: currentUser.is_service_user,
|
||||
} as User)
|
||||
);
|
||||
dispatch(userActions.setUserTabOpen("Users"));
|
||||
dispatch(userActions.setEditUserPopupVisible(true));
|
||||
};
|
||||
|
||||
const createAvatar = (size: AvatarSize) => {
|
||||
return user?.picture ? (
|
||||
<Avatar size={size} src={user?.picture} icon={<UserOutlined/>}/>
|
||||
) : (
|
||||
<Avatar size={size}>{(user?.name || '').slice(0, 1).toUpperCase()}</Avatar>
|
||||
)
|
||||
useEffect(() => {
|
||||
const fs = items.filter(
|
||||
(m) =>
|
||||
showTab(m?.key?.toString(), currentUser) &&
|
||||
m?.key !== userEmailKey &&
|
||||
m?.key !== userLogoutKey &&
|
||||
m?.key !== userDividerKey
|
||||
);
|
||||
if (screens.xs === true) {
|
||||
setHideMenuUser(false);
|
||||
fs.push({ type: "divider", key: userDividerKey });
|
||||
fs.push({
|
||||
label: <Link to="#">{user?.name}</Link>,
|
||||
icon: createAvatar("small"),
|
||||
key: userEmailKey,
|
||||
});
|
||||
fs.push({
|
||||
label: (
|
||||
<Button type="link" block onClick={logoutWithRedirect}>
|
||||
Logout
|
||||
</Button>
|
||||
),
|
||||
key: userLogoutKey,
|
||||
});
|
||||
setMenuItems([...fs]);
|
||||
return;
|
||||
}
|
||||
setMenuItems([...fs]);
|
||||
setHideMenuUser(true);
|
||||
}, [screens, currentUser]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
users.length === 0 &&
|
||||
!isRefreshingUserState &&
|
||||
window.location.pathname !== "/peers" &&
|
||||
window.location.pathname !== "/users"
|
||||
) {
|
||||
setIsRefreshingUserState(true);
|
||||
dispatch(
|
||||
userActions.getUsers.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (users.length === 0 && isRefreshingUserState) {
|
||||
return;
|
||||
}
|
||||
let runUser = oidcUser;
|
||||
if (!oidcUser) {
|
||||
runUser = idTokenPayload;
|
||||
}
|
||||
setIsRefreshingUserState(false);
|
||||
if (runUser) {
|
||||
const found = users.find((u) =>
|
||||
u.is_current ? u.is_current : runUser.sub ? u.id == runUser.sub : false
|
||||
);
|
||||
if (found) {
|
||||
setCurrentUser(found);
|
||||
}
|
||||
}
|
||||
}, [users, oidcUser]);
|
||||
|
||||
const showTab = (key: string | undefined, user: User | undefined) => {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row justify="space-evenly" align="middle">
|
||||
<Col flex="0 1 60px">
|
||||
<Link id="logo" to="/">
|
||||
<img
|
||||
alt="logo"
|
||||
style={{width: "55px"}}
|
||||
src={logo}
|
||||
/>
|
||||
</Link>
|
||||
</Col>
|
||||
<Col flex="1 1 auto">
|
||||
<div>
|
||||
<Menu mode="horizontal" selectable={true} selectedKeys={[location.pathname]}
|
||||
onSelect={(e) => {
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null))
|
||||
}}
|
||||
defaultSelectedKeys={[location.pathname]} items={menuItems}/>
|
||||
</div>
|
||||
</Col>
|
||||
{hideMenuUser &&
|
||||
<Col>
|
||||
<Dropdown overlay={menuUser} placement="bottomRight" trigger={['click']}>
|
||||
{createAvatar("large")}
|
||||
</Dropdown>
|
||||
</Col>
|
||||
}
|
||||
</Row>
|
||||
</>
|
||||
if (user.role?.toLowerCase() === "admin") {
|
||||
return true;
|
||||
}
|
||||
return !adminOnlyTabs.find((t) => t === key);
|
||||
};
|
||||
|
||||
const menuUser = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
label: (
|
||||
<Link to="/users" onClick={openPersonalUserPage}>
|
||||
{user?.email}
|
||||
</Link>
|
||||
),
|
||||
key: "0",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Link to="/logout" onClick={logoutWithRedirect}>
|
||||
Logout
|
||||
</Link>
|
||||
),
|
||||
key: "1",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const createAvatar = (size: AvatarSize) => {
|
||||
return user?.picture ? (
|
||||
<Avatar size={size} src={user?.picture} icon={<UserOutlined />} />
|
||||
) : (
|
||||
<Avatar size={size}>
|
||||
{(user?.name || "").slice(0, 1).toUpperCase()}
|
||||
</Avatar>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row justify="space-evenly" align="middle">
|
||||
<Col flex="0 1 60px">
|
||||
<Link id="logo" to="/">
|
||||
<img alt="logo" style={{ width: "55px" }} src={logo} />
|
||||
</Link>
|
||||
</Col>
|
||||
<Col flex="1 1 auto">
|
||||
<div>
|
||||
<Menu
|
||||
mode="horizontal"
|
||||
selectable={true}
|
||||
selectedKeys={[location.pathname]}
|
||||
onSelect={(e) => {
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
dispatch(
|
||||
personalAccessTokenActions.resetPersonalAccessTokens(null)
|
||||
);
|
||||
}}
|
||||
defaultSelectedKeys={[location.pathname]}
|
||||
items={menuItems}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
{hideMenuUser && (
|
||||
<Col>
|
||||
<Dropdown
|
||||
overlay={menuUser}
|
||||
placement="bottomRight"
|
||||
trigger={["click"]}
|
||||
>
|
||||
{createAvatar("large")}
|
||||
</Dropdown>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
|
||||
913
src/components/RouteAddNew.tsx
Normal file
@@ -0,0 +1,913 @@
|
||||
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 {
|
||||
Button,
|
||||
Col,
|
||||
Collapse,
|
||||
Form,
|
||||
Input,
|
||||
message,
|
||||
InputNumber,
|
||||
Row,
|
||||
Select,
|
||||
SelectProps,
|
||||
Space,
|
||||
Switch,
|
||||
Modal,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import CreatableSelect from "react-select/creatable";
|
||||
import { Route, RouteToSave } from "../store/route/types";
|
||||
import { Header } from "antd/es/layout/layout";
|
||||
import { RuleObject } from "antd/lib/form";
|
||||
import cidrRegex from "cidr-regex";
|
||||
import {
|
||||
initPeerMaps,
|
||||
peerToPeerIP,
|
||||
routePeerSeparator,
|
||||
transformGroupedDataTable,
|
||||
} from "../utils/routes";
|
||||
import { useGetTokenSilently } from "../utils/token";
|
||||
import { useGetGroupTagHelpers } from "../utils/groups";
|
||||
|
||||
const { Paragraph, Text } = Typography;
|
||||
const { Panel } = Collapse;
|
||||
|
||||
interface FormRoute extends Route {}
|
||||
|
||||
const RouteAddNew = (selectedPeer: any) => {
|
||||
const {
|
||||
blueTagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator,
|
||||
} = useGetGroupTagHelpers();
|
||||
|
||||
const { Option } = Select;
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
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 [previousRouteKey, setPreviousRouteKey] = useState("");
|
||||
const [editName, setEditName] = useState(false);
|
||||
const [editDescription, setEditDescription] = useState(false);
|
||||
const options: SelectProps["options"] = [];
|
||||
const testOptions: SelectProps["options"] = [];
|
||||
const [formRoute, setFormRoute] = useState({} as FormRoute);
|
||||
const [form] = Form.useForm();
|
||||
const inputNameRef = useRef<any>(null);
|
||||
const inputDescriptionRef = useRef<any>(null);
|
||||
const [enableNetwork, setEnableNetwork] = useState(false);
|
||||
const [peerNameToIP, peerIPToName, peerIPToID] = initPeerMaps(peers);
|
||||
const [newRoute, setNewRoute] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (editName)
|
||||
inputNameRef.current!.focus({
|
||||
cursor: "end",
|
||||
});
|
||||
}, [editName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editDescription)
|
||||
inputDescriptionRef.current!.focus({
|
||||
cursor: "end",
|
||||
});
|
||||
}, [editDescription]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!route) return;
|
||||
|
||||
if (selectedPeer && selectedPeer.selectedPeer) {
|
||||
options?.push({
|
||||
label: peerToPeerIP(
|
||||
selectedPeer.selectedPeer.name,
|
||||
selectedPeer.selectedPeer.ip
|
||||
),
|
||||
value: peerToPeerIP(
|
||||
selectedPeer.selectedPeer.name,
|
||||
selectedPeer.selectedPeer.ip
|
||||
),
|
||||
disabled: false,
|
||||
});
|
||||
const udpateRoute = { ...route, peer: options[0].value } as FormRoute;
|
||||
setFormRoute(udpateRoute);
|
||||
form.setFieldsValue(udpateRoute);
|
||||
setPreviousRouteKey(udpateRoute.network_id + udpateRoute.network);
|
||||
} else {
|
||||
const fRoute = {
|
||||
...route,
|
||||
groups: getGroupNamesFromIDs(route.groups),
|
||||
} as FormRoute;
|
||||
setFormRoute(fRoute);
|
||||
setPreviousRouteKey(fRoute.network_id + fRoute.network);
|
||||
form.setFieldsValue(fRoute);
|
||||
}
|
||||
|
||||
if (!route.network_id) {
|
||||
setNewRoute(true);
|
||||
} else {
|
||||
setNewRoute(false);
|
||||
}
|
||||
}, [route]);
|
||||
|
||||
selectedPeer &&
|
||||
selectedPeer.notPeerRoutes &&
|
||||
selectedPeer.notPeerRoutes.forEach((element: any, index: number) => {
|
||||
testOptions?.push({
|
||||
label: element.network_id + " - " + element.network,
|
||||
value: element.network_id + "+" + index,
|
||||
network: element.network,
|
||||
disabled: false,
|
||||
key: index,
|
||||
});
|
||||
});
|
||||
if (!selectedPeer.selectedPeer) {
|
||||
peers.forEach((p) => {
|
||||
let os: string;
|
||||
os = p.os;
|
||||
if (
|
||||
!os.toLowerCase().startsWith("darwin") &&
|
||||
!os.toLowerCase().startsWith("windows") &&
|
||||
!os.toLowerCase().startsWith("android") &&
|
||||
route &&
|
||||
!routes
|
||||
.filter((r) => r.network_id === route.network_id)
|
||||
.find((r) => r.peer === p.id)
|
||||
) {
|
||||
options?.push({
|
||||
label: peerToPeerIP(p.name, p.ip),
|
||||
value: peerToPeerIP(p.name, p.ip),
|
||||
disabled: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const createRouteToSave = (inputRoute: FormRoute): RouteToSave => {
|
||||
let peerIDList = inputRoute.peer.split(routePeerSeparator);
|
||||
let peerID: string;
|
||||
if (peerIDList.length === 1) {
|
||||
peerID = inputRoute.peer;
|
||||
} else {
|
||||
if (peerIDList[1]) {
|
||||
peerID = peerIPToID[peerIDList[1]];
|
||||
} else {
|
||||
peerID = peerIPToID[peerNameToIP[inputRoute.peer]];
|
||||
}
|
||||
}
|
||||
|
||||
let [existingGroups, groupsToCreate] = getExistingAndToCreateGroupsLists(
|
||||
inputRoute.groups
|
||||
);
|
||||
|
||||
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,
|
||||
groups: existingGroups,
|
||||
groupsToCreate: groupsToCreate,
|
||||
} as RouteToSave;
|
||||
};
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
if (!setupNewRouteHA || formRoute.peer != "") {
|
||||
const routeToSave = createRouteToSave(formRoute);
|
||||
dispatch(
|
||||
routeActions.saveRoute.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: routeToSave,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
let groupedDataTable = transformGroupedDataTable(routes, peers);
|
||||
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: getTokenSilently,
|
||||
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,
|
||||
groups: [],
|
||||
} as Route)
|
||||
);
|
||||
setVisibleNewRoute(false);
|
||||
setSetupNewRouteHA(false);
|
||||
setPreviousRouteKey("");
|
||||
setNewRoute(false);
|
||||
};
|
||||
|
||||
const onChange = (data: any) => {
|
||||
setFormRoute({ ...formRoute, ...data });
|
||||
};
|
||||
|
||||
const peerDropDownRender = (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();
|
||||
};
|
||||
|
||||
const peerValidator = (_: RuleObject, value: string) => {
|
||||
if (value == "" && newRoute) {
|
||||
return Promise.reject(new Error("Please select routing one peer"));
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const selectPreValidator = (obj: RuleObject, value: string[]) => {
|
||||
if (setupNewRouteHA && formRoute.peer == "") {
|
||||
let [, newGroups] = getExistingAndToCreateGroupsLists(value);
|
||||
if (newGroups.length > 0) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
"You can't add new Groups from the group update view, please remove:\"" +
|
||||
newGroups +
|
||||
'"'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return selectValidator(obj, value);
|
||||
};
|
||||
|
||||
const handleMasqueradeChange = (checked: boolean) => {
|
||||
setFormRoute({
|
||||
...formRoute,
|
||||
masquerade: checked,
|
||||
});
|
||||
};
|
||||
|
||||
const handleEnableChange = (checked: boolean) => {
|
||||
setFormRoute({
|
||||
...formRoute,
|
||||
enabled: checked,
|
||||
});
|
||||
};
|
||||
|
||||
const onNetworkChange = (selectedOption: any) => {
|
||||
if (selectedOption === null) {
|
||||
const updateNetwork = {
|
||||
...formRoute,
|
||||
network: "",
|
||||
network_id: "",
|
||||
};
|
||||
form.setFieldsValue(updateNetwork);
|
||||
setFormRoute(updateNetwork);
|
||||
setEnableNetwork(false);
|
||||
} else if (!!selectedOption.__isNew__) {
|
||||
const updateNetwork = {
|
||||
...formRoute,
|
||||
network: "",
|
||||
network_id: selectedOption.value.split("+")[0],
|
||||
};
|
||||
form.setFieldsValue(updateNetwork);
|
||||
setFormRoute(updateNetwork);
|
||||
setEnableNetwork(false);
|
||||
} else {
|
||||
const updateNetwork = {
|
||||
...formRoute,
|
||||
network: selectedOption.network,
|
||||
network_id: selectedOption.value.split("+")[0],
|
||||
};
|
||||
form.setFieldsValue(updateNetwork);
|
||||
setFormRoute(updateNetwork);
|
||||
setEnableNetwork(true);
|
||||
}
|
||||
};
|
||||
|
||||
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 added.",
|
||||
key: saveKey,
|
||||
duration: 2,
|
||||
style: styleNotification,
|
||||
});
|
||||
dispatch(routeActions.setSetupNewRouteVisible(false));
|
||||
dispatch(routeActions.setSetupEditRouteVisible(false));
|
||||
dispatch(routeActions.setSetupEditRoutePeerVisible(false));
|
||||
dispatch(routeActions.setSavedRoute({ ...savedRoute, success: false }));
|
||||
dispatch(routeActions.resetSavedRoute(null));
|
||||
} else if (savedRoute.error) {
|
||||
let errorMsg = "Failed to update network route";
|
||||
switch (savedRoute.error.statusCode) {
|
||||
case 403:
|
||||
errorMsg =
|
||||
"Failed to update network route. You might not have enough permissions.";
|
||||
break;
|
||||
default:
|
||||
errorMsg = savedRoute.error.data.message
|
||||
? savedRoute.error.data.message
|
||||
: errorMsg;
|
||||
break;
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: saveKey,
|
||||
duration: 5,
|
||||
style: styleNotification,
|
||||
});
|
||||
dispatch(routeActions.setSavedRoute({ ...savedRoute, error: null }));
|
||||
dispatch(routeActions.resetSavedRoute(null));
|
||||
}
|
||||
}, [savedRoute]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{route && (
|
||||
<Modal
|
||||
open={setupNewRouteVisible}
|
||||
onCancel={onCancel}
|
||||
footer={
|
||||
<Space style={{ display: "flex", justifyContent: "end" }}>
|
||||
<Button onClick={onCancel} disabled={savedRoute.loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={savedRoute.loading}
|
||||
onClick={handleFormSubmit}
|
||||
>
|
||||
Add route
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
layout="vertical"
|
||||
form={form}
|
||||
requiredMark={false}
|
||||
onValuesChange={onChange}
|
||||
className="route-form"
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Header
|
||||
style={{
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "18px",
|
||||
margin: "0px",
|
||||
fontWeight: 500,
|
||||
marginBottom: "25px",
|
||||
}}
|
||||
>
|
||||
Add route
|
||||
</Paragraph>
|
||||
|
||||
{!!selectedPeer.selectedPeer && (
|
||||
<div style={{ lineHeight: "20px" }}>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Routing Peer
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Assign a peer as a routing peer for the Network CIDR
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="peer"
|
||||
rules={[{ validator: peerValidator }]}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Select Peer"
|
||||
dropdownRender={peerDropDownRender}
|
||||
options={options}
|
||||
allowClear={true}
|
||||
disabled={!!selectedPeer.selectedPeer}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Row align="top">
|
||||
<Col span={24} style={{ lineHeight: "20px" }}>
|
||||
{!editName && formRoute.id ? (
|
||||
<div
|
||||
className={
|
||||
"access-control input-text ant-drawer-title"
|
||||
}
|
||||
onClick={() => toggleEditName(true)}
|
||||
>
|
||||
{formRoute.id ? formRoute.network_id : "New Route"}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ marginBottom: "15px" }}>
|
||||
{!!selectedPeer.selectedPeer && (
|
||||
<>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Network Identifier
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Add a unique cryptographic key that is assigned
|
||||
to each device
|
||||
</Paragraph>
|
||||
<CreatableSelect
|
||||
isClearable
|
||||
className="ant-select-selector-custom"
|
||||
options={testOptions}
|
||||
onChange={onNetworkChange}
|
||||
placeholder="Select an existing network or add a new one"
|
||||
classNamePrefix="react-select"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!!!selectedPeer.selectedPeer && (
|
||||
<>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Network Identifier
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Add a unique cryptographic key that is assigned
|
||||
to each device
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="network_id"
|
||||
label=""
|
||||
style={{ marginBottom: "10px" }}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message:
|
||||
"Please add an identifier for this access route",
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="for example “e.g. aws-eu-central-1-vpc”"
|
||||
ref={inputNameRef}
|
||||
disabled={!setupNewRouteHA && !newRoute}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
autoComplete="off"
|
||||
maxLength={40}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!editDescription ? (
|
||||
<div
|
||||
onClick={() => toggleEditDescription(true)}
|
||||
style={{
|
||||
margin: "0 0 15px",
|
||||
lineHeight: "22px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{formRoute.description &&
|
||||
formRoute.description.trim() !== "" ? (
|
||||
formRoute.description
|
||||
) : (
|
||||
<span style={{ textDecoration: "underline" }}>
|
||||
Add description
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="Description"
|
||||
style={{ marginTop: 24, fontWeight: 500 }}
|
||||
>
|
||||
<Input
|
||||
placeholder="Add description..."
|
||||
ref={inputDescriptionRef}
|
||||
disabled={!setupNewRouteHA && !newRoute}
|
||||
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}>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Network Range
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Add a private IP address range
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="network"
|
||||
label=""
|
||||
rules={[{ validator: networkRangeValidator }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="for example “172.16.0.0/16”"
|
||||
disabled={(!setupNewRouteHA && !newRoute) || enableNetwork}
|
||||
autoComplete="off"
|
||||
minLength={9}
|
||||
maxLength={43}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
{!!!selectedPeer.selectedPeer && (
|
||||
<Col span={24}>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Routing Peer
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Assign a peer as a routing peer for the Network CIDR
|
||||
</Paragraph>
|
||||
<Form.Item name="peer" rules={[{ validator: peerValidator }]}>
|
||||
<Select
|
||||
showSearch
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Select Peer"
|
||||
dropdownRender={peerDropDownRender}
|
||||
options={options}
|
||||
allowClear={true}
|
||||
disabled={!!selectedPeer.selectedPeer}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
<Col span={24}>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Distribution groups
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Advertise this route to peers that belong to the following
|
||||
groups
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="groups"
|
||||
label=""
|
||||
rules={[{ validator: selectPreValidator }]}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Associate groups with the network route"
|
||||
tagRender={blueTagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
optionFilterProp="serchValue"
|
||||
>
|
||||
{tagGroups.map((m, index) => (
|
||||
<Option key={index} value={m.id} serchValue={m.name}>
|
||||
{optionRender(m.name, m.id)}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Form.Item name="enabled" label="">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
size={"small"}
|
||||
checked={formRoute.enabled}
|
||||
onChange={handleEnableChange}
|
||||
/>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Enabled
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "0",
|
||||
}}
|
||||
>
|
||||
You can enable or disable the route
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Collapse
|
||||
onChange={onChange}
|
||||
bordered={false}
|
||||
ghost={true}
|
||||
style={{ padding: "0" }}
|
||||
className="remove-bg"
|
||||
>
|
||||
<Panel
|
||||
key="0"
|
||||
header={
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "left",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "14px",
|
||||
fontWeight: "400",
|
||||
margin: "0",
|
||||
textDecoration: "underline",
|
||||
}}
|
||||
>
|
||||
More settings
|
||||
</Paragraph>
|
||||
}
|
||||
className="system-info-panel"
|
||||
>
|
||||
<Row gutter={16} style={{ padding: "15px 0 0" }}>
|
||||
<Col span={22}>
|
||||
<Form.Item name="masquerade" label="">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
size={"small"}
|
||||
disabled={!setupNewRouteHA && !newRoute}
|
||||
checked={formRoute.masquerade}
|
||||
onChange={handleMasqueradeChange}
|
||||
/>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Masquerade
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "0",
|
||||
}}
|
||||
>
|
||||
Allow access to your private networks without
|
||||
configuring routes on your local routers or
|
||||
other devices.
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Metric
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Lower metrics indicating higher priority routes
|
||||
</Paragraph>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item name="metric" label="">
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={9999}
|
||||
autoComplete="off"
|
||||
className="w-100"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</Col>
|
||||
<Col
|
||||
span={24}
|
||||
style={{ marginTop: "24px", marginBottom: "12px" }}
|
||||
>
|
||||
<Text type={"secondary"}>
|
||||
Learn more about
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://docs.netbird.io/how-to/routing-traffic-to-private-networks"
|
||||
>
|
||||
{" "}
|
||||
network routes
|
||||
</a>
|
||||
</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RouteAddNew;
|
||||
624
src/components/RoutePeerUpdate.tsx
Normal file
@@ -0,0 +1,624 @@
|
||||
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 {
|
||||
Button,
|
||||
Col,
|
||||
Collapse,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
Row,
|
||||
Select,
|
||||
SelectProps,
|
||||
Breadcrumb,
|
||||
Switch,
|
||||
Card,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import { Route, RouteToSave } from "../store/route/types";
|
||||
import { Header } from "antd/es/layout/layout";
|
||||
import { RuleObject } from "antd/lib/form";
|
||||
import {
|
||||
initPeerMaps,
|
||||
peerToPeerIP,
|
||||
routePeerSeparator,
|
||||
} from "../utils/routes";
|
||||
import { useGetTokenSilently } from "../utils/token";
|
||||
import { useGetGroupTagHelpers } from "../utils/groups";
|
||||
import { Container } from "./Container";
|
||||
|
||||
const { Paragraph, Text } = Typography;
|
||||
const { Panel } = Collapse;
|
||||
|
||||
interface FormRoute extends Route {}
|
||||
|
||||
const RoutePeerUpdate = () => {
|
||||
const {
|
||||
blueTagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator,
|
||||
} = useGetGroupTagHelpers();
|
||||
|
||||
const { Option } = Select;
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
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 [editDescription, setEditDescription] = useState(false);
|
||||
const options: SelectProps["options"] = [];
|
||||
const [formRoute, setFormRoute] = useState({} as FormRoute);
|
||||
const [form] = Form.useForm();
|
||||
const inputDescriptionRef = useRef<any>(null);
|
||||
const [peerNameToIP, peerIPToName, peerIPToID] = initPeerMaps(peers);
|
||||
|
||||
useEffect(() => {
|
||||
if (editDescription)
|
||||
inputDescriptionRef.current!.focus({
|
||||
cursor: "end",
|
||||
});
|
||||
}, [editDescription]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!route) return;
|
||||
const fRoute = {
|
||||
...route,
|
||||
groups: route.groups,
|
||||
} as FormRoute;
|
||||
setFormRoute(fRoute);
|
||||
form.setFieldsValue(fRoute);
|
||||
// let options = [];
|
||||
}, [route]);
|
||||
|
||||
peers.forEach((p) => {
|
||||
let os: string;
|
||||
os = p.os;
|
||||
if (
|
||||
!os.toLowerCase().startsWith("darwin") &&
|
||||
!os.toLowerCase().startsWith("windows") &&
|
||||
!os.toLowerCase().startsWith("android") &&
|
||||
route &&
|
||||
!routes
|
||||
.filter((r) => r.network_id === route.network_id)
|
||||
.find((r) => r.peer === p.id)
|
||||
) {
|
||||
options?.push({
|
||||
label: peerToPeerIP(p.name, p.ip),
|
||||
value: peerToPeerIP(p.name, p.ip),
|
||||
disabled: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const createRouteToSave = (inputRoute: FormRoute): RouteToSave => {
|
||||
let peerIDList = inputRoute.peer.split(routePeerSeparator);
|
||||
let peerID: string;
|
||||
if (peerIDList.length === 1) {
|
||||
peerID = inputRoute.peer;
|
||||
} else {
|
||||
if (peerIDList[1]) {
|
||||
peerID = peerIPToID[peerIDList[1]];
|
||||
} else {
|
||||
peerID = peerIPToID[peerNameToIP[inputRoute.peer]];
|
||||
}
|
||||
}
|
||||
let [existingGroups, groupsToCreate] = getExistingAndToCreateGroupsLists(
|
||||
inputRoute.groups
|
||||
);
|
||||
|
||||
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,
|
||||
groups: existingGroups,
|
||||
groupsToCreate: groupsToCreate,
|
||||
} as RouteToSave;
|
||||
};
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
if (formRoute.peer !== "") {
|
||||
const routeToSave = createRouteToSave(formRoute);
|
||||
dispatch(
|
||||
routeActions.saveRoute.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: routeToSave,
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log("errorInfo", errorInfo);
|
||||
});
|
||||
};
|
||||
|
||||
const setVisibleNewRoute = (status: boolean) => {
|
||||
dispatch(routeActions.setSetupEditRoutePeerVisible(status));
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedRoute.loading) return;
|
||||
dispatch(
|
||||
routeActions.setRoute({
|
||||
network: "",
|
||||
network_id: "",
|
||||
description: "",
|
||||
peer: "",
|
||||
metric: 9999,
|
||||
masquerade: false,
|
||||
enabled: true,
|
||||
groups: [],
|
||||
} as Route)
|
||||
);
|
||||
setVisibleNewRoute(false);
|
||||
};
|
||||
|
||||
const onChange = (data: any) => {
|
||||
setFormRoute({ ...formRoute, ...data });
|
||||
};
|
||||
|
||||
const peerDropDownRender = (menu: React.ReactElement) => <>{menu}</>;
|
||||
|
||||
const toggleEditDescription = (status: boolean) => {
|
||||
setEditDescription(status);
|
||||
};
|
||||
|
||||
const peerValidator = (_: RuleObject, value: string) => {
|
||||
if (value == "") {
|
||||
return Promise.reject(new Error("Please select routing one peer"));
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const selectPreValidator = (obj: RuleObject, value: string[]) => {
|
||||
if (formRoute.peer === "") {
|
||||
let [, newGroups] = getExistingAndToCreateGroupsLists(value);
|
||||
if (newGroups.length > 0) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
"You can't add new Groups from the group update view, please remove:\"" +
|
||||
newGroups +
|
||||
'"'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return selectValidator(obj, value);
|
||||
};
|
||||
|
||||
const handleMasqueradeChange = (checked: boolean) => {
|
||||
setFormRoute({
|
||||
...formRoute,
|
||||
masquerade: checked,
|
||||
});
|
||||
};
|
||||
|
||||
const handleEnableChange = (checked: boolean) => {
|
||||
setFormRoute({
|
||||
...formRoute,
|
||||
enabled: checked,
|
||||
});
|
||||
};
|
||||
|
||||
const onBreadcrumbUsersClick = () => {
|
||||
onCancel();
|
||||
};
|
||||
|
||||
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.setSetupEditRouteVisible(false));
|
||||
dispatch(routeActions.setSetupEditRoutePeerVisible(false));
|
||||
dispatch(routeActions.setSavedRoute({ ...savedRoute, success: false }));
|
||||
dispatch(routeActions.resetSavedRoute(null));
|
||||
} else if (savedRoute.error) {
|
||||
let errorMsg = "Failed to update network route";
|
||||
switch (savedRoute.error.statusCode) {
|
||||
case 403:
|
||||
errorMsg =
|
||||
"Failed to update network route. You might not have enough permissions.";
|
||||
break;
|
||||
default:
|
||||
errorMsg = savedRoute.error.data.message
|
||||
? savedRoute.error.data.message
|
||||
: errorMsg;
|
||||
break;
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: saveKey,
|
||||
duration: 5,
|
||||
style: styleNotification,
|
||||
});
|
||||
dispatch(routeActions.setSavedRoute({ ...savedRoute, error: null }));
|
||||
dispatch(routeActions.resetSavedRoute(null));
|
||||
}
|
||||
}, [savedRoute]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container style={{ paddingTop: "40px", paddingBottom: "50px" }}>
|
||||
<Breadcrumb
|
||||
style={{ marginBottom: "25px" }}
|
||||
items={[
|
||||
{
|
||||
title: <a onClick={onBreadcrumbUsersClick}>Network Routes</a>,
|
||||
},
|
||||
{
|
||||
title: formRoute.network_id,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{route && (
|
||||
<Card>
|
||||
<Form
|
||||
layout="vertical"
|
||||
form={form}
|
||||
requiredMark={false}
|
||||
onValuesChange={onChange}
|
||||
style={{ width: "100%", maxWidth: "600px" }}
|
||||
className="route-form edit-form-wrapper"
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Header
|
||||
style={{
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
<Row align="top">
|
||||
<Col span={24} style={{ lineHeight: "20px" }}>
|
||||
<div
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontWeight: "500",
|
||||
fontSize: "22px",
|
||||
}}
|
||||
>
|
||||
{formRoute.network_id}
|
||||
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
textAlign: "left",
|
||||
whiteSpace: "pre-line",
|
||||
fontWeight: "400",
|
||||
marginBottom: "0",
|
||||
}}
|
||||
>
|
||||
<div style={{ margin: "5px 0" }}>
|
||||
{" "}
|
||||
{formRoute.network}
|
||||
</div>
|
||||
<div></div>
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
{!editDescription ? (
|
||||
<div
|
||||
onClick={() => toggleEditDescription(true)}
|
||||
style={{
|
||||
margin: "0 0 30px",
|
||||
lineHeight: "22px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{formRoute.description &&
|
||||
formRoute.description.trim() !== "" ? (
|
||||
formRoute.description
|
||||
) : (
|
||||
<span style={{ textDecoration: "underline" }}>
|
||||
Add description
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="Description"
|
||||
style={{ marginTop: 24, fontWeight: 500 }}
|
||||
>
|
||||
<Input
|
||||
placeholder="Add description..."
|
||||
ref={inputDescriptionRef}
|
||||
onPressEnter={() => toggleEditDescription(false)}
|
||||
onBlur={() => toggleEditDescription(false)}
|
||||
autoComplete="off"
|
||||
style={{ maxWidth: "400px" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="top">
|
||||
<Col flex="auto"></Col>
|
||||
</Row>
|
||||
</Header>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Form.Item name="enabled" label="">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
size={"small"}
|
||||
checked={formRoute.enabled}
|
||||
onChange={handleEnableChange}
|
||||
/>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Enabled
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "0",
|
||||
}}
|
||||
>
|
||||
You can enable or disable the route
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Routing Peer
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Assign a peer as a routing peer for the Network CIDR
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="peer"
|
||||
rules={[{ validator: peerValidator }]}
|
||||
style={{ maxWidth: "400px" }}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Select Peer"
|
||||
dropdownRender={peerDropDownRender}
|
||||
options={options}
|
||||
allowClear={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Distribution groups
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Advertise this route to peers that belong to the following
|
||||
groups
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="groups"
|
||||
label=""
|
||||
rules={[{ validator: selectPreValidator }]}
|
||||
style={{ maxWidth: "400px" }}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Associate groups with the network route"
|
||||
tagRender={blueTagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
optionFilterProp="serchValue"
|
||||
>
|
||||
{tagGroups.map((m, index) => (
|
||||
<Option key={index} value={m.id} serchValue={m.name}>
|
||||
{optionRender(m.name, m.id)}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Collapse
|
||||
onChange={onChange}
|
||||
bordered={false}
|
||||
ghost={true}
|
||||
style={{ padding: "0" }}
|
||||
className="remove-bg"
|
||||
>
|
||||
<Panel
|
||||
key="0"
|
||||
header={
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "left",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "14px",
|
||||
fontWeight: "400",
|
||||
margin: "0",
|
||||
textDecoration: "underline",
|
||||
}}
|
||||
>
|
||||
More settings
|
||||
</Paragraph>
|
||||
}
|
||||
className="system-info-panel"
|
||||
>
|
||||
<Row gutter={16} style={{ padding: "15px 0 0" }}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="masquerade" label="">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
size={"small"}
|
||||
checked={formRoute.masquerade}
|
||||
onChange={handleMasqueradeChange}
|
||||
/>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Masquerade
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "0",
|
||||
}}
|
||||
>
|
||||
Allow access to your private networks without
|
||||
configuring routes on your local routers or
|
||||
other devices.
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Metric
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Lower metrics indicating higher priority routes
|
||||
</Paragraph>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item name="metric" label="">
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={9999}
|
||||
autoComplete="off"
|
||||
className="w-100"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</Col>
|
||||
</Row>
|
||||
<Col
|
||||
span={24}
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "start",
|
||||
gap: "10px",
|
||||
marginTop: "20px",
|
||||
}}
|
||||
>
|
||||
<Button onClick={onCancel} disabled={savedRoute.loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={savedRoute.loading}
|
||||
onClick={handleFormSubmit}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Col>
|
||||
</Form>
|
||||
</Card>
|
||||
)}
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoutePeerUpdate;
|
||||
@@ -1,472 +1,496 @@
|
||||
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 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 {
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Drawer,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Radio,
|
||||
Row,
|
||||
Select,
|
||||
SelectProps,
|
||||
Space,
|
||||
Switch,
|
||||
Typography
|
||||
Button,
|
||||
Col,
|
||||
Form,
|
||||
Input,
|
||||
Row,
|
||||
Select,
|
||||
SelectProps,
|
||||
message,
|
||||
Space,
|
||||
Modal,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {CloseOutlined, FlagFilled, QuestionCircleFilled} from "@ant-design/icons";
|
||||
import {Route, RouteToSave} from "../store/route/types";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import cidrRegex from 'cidr-regex';
|
||||
import { Route, RouteToSave } from "../store/route/types";
|
||||
import { Header } from "antd/es/layout/layout";
|
||||
import { RuleObject } from "antd/lib/form";
|
||||
import {
|
||||
initPeerMaps,
|
||||
masqueradeDisabledMSG,
|
||||
peerToPeerIP,
|
||||
routePeerSeparator,
|
||||
transformGroupedDataTable
|
||||
} from '../utils/routes'
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
initPeerMaps,
|
||||
peerToPeerIP,
|
||||
routePeerSeparator,
|
||||
transformGroupedDataTable,
|
||||
} from "../utils/routes";
|
||||
import { useGetTokenSilently } from "../utils/token";
|
||||
import { useGetGroupTagHelpers } from "../utils/groups";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
interface FormRoute extends Route {
|
||||
}
|
||||
interface FormRoute extends Route {}
|
||||
|
||||
const RouteUpdate = () => {
|
||||
const {
|
||||
tagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator
|
||||
} = useGetGroupTagHelpers()
|
||||
const {Option} = Select;
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
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 [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 RouteAddNew = () => {
|
||||
const {
|
||||
blueTagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator,
|
||||
} = useGetGroupTagHelpers();
|
||||
|
||||
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, peerIPToID] = initPeerMaps(peers);
|
||||
const [newRoute, setNewRoute] = useState(false)
|
||||
const { Option } = Select;
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
const setupEditRouteVisible = useSelector(
|
||||
(state: RootState) => state.route.setupEditRouteVisible
|
||||
);
|
||||
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 [previousRouteKey, setPreviousRouteKey] = useState("");
|
||||
const [editName, setEditName] = useState(false);
|
||||
const options: SelectProps["options"] = [];
|
||||
const [formRoute, setFormRoute] = useState({} as FormRoute);
|
||||
const [form] = Form.useForm();
|
||||
const inputNameRef = useRef<any>(null);
|
||||
const [peerNameToIP, peerIPToName, peerIPToID] = initPeerMaps(peers);
|
||||
const [newRoute, setNewRoute] = useState(false);
|
||||
|
||||
const optionsDisabledEnabled = [{label: 'Enabled', value: true}, {label: 'Disabled', value: false}]
|
||||
useEffect(() => {
|
||||
if (editName)
|
||||
inputNameRef.current!.focus({
|
||||
cursor: "end",
|
||||
});
|
||||
}, [editName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!newRoute ) {
|
||||
setRoutingPeerMSG(defaultRoutingPeerMSG)
|
||||
setMasqueradeMSG("Update Masquerade")
|
||||
setStatusMSG("Update Status")
|
||||
} else {
|
||||
setRoutingPeerMSG(defaultRoutingPeerMSG)
|
||||
setMasqueradeMSG(defaultMasqueradeMSG)
|
||||
setStatusMSG(defaultStatusMSG)
|
||||
setPreviousRouteKey("")
|
||||
}
|
||||
}, [newRoute])
|
||||
useEffect(() => {
|
||||
if (!route) return;
|
||||
const fRoute = {
|
||||
...route,
|
||||
groups: route.groups,
|
||||
} as FormRoute;
|
||||
setFormRoute(fRoute);
|
||||
setPreviousRouteKey(fRoute.network_id + fRoute.network);
|
||||
form.setFieldsValue(fRoute);
|
||||
|
||||
useEffect(() => {
|
||||
if (editName) inputNameRef.current!.focus({
|
||||
cursor: 'end',
|
||||
});
|
||||
}, [editName]);
|
||||
if (!route.network_id) {
|
||||
setNewRoute(true);
|
||||
} else {
|
||||
setNewRoute(false);
|
||||
}
|
||||
}, [route]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editDescription) inputDescriptionRef.current!.focus({
|
||||
cursor: 'end',
|
||||
});
|
||||
}, [editDescription]);
|
||||
peers.forEach((p) => {
|
||||
let os: string;
|
||||
os = p.os;
|
||||
if (
|
||||
!os.toLowerCase().startsWith("darwin") &&
|
||||
!os.toLowerCase().startsWith("windows") &&
|
||||
!os.toLowerCase().startsWith("android") &&
|
||||
route &&
|
||||
!routes
|
||||
.filter((r) => r.network_id === route.network_id)
|
||||
.find((r) => r.peer === p.id)
|
||||
) {
|
||||
options?.push({
|
||||
label: peerToPeerIP(p.name, p.ip),
|
||||
value: peerToPeerIP(p.name, p.ip),
|
||||
disabled: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!route) return
|
||||
const createRouteToSave = (inputRoute: FormRoute): RouteToSave => {
|
||||
let peerIDList = inputRoute.peer.split(routePeerSeparator);
|
||||
let peerID: string;
|
||||
if (peerIDList.length === 1) {
|
||||
peerID = inputRoute.peer;
|
||||
} else {
|
||||
if (peerIDList[1]) {
|
||||
peerID = peerIPToID[peerIDList[1]];
|
||||
} else {
|
||||
peerID = peerIPToID[peerNameToIP[inputRoute.peer]];
|
||||
}
|
||||
}
|
||||
|
||||
const fRoute = {
|
||||
...route,
|
||||
groups: getGroupNamesFromIDs(route.groups)
|
||||
} as FormRoute
|
||||
setFormRoute(fRoute)
|
||||
setPreviousRouteKey(fRoute.network_id + fRoute.network)
|
||||
if (!route.network_id) {
|
||||
setNewRoute(true)
|
||||
} else {
|
||||
setNewRoute(false)
|
||||
}
|
||||
form.setFieldsValue(fRoute)
|
||||
}, [route])
|
||||
let [existingGroups, groupsToCreate] = getExistingAndToCreateGroupsLists(
|
||||
inputRoute.groups
|
||||
);
|
||||
|
||||
peers.forEach((p) => {
|
||||
let os: string
|
||||
os = p.os
|
||||
if (!os.toLowerCase().startsWith("darwin") && !os.toLowerCase().startsWith("windows") && !os.toLowerCase().startsWith("android")
|
||||
&& route && !routes.filter(r => r.network_id === route.network_id).find(r => r.peer === p.id)) {
|
||||
options?.push({
|
||||
label: peerToPeerIP(p.name, p.ip),
|
||||
value: peerToPeerIP(p.name, p.ip),
|
||||
disabled: false
|
||||
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,
|
||||
groups: existingGroups,
|
||||
groupsToCreate: groupsToCreate,
|
||||
} as RouteToSave;
|
||||
};
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
if (!setupNewRouteHA || formRoute.peer != "") {
|
||||
const routeToSave = createRouteToSave(formRoute);
|
||||
dispatch(
|
||||
routeActions.saveRoute.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: routeToSave,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const createRouteToSave = (inputRoute: FormRoute): RouteToSave => {
|
||||
let peerIDList = inputRoute.peer.split(routePeerSeparator)
|
||||
let peerID: string
|
||||
if (peerIDList.length === 1) {
|
||||
peerID = inputRoute.peer
|
||||
);
|
||||
} else {
|
||||
if (peerIDList[1]) {
|
||||
peerID = peerIPToID[peerIDList[1]]
|
||||
} else {
|
||||
peerID = peerIPToID[peerNameToIP[inputRoute.peer]]
|
||||
let groupedDataTable = transformGroupedDataTable(routes, peers);
|
||||
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: getTokenSilently,
|
||||
payload: routeToSave,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log("errorInfo", errorInfo);
|
||||
});
|
||||
};
|
||||
|
||||
let [ existingGroups, groupsToCreate ] = getExistingAndToCreateGroupsLists(inputRoute.groups)
|
||||
const setVisibleNewRoute = (status: boolean) => {
|
||||
dispatch(routeActions.setSetupEditRouteVisible(status));
|
||||
};
|
||||
|
||||
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,
|
||||
groups: existingGroups,
|
||||
groupsToCreate: groupsToCreate,
|
||||
} as RouteToSave
|
||||
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,
|
||||
groups: [],
|
||||
} as Route)
|
||||
);
|
||||
setVisibleNewRoute(false);
|
||||
setSetupNewRouteHA(false);
|
||||
setPreviousRouteKey("");
|
||||
setNewRoute(false);
|
||||
};
|
||||
|
||||
const onChange = (data: any) => {
|
||||
setFormRoute({ ...formRoute, ...data });
|
||||
};
|
||||
|
||||
const peerDropDownRender = (menu: React.ReactElement) => <>{menu}</>;
|
||||
|
||||
const toggleEditName = (status: boolean) => {
|
||||
setEditName(status);
|
||||
};
|
||||
|
||||
const peerValidator = (_: RuleObject, value: string) => {
|
||||
if (value == "" && newRoute) {
|
||||
return Promise.reject(new Error("Please select routing one peer"));
|
||||
}
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then(() => {
|
||||
if (!setupNewRouteHA || formRoute.peer != '') {
|
||||
const routeToSave = createRouteToSave(formRoute)
|
||||
dispatch(routeActions.saveRoute.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: routeToSave
|
||||
}))
|
||||
} else {
|
||||
let groupedDataTable = transformGroupedDataTable(routes, peers)
|
||||
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: getTokenSilently,
|
||||
payload: routeToSave
|
||||
}))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log('errorInfo', errorInfo)
|
||||
});
|
||||
};
|
||||
|
||||
const setVisibleNewRoute = (status: boolean) => {
|
||||
dispatch(routeActions.setSetupNewRouteVisible(status));
|
||||
const selectPreValidator = (obj: RuleObject, value: string[]) => {
|
||||
if (setupNewRouteHA && formRoute.peer == "") {
|
||||
let [, newGroups] = getExistingAndToCreateGroupsLists(value);
|
||||
if (newGroups.length > 0) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
"You can't add new Groups from the group update view, please remove:\"" +
|
||||
newGroups +
|
||||
'"'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return selectValidator(obj, value);
|
||||
};
|
||||
|
||||
const setSetupNewRouteHA = (status: boolean) => {
|
||||
dispatch(routeActions.setSetupNewRouteHA(status));
|
||||
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 added.",
|
||||
key: saveKey,
|
||||
duration: 2,
|
||||
style: styleNotification,
|
||||
});
|
||||
dispatch(routeActions.setSetupNewRouteVisible(false));
|
||||
dispatch(routeActions.setSetupEditRouteVisible(false));
|
||||
dispatch(routeActions.setSetupEditRoutePeerVisible(false));
|
||||
dispatch(routeActions.setSavedRoute({ ...savedRoute, success: false }));
|
||||
dispatch(routeActions.resetSavedRoute(null));
|
||||
} else if (savedRoute.error) {
|
||||
let errorMsg = "Failed to update network route";
|
||||
switch (savedRoute.error.statusCode) {
|
||||
case 403:
|
||||
errorMsg =
|
||||
"Failed to update network route. You might not have enough permissions.";
|
||||
break;
|
||||
default:
|
||||
errorMsg = savedRoute.error.data.message
|
||||
? savedRoute.error.data.message
|
||||
: errorMsg;
|
||||
break;
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: saveKey,
|
||||
duration: 5,
|
||||
style: styleNotification,
|
||||
});
|
||||
dispatch(routeActions.setSavedRoute({ ...savedRoute, error: null }));
|
||||
dispatch(routeActions.resetSavedRoute(null));
|
||||
}
|
||||
}, [savedRoute]);
|
||||
|
||||
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("")
|
||||
setNewRoute(false)
|
||||
}
|
||||
|
||||
const onChange = (data: any) => {
|
||||
setFormRoute({...formRoute, ...data})
|
||||
}
|
||||
|
||||
const peerDropDownRender = (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()
|
||||
}
|
||||
|
||||
const peerValidator = (_: RuleObject, value: string) => {
|
||||
|
||||
if (value == "" && newRoute) {
|
||||
return Promise.reject(new Error("Please select routing one peer"))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const selectPreValidator = (obj: RuleObject, value: string[]) => {
|
||||
if (setupNewRouteHA && formRoute.peer == '') {
|
||||
let [, newGroups ] = getExistingAndToCreateGroupsLists(value)
|
||||
if (newGroups.length > 0) {
|
||||
return Promise.reject(new Error("You can't add new Groups from the group update view, please remove:\"" + newGroups +"\""))
|
||||
}
|
||||
}
|
||||
return selectValidator(obj, value)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{route &&
|
||||
<Drawer
|
||||
headerStyle={{display: "none"}}
|
||||
forceRender={true}
|
||||
open={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}>{`${newRoute ? 'Create' : 'Save'}`}</Button>
|
||||
</Space>
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{route && (
|
||||
<Modal
|
||||
open={setupEditRouteVisible}
|
||||
onCancel={onCancel}
|
||||
footer={
|
||||
<Space style={{ display: "flex", justifyContent: "end" }}>
|
||||
<Button onClick={onCancel} disabled={savedRoute.loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={savedRoute.loading}
|
||||
onClick={handleFormSubmit}
|
||||
>
|
||||
Add route
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
layout="vertical"
|
||||
form={form}
|
||||
requiredMark={false}
|
||||
onValuesChange={onChange}
|
||||
className="route-form"
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Header
|
||||
style={{
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
<Form layout="vertical" form={form} requiredMark={false} 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 && !newRoute}
|
||||
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 && !newRoute}
|
||||
onPressEnter={() => toggleEditDescription(false)}
|
||||
onBlur={() => toggleEditDescription(false)}
|
||||
autoComplete="off" maxLength={200}/>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "18px",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Add new routing peer
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
marginTop: "-23px",
|
||||
fontSize: "14px",
|
||||
paddingBottom: "25px",
|
||||
marginBottom: "4px",
|
||||
}}
|
||||
>
|
||||
When you add multiple routing peers, NetBird enables high
|
||||
availability
|
||||
</Paragraph>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="top">
|
||||
<Col flex="auto">
|
||||
<Row align="top">
|
||||
<Col span={24} style={{ lineHeight: "20px" }}>
|
||||
<>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Network Identifier
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Network name and CIDR that you are adding the route to
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
// name="network_id"
|
||||
label=""
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message:
|
||||
"Please add an identifier for this access route",
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="for example “e.g. aws-eu-central-1-vpc”"
|
||||
ref={inputNameRef}
|
||||
disabled={true}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
autoComplete="off"
|
||||
maxLength={40}
|
||||
value={
|
||||
formRoute.network_id + "-" + formRoute.network
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="top">
|
||||
<Col flex="auto"></Col>
|
||||
</Row>
|
||||
</Header>
|
||||
</Col>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
<Col span={24}>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Routing Peer
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Assign a routing peer to the network. This peer has to reside
|
||||
in the network
|
||||
</Paragraph>
|
||||
<Form.Item name="peer" rules={[{ validator: peerValidator }]}>
|
||||
<Select
|
||||
showSearch
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Select Peer"
|
||||
dropdownRender={peerDropDownRender}
|
||||
options={options}
|
||||
allowClear={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Distribution groups
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Advertise this route to peers that belong to the following
|
||||
groups
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="groups"
|
||||
label=""
|
||||
rules={[{ validator: selectPreValidator }]}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Associate groups with the network route"
|
||||
tagRender={blueTagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
optionFilterProp="serchValue"
|
||||
>
|
||||
{tagGroups.map((m, index) => (
|
||||
<Option key={index} value={m.id} serchValue={m.name}>
|
||||
{optionRender(m.name, m.id)}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
</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 && !newRoute}
|
||||
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"
|
||||
rules={[{validator:peerValidator}]}
|
||||
>
|
||||
<Select
|
||||
showSearch
|
||||
style={{width: '100%'}}
|
||||
placeholder="Select Peer"
|
||||
dropdownRender={peerDropDownRender}
|
||||
options={options}
|
||||
allowClear={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="masquerade"
|
||||
label={masqueradeMSG}
|
||||
tooltip={masqueradeDisabledMSG}
|
||||
>
|
||||
<Switch size={"small"} disabled={!setupNewRouteHA && !newRoute} 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}>
|
||||
<Form.Item
|
||||
name="groups"
|
||||
label="Distribution groups"
|
||||
tooltip="NetBird will advertise this route to peers that belong to the following groups"
|
||||
rules={[{validator: selectPreValidator}]}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{width: '100%'}}
|
||||
placeholder="Associate groups with the network route"
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</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">Learn more about network routes</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
</Drawer>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RouteUpdate
|
||||
export default RouteAddNew;
|
||||
|
||||
514
src/components/SetupKeyEdit.tsx
Normal file
@@ -0,0 +1,514 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { actions as setupKeyActions } from "../store/setup-key";
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
Row,
|
||||
Select,
|
||||
Breadcrumb,
|
||||
Tag,
|
||||
Typography,
|
||||
Card,
|
||||
Tooltip,
|
||||
} from "antd";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import {
|
||||
FormSetupKey,
|
||||
SetupKey,
|
||||
SetupKeyToSave,
|
||||
} from "../store/setup-key/types";
|
||||
import { formatDate } from "../utils/common";
|
||||
import { RuleObject } from "antd/lib/form";
|
||||
import { Group } from "../store/group/types";
|
||||
import { useGetTokenSilently } from "../utils/token";
|
||||
import moment from "moment";
|
||||
import { Container } from "./Container";
|
||||
import Paragraph from "antd/es/typography/Paragraph";
|
||||
import { LockOutlined } from "@ant-design/icons";
|
||||
import { actions as personalAccessTokenActions } from "../store/personal-access-token";
|
||||
import { useGetGroupTagHelpers } from "../utils/groups";
|
||||
|
||||
const { Option } = Select;
|
||||
const { Text } = Typography;
|
||||
|
||||
const customExpiresFormat = (value: Date): string | null => {
|
||||
return formatDate(value);
|
||||
};
|
||||
|
||||
const SetupKeyNew = (props: any) => {
|
||||
const { isGroupUpdateView, setShowGroupModal } = props;
|
||||
const {
|
||||
optionRender,
|
||||
blueTagRender,
|
||||
tagGroups,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
setGroupTagFilterAll,
|
||||
} = useGetGroupTagHelpers();
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
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 [form] = Form.useForm();
|
||||
const [editName, setEditName] = useState(false);
|
||||
const [formSetupKey, setFormSetupKey] = useState({} as FormSetupKey);
|
||||
const inputNameRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setGroupTagFilterAll(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
//Unmounting component clean
|
||||
return () => {
|
||||
setVisibleNewSetupKey(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editName) return;
|
||||
|
||||
inputNameRef.current!.focus({ cursor: "end" });
|
||||
}, [editName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!setupKey) return;
|
||||
|
||||
const allGroups = new Map<string, Group>();
|
||||
let formKeyGroups: string[] = [];
|
||||
groups.forEach((g) => allGroups.set(g.id!, g));
|
||||
|
||||
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 || [],
|
||||
exp: moment(setupKey.expires),
|
||||
last: moment(setupKey.last_used),
|
||||
} as FormSetupKey;
|
||||
form.setFieldsValue(fSetupKey);
|
||||
setFormSetupKey(fSetupKey);
|
||||
}, [setupKey]);
|
||||
|
||||
const createSetupKeyToSave = (): SetupKeyToSave => {
|
||||
let [existingGroups, groupsToCreate] = getExistingAndToCreateGroupsLists(
|
||||
formSetupKey.autoGroupNames
|
||||
);
|
||||
|
||||
const expiresIn = formSetupKey.expires_in * 24 * 3600; // the api expects seconds while the form returns days
|
||||
return {
|
||||
id: formSetupKey.id,
|
||||
name: formSetupKey.name,
|
||||
type: formSetupKey.type,
|
||||
auto_groups: existingGroups,
|
||||
revoked: formSetupKey.revoked,
|
||||
groupsToCreate: groupsToCreate,
|
||||
expires_in: expiresIn,
|
||||
usage_limit: formSetupKey.usage_limit,
|
||||
ephemeral: formSetupKey.ephemeral,
|
||||
} as SetupKeyToSave;
|
||||
};
|
||||
|
||||
const handleFormSubmit = async () => {
|
||||
try {
|
||||
await form.validateFields();
|
||||
} catch (e) {
|
||||
const errorFields = (e as any).errorFields;
|
||||
}
|
||||
|
||||
const setupKeyToSave = createSetupKeyToSave();
|
||||
dispatch(
|
||||
setupKeyActions.saveSetupKey.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: setupKeyToSave,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const setVisibleNewSetupKey = (status: boolean) => {
|
||||
form.resetFields();
|
||||
dispatch(setupKeyActions.setSetupEditKeyVisible(status));
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedSetupKey.loading) return;
|
||||
|
||||
dispatch(
|
||||
setupKeyActions.setSetupKey({
|
||||
name: "",
|
||||
type: "one-off",
|
||||
key: "",
|
||||
last_used: "",
|
||||
expires: "",
|
||||
state: "valid",
|
||||
auto_groups: [] as string[],
|
||||
usage_limit: 0,
|
||||
used_times: 0,
|
||||
expires_in: 0,
|
||||
} as SetupKey)
|
||||
);
|
||||
setFormSetupKey({} as FormSetupKey);
|
||||
setVisibleNewSetupKey(false);
|
||||
if (setShowGroupModal) {
|
||||
setShowGroupModal(false);
|
||||
}
|
||||
};
|
||||
|
||||
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 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 changesDetected = (): boolean => {
|
||||
return (
|
||||
formSetupKey.name == null ||
|
||||
formSetupKey.name !== setupKey.name ||
|
||||
groupsChanged() ||
|
||||
formSetupKey.usage_limit !== setupKey.usage_limit
|
||||
);
|
||||
};
|
||||
|
||||
const groupsChanged = (): boolean => {
|
||||
if (
|
||||
setupKey &&
|
||||
setupKey.auto_groups &&
|
||||
formSetupKey.autoGroupNames.length !== setupKey.auto_groups.length
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const formGroupIds =
|
||||
groups
|
||||
?.filter((g) => formSetupKey.autoGroupNames.includes(g.name))
|
||||
.map((g) => g.id || "") || [];
|
||||
|
||||
return (
|
||||
setupKey.auto_groups?.filter((g) => !formGroupIds.includes(g)).length > 0
|
||||
);
|
||||
};
|
||||
|
||||
const getFormKey = (key: string) => {
|
||||
if (key) return key.substring(0, 4).concat("****");
|
||||
};
|
||||
|
||||
const onBreadcrumbUsersClick = () => {
|
||||
if (savedSetupKey.loading) return;
|
||||
// dispatch(userActions.setUser(null as unknown as User));
|
||||
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null));
|
||||
setVisibleNewSetupKey(false);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{!isGroupUpdateView && (
|
||||
<Breadcrumb
|
||||
style={{ marginBottom: "25px" }}
|
||||
items={[
|
||||
{
|
||||
title: <a onClick={onBreadcrumbUsersClick}>Setup Keys</a>,
|
||||
},
|
||||
{
|
||||
title: setupKey.name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<Card
|
||||
bordered={true}
|
||||
className={isGroupUpdateView ? " noborderPadding" : ""}
|
||||
style={{ marginBottom: "7px", border: "none" }}
|
||||
>
|
||||
<div style={{ maxWidth: "800px" }}>
|
||||
{!isGroupUpdateView && (
|
||||
<h3
|
||||
style={{
|
||||
fontSize: "22px",
|
||||
fontWeight: "500",
|
||||
marginBottom: "30px",
|
||||
}}
|
||||
>
|
||||
{setupKey.name}
|
||||
</h3>
|
||||
)}
|
||||
<Form
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
form={form}
|
||||
onValuesChange={onChange}
|
||||
initialValues={{
|
||||
usage_limit: 1,
|
||||
}}
|
||||
>
|
||||
{!isGroupUpdateView && (
|
||||
<Row style={{ marginTop: "10px" }}>
|
||||
<Col
|
||||
sm={24}
|
||||
md={8}
|
||||
lg={8}
|
||||
style={{
|
||||
paddingRight: "70px",
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
fontWeight: "500",
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
Key
|
||||
<Tag
|
||||
color={`${
|
||||
formSetupKey.state === "valid" ? "green" : "red"
|
||||
}`}
|
||||
style={{
|
||||
marginLeft: "10px",
|
||||
borderRadius: "2px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
{formSetupKey.state}
|
||||
</Tag>
|
||||
</Paragraph>
|
||||
<Input
|
||||
style={{ marginTop: "8px" }}
|
||||
disabled
|
||||
value={getFormKey(formSetupKey.key)}
|
||||
suffix={<LockOutlined style={{ color: "#BFBFBF" }} />}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col
|
||||
sm={24}
|
||||
md={8}
|
||||
lg={6}
|
||||
style={{
|
||||
paddingRight: "70px",
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "500",
|
||||
}}
|
||||
></Paragraph>
|
||||
Type{" "}
|
||||
{formSetupKey.ephemeral ? (
|
||||
<Tooltip title="Peers that are offline for over 10 minutes will be removed automatically">
|
||||
<Tag>
|
||||
<Text type="secondary" style={{ fontSize: 10 }}>
|
||||
Ephemeral
|
||||
</Text>
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
) : (
|
||||
" "
|
||||
)}
|
||||
</Paragraph>
|
||||
<Col>
|
||||
<Input
|
||||
disabled
|
||||
value={
|
||||
formSetupKey.type === "one-off" ? "One-off" : "Reusable"
|
||||
}
|
||||
suffix={<LockOutlined style={{ color: "#BFBFBF" }} />}
|
||||
style={{ marginTop: "8px" }}
|
||||
/>
|
||||
</Col>
|
||||
</Col>
|
||||
|
||||
<Col sm={24} md={8} lg={3}>
|
||||
<Paragraph
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "500",
|
||||
}}
|
||||
></Paragraph>
|
||||
{/* {formSetupKey.type === "one-off" ? "One-off" : "Reusable"}, */}
|
||||
Available uses
|
||||
</Paragraph>
|
||||
<Col>
|
||||
<Input
|
||||
disabled
|
||||
value={
|
||||
formSetupKey.type === "reusable" &&
|
||||
formSetupKey.usage_limit === 0
|
||||
? "unlimited"
|
||||
: formSetupKey.usage_limit - formSetupKey.used_times
|
||||
}
|
||||
suffix={<LockOutlined style={{ color: "#BFBFBF" }} />}
|
||||
style={{ marginTop: "8px" }}
|
||||
/>
|
||||
</Col>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<Row style={{ marginTop: `${isGroupUpdateView ? "0" : "39px"}` }}>
|
||||
{!isGroupUpdateView && (
|
||||
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
|
||||
<Paragraph style={{ margin: 0, fontWeight: "500" }}>
|
||||
Expires
|
||||
</Paragraph>
|
||||
<Row>
|
||||
<Input
|
||||
style={{ marginTop: "8px" }}
|
||||
disabled
|
||||
suffix={<LockOutlined style={{ color: "#BFBFBF" }} />}
|
||||
value={
|
||||
customExpiresFormat(new Date(formSetupKey.expires))!
|
||||
}
|
||||
/>
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
<Row style={{ marginTop: `${isGroupUpdateView ? "0" : "39px"}` }}>
|
||||
<Col
|
||||
xs={24}
|
||||
sm={24}
|
||||
md={!isGroupUpdateView ? 11 : 24}
|
||||
lg={!isGroupUpdateView ? 11 : 24}
|
||||
xl={!isGroupUpdateView ? 11 : 24}
|
||||
xxl={!isGroupUpdateView ? 11 : 24}
|
||||
span={!isGroupUpdateView ? 11 : 24}
|
||||
style={{
|
||||
paddingRight: `${!isGroupUpdateView ? "70px" : "0"}`,
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Auto-assigned groups
|
||||
</Paragraph>
|
||||
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
style={{ marginTop: "8px", marginBottom: 0 }}
|
||||
name="autoGroupNames"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Associate groups with the key"
|
||||
tagRender={blueTagRender}
|
||||
dropdownRender={dropDownRender}
|
||||
optionFilterProp="searchValue"
|
||||
>
|
||||
{tagGroups.map((m, index) => (
|
||||
<Option key={index} value={m.id} serchValue={m.name}>
|
||||
{optionRender(m.name, m.id)}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</div>
|
||||
<Container
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: `${!isGroupUpdateView ? "start" : "end"}`,
|
||||
padding: 0,
|
||||
gap: "10px",
|
||||
marginTop: "24px",
|
||||
}}
|
||||
key={0}
|
||||
>
|
||||
<Button onClick={onCancel}>Cancel</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={savedSetupKey.loading || !changesDetected()}
|
||||
onClick={handleFormSubmit}
|
||||
>
|
||||
{`${formSetupKey.id ? "Save" : "Create"} key`}
|
||||
</Button>
|
||||
</Container>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupKeyNew;
|
||||
21
src/components/UpdateKeyGroupModal.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Modal } from "antd";
|
||||
import SetupKeyNew from "./SetupKeyEdit";
|
||||
export const UpdateKeyGroupModal = (props:any) => {
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
closable={false}
|
||||
open={true}
|
||||
footer={[]}
|
||||
onCancel={() => props.setShowGroupModal(false)}
|
||||
width={450}
|
||||
>
|
||||
<SetupKeyNew
|
||||
isGroupUpdateView={true}
|
||||
setShowGroupModal={props.setShowGroupModal}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
21
src/components/UpdateNameServerGroupModal.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Modal } from "antd";
|
||||
import NameServerGroupUpdate from "./NameServerGroupUpdate";
|
||||
export const UpdateNameServerGroupModal = (props:any) => {
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
closable={false}
|
||||
open={true}
|
||||
footer={[]}
|
||||
onCancel={() => props.setShowGroupModal(false)}
|
||||
width={450}
|
||||
>
|
||||
<NameServerGroupUpdate
|
||||
isGroupUpdateView={true}
|
||||
setShowGroupModal={props.setShowGroupModal}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
21
src/components/UpdatePeerGroupModal.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Modal } from "antd";
|
||||
import PeerUpdate from "./PeerUpdate";
|
||||
export const UpdatePeerGroupModal = (props:any) => {
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
closable={false}
|
||||
open={true}
|
||||
footer={[]}
|
||||
onCancel={() => props.setShowGroupModal(false)}
|
||||
width={450}
|
||||
>
|
||||
<PeerUpdate
|
||||
isGroupUpdateView={true}
|
||||
setShowGroupModal={props.setShowGroupModal}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
24
src/components/UpdateUsersGroupModal.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Modal } from "antd";
|
||||
import UserEdit from "./UserEdit";
|
||||
|
||||
export const UpdateUsersGroupModal = (props: any) => {
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
closable={false}
|
||||
open={true}
|
||||
footer={[]}
|
||||
onCancel={() => props.setShowGroupModal(false)}
|
||||
width={450}
|
||||
>
|
||||
{props.showGroupModal && (
|
||||
<UserEdit
|
||||
isGroupUpdateView={true}
|
||||
setShowGroupModal={props.setShowGroupModal}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -98,22 +98,6 @@ const AddPATPopup = () => {
|
||||
}
|
||||
}, [savedPersonalAccessToken])
|
||||
|
||||
useEffect(() => {
|
||||
const keyDownHandler = (event: any) => {
|
||||
console.log('User pressed: ', event.key);
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
handleFormSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', keyDownHandler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', keyDownHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -132,7 +116,7 @@ const AddPATPopup = () => {
|
||||
>
|
||||
<Container style={{textAlign: "start", marginLeft: "-15px", marginRight: "-15px"}}>
|
||||
<Paragraph
|
||||
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "22px"}}>
|
||||
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "18px",fontWeight:"500"}}>
|
||||
{showPlainToken ? "Token created successfully!" : "Create token"}
|
||||
</Paragraph>
|
||||
{!showPlainToken && <Paragraph type={"secondary"}
|
||||
@@ -158,7 +142,7 @@ const AddPATPopup = () => {
|
||||
<Col span={24}>
|
||||
<Row align="top">
|
||||
<Col flex="auto">
|
||||
<Paragraph style={{fontWeight: "bold", marginTop: "-10px"}}>Name</Paragraph>
|
||||
<Paragraph style={{fontWeight: "500", marginTop: "-10px"}}>Name</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Set an easily identifiable name for your token</Paragraph>
|
||||
<Form.Item
|
||||
name="name"
|
||||
@@ -178,7 +162,7 @@ const AddPATPopup = () => {
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={24} style={{textAlign: "left", marginTop: "10px"}}>
|
||||
<Paragraph style={{fontWeight: "bold", marginTop: "-10px"}}>Expires in</Paragraph>
|
||||
<Paragraph style={{fontWeight: "500", marginTop: "-10px"}}>Expires in</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Number of days this token will be valid for</Paragraph>
|
||||
<Form.Item
|
||||
name="expires_in"
|
||||
@@ -193,10 +177,19 @@ const AddPATPopup = () => {
|
||||
</Form.Item>
|
||||
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Should be between 1 and 365 days</Paragraph>
|
||||
</Col>
|
||||
{/*<Col span={24}>*/}
|
||||
{/* <Button icon={<QuestionCircleFilled/>} type="link" target="_blank" disabled={true} style={{marginTop: "20px", marginBottom: "20px"}}*/}
|
||||
{/* href="https://netbird.io/docs/overview/personal-access-tokens">Learn more about personal access tokens</Button>*/}
|
||||
{/*</Col>*/}
|
||||
<Col span={24} style={{marginTop: "15px"}}>
|
||||
<Text type={"secondary"}>
|
||||
Learn more about
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://docs.netbird.io/how-to/access-netbird-public-api"
|
||||
>
|
||||
{" "}
|
||||
access tokens
|
||||
</a>
|
||||
</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>}
|
||||
{showPlainToken &&
|
||||
|
||||
@@ -1,292 +1,362 @@
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {Container} from "../Container";
|
||||
import {CloseOutlined} from "@ant-design/icons";
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {useGetTokenSilently} from "../../utils/token";
|
||||
import {actions as userActions} from "../../store/user";
|
||||
import {actions as groupActions} from "../../store/group";
|
||||
import {User, UserToSave} from "../../store/user/types";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import {CustomTagProps} from "rc-select/lib/BaseSelect";
|
||||
import { Container } from "../Container";
|
||||
import { CloseOutlined } from "@ant-design/icons";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import { useGetTokenSilently } from "../../utils/token";
|
||||
import { actions as userActions } from "../../store/user";
|
||||
import { actions as groupActions } from "../../store/group";
|
||||
import { User, UserToSave } from "../../store/user/types";
|
||||
import { Header } from "antd/es/layout/layout";
|
||||
import { RuleObject } from "antd/lib/form";
|
||||
import { CustomTagProps } from "rc-select/lib/BaseSelect";
|
||||
|
||||
const {Title, Text, Paragraph} = Typography;
|
||||
const {Option} = Select;
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
const AddServiceUserPopup = () => {
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const users = useSelector((state: RootState) => state.user.data)
|
||||
const groups = useSelector((state: RootState) => state.group.data);
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
|
||||
const user = useSelector((state: RootState) => state.user.user)
|
||||
const failed = useSelector((state: RootState) => state.user.failed);
|
||||
const loading = useSelector((state: RootState) => state.user.loading);
|
||||
const addServiceUserModalOpen = useSelector((state: RootState) => state.user.addServiceUserPopupVisible)
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser)
|
||||
const user = useSelector((state: RootState) => state.user.user);
|
||||
const failed = useSelector((state: RootState) => state.user.failed);
|
||||
const loading = useSelector((state: RootState) => state.user.loading);
|
||||
const addServiceUserModalOpen = useSelector(
|
||||
(state: RootState) => state.user.addServiceUserPopupVisible
|
||||
);
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser);
|
||||
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
|
||||
const [form] = Form.useForm();
|
||||
const inputNameRef = useRef<any>(null);
|
||||
|
||||
const [form] = Form.useForm()
|
||||
const inputNameRef = useRef<any>(null)
|
||||
const [tagGroups, setTagGroups] = useState([] as string[]);
|
||||
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[]);
|
||||
|
||||
const [tagGroups, setTagGroups] = useState([] as string[])
|
||||
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[])
|
||||
|
||||
const createUserToSave = (values: any): UserToSave => {
|
||||
const autoGroups = groups?.filter(g => values.autoGroupsNames && values.autoGroupsNames.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 = values.autoGroupsNames?.filter((s: string) => !allGroupsNames.includes(s)) || []
|
||||
return {
|
||||
id: values.id,
|
||||
role: values.role,
|
||||
name: values.name,
|
||||
groupsToCreate: groupsToCreate,
|
||||
auto_groups: autoGroups,
|
||||
is_service_user: true
|
||||
} as UserToSave
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedUser.loading) return
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
form.resetFields();
|
||||
dispatch(userActions.setAddServiceUserPopupVisible(false));
|
||||
}
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
let userToSave = createUserToSave(values)
|
||||
dispatch(userActions.saveUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: userToSave
|
||||
}))
|
||||
form.resetFields();
|
||||
dispatch(userActions.getServiceUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
dispatch(userActions.setAddServiceUserPopupVisible(false));
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log('errorInfo', errorInfo)
|
||||
});
|
||||
};
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = []
|
||||
|
||||
if (!value) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
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 handleChangeTags = (value: string[]) => {
|
||||
let validatedValues: string[] = []
|
||||
value.forEach(function (v) {
|
||||
if (v.trim().length) {
|
||||
validatedValues.push(v)
|
||||
}
|
||||
})
|
||||
setSelectedTagGroups(validatedValues)
|
||||
};
|
||||
|
||||
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 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 createUserToSave = (values: any): UserToSave => {
|
||||
const autoGroups =
|
||||
groups
|
||||
?.filter(
|
||||
(g) =>
|
||||
values.autoGroupsNames && values.autoGroupsNames.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 =
|
||||
values.autoGroupsNames?.filter(
|
||||
(s: string) => !allGroupsNames.includes(s)
|
||||
) || [];
|
||||
return {
|
||||
id: values.id,
|
||||
role: values.role,
|
||||
name: values.name,
|
||||
groupsToCreate: groupsToCreate,
|
||||
auto_groups: autoGroups,
|
||||
is_service_user: true,
|
||||
} as UserToSave;
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedUser.loading) return;
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
form.resetFields();
|
||||
dispatch(userActions.setAddServiceUserPopupVisible(false));
|
||||
};
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
let userToSave = createUserToSave(values);
|
||||
dispatch(
|
||||
userActions.saveUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: userToSave,
|
||||
})
|
||||
);
|
||||
form.resetFields();
|
||||
dispatch(
|
||||
userActions.getServiceUsers.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
dispatch(userActions.setAddServiceUserPopupVisible(false));
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log("errorInfo", errorInfo);
|
||||
});
|
||||
};
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = [];
|
||||
|
||||
if (!value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setTagGroups(groups?.filter(g => g.name != "All").map(g => g.name) || [])
|
||||
}, [groups])
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null
|
||||
}))
|
||||
}, [])
|
||||
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 (
|
||||
<>
|
||||
<Modal
|
||||
open={addServiceUserModalOpen}
|
||||
onCancel={onCancel}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button disabled={loading} onClick={onCancel}>Cancel</Button>
|
||||
<Button type="primary"
|
||||
onClick={handleFormSubmit}>Create user</Button>
|
||||
</Space>
|
||||
}
|
||||
width={460}
|
||||
>
|
||||
<Container style={{textAlign: "start", marginLeft: "-15px", marginRight: "-15px"}}>
|
||||
<Paragraph
|
||||
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "22px"}}>
|
||||
{"Add service user"}
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"}
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
marginTop: "-23px",
|
||||
paddingBottom: "25px",
|
||||
}}>
|
||||
{"Service users are non-login users that are not associated with any specific person."}
|
||||
</Paragraph>
|
||||
<Form layout="vertical" hideRequiredMark form={form}
|
||||
initialValues={{
|
||||
["role"]: "user"
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{fontWeight: "bold", marginTop: "-10px"}}>Name</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Set a name to easily identify the user</Paragraph>
|
||||
<Form.Item
|
||||
name="name"
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please add a new name for this user',
|
||||
whitespace: true
|
||||
}]}
|
||||
style={{marginTop: "-8px"}}
|
||||
>
|
||||
<Input
|
||||
placeholder={'for example "Ansible user"'}
|
||||
ref={inputNameRef}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{fontWeight: "bold", marginTop: "0px"}}>Role</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-15px"}}>Set a role for the user to assign access permissions</Paragraph>
|
||||
<Form.Item
|
||||
name="role"
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please select a role for this user',
|
||||
whitespace: true
|
||||
}]}
|
||||
style={{marginTop: "-8px"}}
|
||||
>
|
||||
<Select style={{width: "120px"}}>
|
||||
<Option value="admin">admin</Option>
|
||||
<Option value="user">user</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{/*<Col span={24}>*/}
|
||||
{/* <Paragraph style={{fontWeight: "bold", marginTop: "0px"}}>Auto-assigned groups</Paragraph>*/}
|
||||
{/* <Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-15px"}}>Add groups, that will be assigned to peers added by this user</Paragraph>*/}
|
||||
{/* <Form.Item*/}
|
||||
{/* name="autoGroupsNames"*/}
|
||||
{/* label="Auto-assigned groups"*/}
|
||||
{/* tooltip="Every peer enrolled with this user will be automatically added to these groups"*/}
|
||||
{/* rules={[{validator: selectValidator}]}*/}
|
||||
{/* >*/}
|
||||
{/* <Select mode="tags"*/}
|
||||
{/* style={{width: '100%'}}*/}
|
||||
{/* placeholder="Associate groups with the user"*/}
|
||||
{/* tagRender={tagRender}*/}
|
||||
{/* onChange={handleChangeTags}*/}
|
||||
{/* dropdownRender={dropDownRender}*/}
|
||||
{/* >*/}
|
||||
{/* {*/}
|
||||
{/* tagGroups.map(m =>*/}
|
||||
{/* <Option key={m}>{optionRender(m)}</Option>*/}
|
||||
{/* )*/}
|
||||
{/* }*/}
|
||||
{/* </Select>*/}
|
||||
{/* </Form.Item>*/}
|
||||
{/*</Col>*/}
|
||||
</Row>
|
||||
</Form>
|
||||
</Container>
|
||||
</Modal>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
)
|
||||
<Tag
|
||||
color="blue"
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{ marginRight: 3 }}
|
||||
>
|
||||
<strong>{value}</strong>
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
const handleChangeTags = (value: string[]) => {
|
||||
let validatedValues: string[] = [];
|
||||
value.forEach(function (v) {
|
||||
if (v.trim().length) {
|
||||
validatedValues.push(v);
|
||||
}
|
||||
});
|
||||
setSelectedTagGroups(validatedValues);
|
||||
};
|
||||
|
||||
export default AddServiceUserPopup
|
||||
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 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTagGroups(
|
||||
groups?.filter((g) => g.name != "All").map((g) => g.name) || []
|
||||
);
|
||||
}, [groups]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
groupActions.getGroups.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
open={addServiceUserModalOpen}
|
||||
onCancel={onCancel}
|
||||
footer={
|
||||
<Space style={{ display: "flex", justifyContent: "end" }}>
|
||||
<Button disabled={loading} onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleFormSubmit}>
|
||||
Create user
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
width={460}
|
||||
>
|
||||
<Container
|
||||
style={{
|
||||
textAlign: "start",
|
||||
marginLeft: "-15px",
|
||||
marginRight: "-15px",
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "18px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
{"Add service user"}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
marginTop: "-23px",
|
||||
paddingBottom: "25px",
|
||||
}}
|
||||
>
|
||||
{
|
||||
"Service users are non-login users that are not associated with any specific person."
|
||||
}
|
||||
</Paragraph>
|
||||
<Form
|
||||
layout="vertical"
|
||||
hideRequiredMark
|
||||
form={form}
|
||||
initialValues={{
|
||||
["role"]: "user",
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "500", marginTop: "-10px" }}>
|
||||
Name
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{ marginTop: "-15px" }}>
|
||||
Set a name to easily identify the user
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="name"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please add a new name for this user",
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
style={{ marginTop: "-8px" }}
|
||||
>
|
||||
<Input
|
||||
placeholder={'for example "Ansible user"'}
|
||||
ref={inputNameRef}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "500", marginTop: "0px" }}>
|
||||
Role
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{ fontSize: "14px", marginTop: "-15px" }}
|
||||
>
|
||||
Set a role for the user to assign access permissions
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="role"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please select a role for this user",
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
style={{ marginTop: "-8px" }}
|
||||
>
|
||||
<Select style={{ width: "120px" }}>
|
||||
<Option value="admin">admin</Option>
|
||||
<Option value="user">user</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{/*<Col span={24}>*/}
|
||||
{/* <Paragraph style={{fontWeight: "bold", marginTop: "0px"}}>Auto-assigned groups</Paragraph>*/}
|
||||
{/* <Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-15px"}}>Add groups, that will be assigned to peers added by this user</Paragraph>*/}
|
||||
{/* <Form.Item*/}
|
||||
{/* name="autoGroupsNames"*/}
|
||||
{/* label="Auto-assigned groups"*/}
|
||||
{/* tooltip="Every peer enrolled with this user will be automatically added to these groups"*/}
|
||||
{/* rules={[{validator: selectValidator}]}*/}
|
||||
{/* >*/}
|
||||
{/* <Select mode="tags"*/}
|
||||
{/* style={{width: '100%'}}*/}
|
||||
{/* placeholder="Associate groups with the user"*/}
|
||||
{/* tagRender={tagRender}*/}
|
||||
{/* onChange={handleChangeTags}*/}
|
||||
{/* dropdownRender={dropDownRender}*/}
|
||||
{/* >*/}
|
||||
{/* {*/}
|
||||
{/* tagGroups.map(m =>*/}
|
||||
{/* <Option key={m}>{optionRender(m)}</Option>*/}
|
||||
{/* )*/}
|
||||
{/* }*/}
|
||||
{/* </Select>*/}
|
||||
{/* </Form.Item>*/}
|
||||
{/*</Col>*/}
|
||||
</Row>
|
||||
</Form>
|
||||
</Container>
|
||||
</Modal>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddServiceUserPopup;
|
||||
|
||||
@@ -1,315 +1,367 @@
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {Container} from "../Container";
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {useGetTokenSilently} from "../../utils/token";
|
||||
import {actions as userActions} from "../../store/user";
|
||||
import {actions as groupActions} from "../../store/group";
|
||||
import {User, UserToSave} from "../../store/user/types";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import {CustomTagProps} from "rc-select/lib/BaseSelect";
|
||||
import { Container } from "../Container";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import { useGetTokenSilently } from "../../utils/token";
|
||||
import { actions as userActions } from "../../store/user";
|
||||
import { actions as groupActions } from "../../store/group";
|
||||
import { User, UserToSave } from "../../store/user/types";
|
||||
import { RuleObject } from "antd/lib/form";
|
||||
import { CustomTagProps } from "rc-select/lib/BaseSelect";
|
||||
import { QuestionCircleFilled } from "@ant-design/icons";
|
||||
import { useGetGroupTagHelpers } from "../../utils/groups";
|
||||
|
||||
const {Title, Text, Paragraph} = Typography;
|
||||
const {Option} = Select;
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
const InviteUserPopup = () => {
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const { optionRender, blueTagRender, tagGroups, handleChangeTags } =
|
||||
useGetGroupTagHelpers();
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const users = useSelector((state: RootState) => state.user.data)
|
||||
const groups = useSelector((state: RootState) => state.group.data);
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
|
||||
const user = useSelector((state: RootState) => state.user.user)
|
||||
const failed = useSelector((state: RootState) => state.user.failed);
|
||||
const loading = useSelector((state: RootState) => state.user.loading);
|
||||
const inviteUserModalOpen = useSelector((state: RootState) => state.user.inviteUserPopupVisible)
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser)
|
||||
const user = useSelector((state: RootState) => state.user.user);
|
||||
const failed = useSelector((state: RootState) => state.user.failed);
|
||||
const loading = useSelector((state: RootState) => state.user.loading);
|
||||
const inviteUserModalOpen = useSelector(
|
||||
(state: RootState) => state.user.inviteUserPopupVisible
|
||||
);
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser);
|
||||
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
|
||||
const [form] = Form.useForm();
|
||||
const inputNameRef = useRef<any>(null);
|
||||
|
||||
const [form] = Form.useForm()
|
||||
const inputNameRef = useRef<any>(null)
|
||||
|
||||
const [tagGroups, setTagGroups] = useState([] as string[])
|
||||
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[])
|
||||
|
||||
const createUserToSave = (values: any): UserToSave => {
|
||||
const autoGroups = groups?.filter(g => values.autoGroupsNames && values.autoGroupsNames.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 = values.autoGroupsNames?.filter((s: string) => !allGroupsNames.includes(s)) || []
|
||||
return {
|
||||
id: values.id,
|
||||
role: values.role,
|
||||
email: values.email,
|
||||
name: values.name,
|
||||
groupsToCreate: groupsToCreate,
|
||||
auto_groups: autoGroups,
|
||||
is_service_user: false
|
||||
} as UserToSave
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedUser.loading) return
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
form.resetFields();
|
||||
dispatch(userActions.setInviteUserPopupVisible(false));
|
||||
}
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
let userToSave = createUserToSave(values)
|
||||
dispatch(userActions.saveUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: userToSave
|
||||
}))
|
||||
form.resetFields();
|
||||
dispatch(userActions.getRegularUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
dispatch(userActions.setInviteUserPopupVisible(false));
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log('errorInfo', errorInfo)
|
||||
});
|
||||
};
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = []
|
||||
|
||||
if (!value) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
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 handleChangeTags = (value: string[]) => {
|
||||
let validatedValues: string[] = []
|
||||
value.forEach(function (v) {
|
||||
if (v.trim().length) {
|
||||
validatedValues.push(v)
|
||||
}
|
||||
})
|
||||
setSelectedTagGroups(validatedValues)
|
||||
};
|
||||
|
||||
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 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 createUserToSave = (values: any): UserToSave => {
|
||||
const autoGroups =
|
||||
groups
|
||||
?.filter(
|
||||
(g) =>
|
||||
values.autoGroupsNames && values.autoGroupsNames.includes(g.id)
|
||||
)
|
||||
.map((g) => g.id || "") || [];
|
||||
// find groups that do not yet exist (newly added by the user)
|
||||
const allGroupsNames: string[] = groups?.map((g) => g.id || "");
|
||||
const groupsToCreate =
|
||||
values.autoGroupsNames?.filter(
|
||||
(s: string) => !allGroupsNames.includes(s)
|
||||
) || [];
|
||||
return {
|
||||
id: values.id,
|
||||
role: values.role,
|
||||
email: values.email,
|
||||
name: values.name,
|
||||
groupsToCreate: groupsToCreate,
|
||||
auto_groups: autoGroups,
|
||||
is_service_user: false,
|
||||
} as UserToSave;
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedUser.loading) return;
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
form.resetFields();
|
||||
dispatch(userActions.setInviteUserPopupVisible(false));
|
||||
};
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
let userToSave = createUserToSave(values);
|
||||
dispatch(
|
||||
userActions.saveUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: userToSave,
|
||||
})
|
||||
);
|
||||
form.resetFields();
|
||||
dispatch(
|
||||
userActions.getRegularUsers.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
dispatch(userActions.setInviteUserPopupVisible(false));
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log("errorInfo", errorInfo);
|
||||
});
|
||||
};
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = [];
|
||||
|
||||
if (!value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setTagGroups(groups?.filter(g => g.name != "All").map(g => g.name) || [])
|
||||
}, [groups])
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null
|
||||
}))
|
||||
}, [])
|
||||
if (hasSpaceNamed.length) {
|
||||
return Promise.reject(
|
||||
new Error("Group names with just spaces are not allowed")
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
open={inviteUserModalOpen}
|
||||
onCancel={onCancel}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button disabled={loading} onClick={onCancel}>Cancel</Button>
|
||||
<Button type="primary"
|
||||
onClick={handleFormSubmit}>Invite</Button>
|
||||
</Space>
|
||||
}
|
||||
width={460}
|
||||
>
|
||||
<Container style={{textAlign: "start", marginLeft: "-15px", marginRight: "-15px"}}>
|
||||
<Paragraph
|
||||
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "22px", fontWeight: "500"}}>
|
||||
{"Invite user"}
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"}
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "14px",
|
||||
marginTop: "-23px",
|
||||
paddingBottom: "25px",
|
||||
}}>
|
||||
{"Invite a user to your network and set their permissions."}
|
||||
</Paragraph>
|
||||
<Form layout="vertical" hideRequiredMark form={form}
|
||||
initialValues={{
|
||||
["role"]: "user"
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{fontWeight: "bold", marginTop: "-10px"}}>Name</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Set a name to easily identify the user</Paragraph>
|
||||
<Form.Item
|
||||
name="name"
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please add a name for this user',
|
||||
whitespace: true
|
||||
}]}
|
||||
style={{marginTop: "-8px"}}
|
||||
>
|
||||
<Input
|
||||
placeholder={'for example "Max Schmidt"'}
|
||||
ref={inputNameRef}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "bold", marginTop: "0px"}}>Email</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Provide the email address of the user</Paragraph>
|
||||
<Form.Item
|
||||
name="email"
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please add a valid email address for this user',
|
||||
whitespace: false,
|
||||
pattern: new RegExp(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i)
|
||||
}]}
|
||||
style={{marginTop: "-8px"}}
|
||||
>
|
||||
<Input
|
||||
placeholder={'for example "max.schmidt@gmail.com"'}
|
||||
ref={inputNameRef}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "bold", marginTop: "0px"}}>Role</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Set a role for the user to assign access permissions</Paragraph>
|
||||
<Form.Item
|
||||
name="role"
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please select a role for this user',
|
||||
whitespace: true
|
||||
}]}
|
||||
style={{marginTop: "-8px"}}
|
||||
>
|
||||
<Select style={{width: "120px"}}>
|
||||
<Option value="admin">admin</Option>
|
||||
<Option value="user">user</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{fontWeight: "bold", marginTop: "0px"}}>Auto-assigned groups</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Add groups, that will be assigned to peers added by this user</Paragraph>
|
||||
<Form.Item
|
||||
name="autoGroupsNames"
|
||||
tooltip="Every peer enrolled with this user will be automatically added to these groups"
|
||||
rules={[{validator: selectValidator}]}
|
||||
style={{marginTop: "-8px"}}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{width: '100%'}}
|
||||
placeholder="Associate groups with the user"
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{/*<Col span={24}>*/}
|
||||
{/* <Button icon={<QuestionCircleFilled/>} type="link" target="_blank" disabled={true} style={{marginTop: "20px", marginBottom: "20px"}}*/}
|
||||
{/* href="https://netbird.io/docs/overview/personal-access-tokens">Learn more about user</Button>*/}
|
||||
{/*</Col>*/}
|
||||
</Row>
|
||||
</Form>
|
||||
</Container>
|
||||
</Modal>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
)
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
}
|
||||
// const handleChangeTags = (value: string[]) => {
|
||||
// let validatedValues: string[] = [];
|
||||
// value.forEach(function (v) {
|
||||
// if (v.trim().length) {
|
||||
// validatedValues.push(v);
|
||||
// }
|
||||
// });
|
||||
// setSelectedTagGroups(validatedValues);
|
||||
// };
|
||||
|
||||
export default InviteUserPopup
|
||||
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>
|
||||
</>
|
||||
);
|
||||
|
||||
// useEffect(() => {
|
||||
// setTagGroups(
|
||||
// groups?.filter((g) => g.name != "All").map((g) => g.name) || []
|
||||
// );
|
||||
// }, [groups]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
groupActions.getGroups.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
open={inviteUserModalOpen}
|
||||
onCancel={onCancel}
|
||||
footer={
|
||||
<Space style={{ display: "flex", justifyContent: "end" }}>
|
||||
<Button disabled={loading} onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleFormSubmit}>
|
||||
Invite
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
width={460}
|
||||
>
|
||||
<Container
|
||||
style={{
|
||||
textAlign: "start",
|
||||
marginLeft: "-15px",
|
||||
marginRight: "-15px",
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "18px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
{"Invite user"}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "14px",
|
||||
marginTop: "-23px",
|
||||
paddingBottom: "25px",
|
||||
}}
|
||||
>
|
||||
{"Invite a user to your network and set their permissions."}
|
||||
</Paragraph>
|
||||
<Form
|
||||
layout="vertical"
|
||||
hideRequiredMark
|
||||
form={form}
|
||||
initialValues={{
|
||||
["role"]: "user",
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "500", marginTop: "-10px" }}>
|
||||
Name
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{ marginTop: "-15px" }}>
|
||||
Set a name to easily identify the user
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="name"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please add a name for this user",
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
style={{ marginTop: "-8px" }}
|
||||
>
|
||||
<Input
|
||||
placeholder={'for example "Max Schmidt"'}
|
||||
ref={inputNameRef}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "500", marginTop: "0px" }}>
|
||||
Email
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{ marginTop: "-15px" }}>
|
||||
Provide the email address of the user
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="email"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please add a valid email address for this user",
|
||||
whitespace: false,
|
||||
pattern: new RegExp(
|
||||
/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i
|
||||
),
|
||||
},
|
||||
]}
|
||||
style={{ marginTop: "-8px" }}
|
||||
>
|
||||
<Input
|
||||
placeholder={'for example "max.schmidt@gmail.com"'}
|
||||
ref={inputNameRef}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "500", marginTop: "0px" }}>
|
||||
Role
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{ marginTop: "-15px" }}>
|
||||
Set a role for the user to assign access permissions
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="role"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please select a role for this user",
|
||||
whitespace: true,
|
||||
},
|
||||
]}
|
||||
style={{ marginTop: "-8px" }}
|
||||
>
|
||||
<Select style={{ width: "120px" }}>
|
||||
<Option value="admin">admin</Option>
|
||||
<Option value="user">user</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{ fontWeight: "500", marginTop: "0px" }}>
|
||||
Auto-assigned groups
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{ marginTop: "-15px" }}>
|
||||
Add groups, that will be assigned to peers added by this user
|
||||
</Paragraph>
|
||||
<Form.Item
|
||||
name="autoGroupsNames"
|
||||
tooltip="Every peer enrolled with this user will be automatically added to these groups"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
style={{ marginTop: "-8px" }}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Associate groups with the user"
|
||||
tagRender={blueTagRender}
|
||||
onChange={handleChangeTags}
|
||||
optionFilterProp="serchValue"
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
{tagGroups.map((m, index) => (
|
||||
<Option key={index} value={m.id} serchValue={m.name}>
|
||||
{optionRender(m.name, m.id)}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Text type={"secondary"}>
|
||||
Learn more about
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://docs.netbird.io/how-to/add-users-to-your-network"
|
||||
>
|
||||
{" "}
|
||||
user management
|
||||
</a>
|
||||
</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Container>
|
||||
</Modal>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default InviteUserPopup;
|
||||
|
||||
@@ -10,6 +10,7 @@ import WindowsTab from "./WindowsTab";
|
||||
import MacTab from "./MacTab";
|
||||
import Link from "antd/lib/typography/Link";
|
||||
import DockerTab from "./DockerTab";
|
||||
import AndroidTab from "./AndroidTab";
|
||||
|
||||
type Props = {
|
||||
greeting?: string;
|
||||
@@ -38,27 +39,27 @@ export const AddPeerPopup: React.FC<Props> = ({
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
key: "1",
|
||||
label: <span><Icon component={LinuxSVG}/>Linux</span>,
|
||||
label: <span data-testid="add-peer-modal-linux-tab"><Icon component={LinuxSVG}/>Linux</span>,
|
||||
children: <UbuntuTab/>,
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: <span><WindowsFilled/>Windows</span>,
|
||||
label: <span data-testid="add-peer-modal-windows-tab"><WindowsFilled/>Windows</span>,
|
||||
children: <WindowsTab/>,
|
||||
},
|
||||
{
|
||||
key: "3",
|
||||
label: <span><AppleFilled/>macOS</span>,
|
||||
label: <span data-testid="add-peer-modal-mac-tab"><AppleFilled/>MacOS</span>,
|
||||
children: <MacTab/>,
|
||||
},
|
||||
/*{
|
||||
{
|
||||
key: "4",
|
||||
label: <span><AndroidFilled/>Android</span>,
|
||||
children: <></>,
|
||||
},*/
|
||||
label: <span data-testid="add-peer-modal-android-tab"><AndroidFilled/>Android</span>,
|
||||
children: <AndroidTab/>,
|
||||
},
|
||||
{
|
||||
key: "5",
|
||||
label: <span><Icon component={DockerSVG}/>Docker</span>,
|
||||
label: <span data-testid="add-peer-modal-docker-tab"><Icon component={DockerSVG}/>Docker</span>,
|
||||
children: <DockerTab/>,
|
||||
}
|
||||
];
|
||||
@@ -78,7 +79,7 @@ export const AddPeerPopup: React.FC<Props> = ({
|
||||
textAlign: "center",
|
||||
whiteSpace: "pre-line",
|
||||
}}>
|
||||
To get started install NetBird and log in using your {"\n"} email account.
|
||||
To get started, install NetBird and log in using your {"\n"} email account.
|
||||
</Paragraph>
|
||||
|
||||
<Tabs centered={!isMobile}
|
||||
@@ -91,7 +92,7 @@ export const AddPeerPopup: React.FC<Props> = ({
|
||||
After that you should be connected. Add more devices to your network or manage your existing devices in the
|
||||
admin panel.
|
||||
If you have further questions check out our {<Link target="_blank"
|
||||
href={"https://netbird.io/docs/getting-started/installation"}>installation
|
||||
href={"https://docs.netbird.io/how-to/getting-started#installation"}>installation
|
||||
guide</Link>}
|
||||
</Paragraph>
|
||||
</>
|
||||
|
||||
62
src/components/popups/addpeer/addpeer/AndroidTab.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React, {useState} from 'react';
|
||||
|
||||
import {Button, Image, Typography} from "antd";
|
||||
import TabSteps from "./TabSteps";
|
||||
import { StepCommand } from "./types"
|
||||
import googleplay from '../../../../assets/google-play-badge.png';
|
||||
import {getConfig} from "../../../../config";
|
||||
const { grpcApiOrigin } = getConfig();
|
||||
|
||||
const {Text} = Typography;
|
||||
|
||||
export const AndroidTab = () => {
|
||||
|
||||
const [steps, setSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: 'Download and install the application from Google Play Store:',
|
||||
commands: (
|
||||
<a data-testid="download-android-button" href="https://play.google.com/store/apps/details?id=io.netbird.client" target="_blank">
|
||||
<Image width="12em" preview={false} style={{marginTop: "5px"}} src={googleplay}/>
|
||||
</a>
|
||||
),
|
||||
copied: false
|
||||
} as StepCommand,
|
||||
... grpcApiOrigin ? [{
|
||||
key: 2,
|
||||
title: 'Click on "Change Server" and enter the following "Server"',
|
||||
commands: grpcApiOrigin,
|
||||
commandsForCopy: grpcApiOrigin,
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
}] : [],
|
||||
{
|
||||
key: 2 + (grpcApiOrigin ? 1 : 0),
|
||||
title: 'Click on the "Connect" button in the middle of the screen',
|
||||
commands: '',
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
},
|
||||
{
|
||||
key: 3 + (grpcApiOrigin ? 1 : 0),
|
||||
title: 'Sign up using your email address',
|
||||
commands: '',
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
}
|
||||
])
|
||||
|
||||
return (
|
||||
<div style={{marginTop: 10}}>
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Install on Android
|
||||
</Text>
|
||||
<div style={{marginTop: 5}}>
|
||||
<TabSteps stepsItems={steps}/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AndroidTab
|
||||
@@ -11,32 +11,46 @@ const {Title, Paragraph, Text} = Typography;
|
||||
export const DockerTab = () => {
|
||||
|
||||
const [steps, setSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: 'Install Docker',
|
||||
commands: (
|
||||
<Button style={{marginTop: "5px"}} type="primary" href="https://docs.docker.com/engine/install/" target="_blank">Official Docker website</Button>
|
||||
),
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: 'Run NetBird container',
|
||||
commands: formatDockerCommand(),
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 3,
|
||||
title: "Read docs",
|
||||
commands: (
|
||||
<Link href="https://netbird.io/docs/getting-started/installation#running-netbird-in-docker" target="_blank">Running NetBird in Docker</Link>
|
||||
),
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
} as StepCommand
|
||||
])
|
||||
{
|
||||
key: 1,
|
||||
title: "Install Docker",
|
||||
commands: (
|
||||
<Button
|
||||
data-testid="download-docker-button"
|
||||
style={{ marginTop: "5px" }}
|
||||
type="primary"
|
||||
href="https://docs.docker.com/engine/install/"
|
||||
target="_blank"
|
||||
>
|
||||
Official Docker website
|
||||
</Button>
|
||||
),
|
||||
copied: false,
|
||||
showCopyButton: false,
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: "Run NetBird container",
|
||||
commands: formatDockerCommand(),
|
||||
commandsForCopy: formatDockerCommand(),
|
||||
copied: false,
|
||||
showCopyButton: false,
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 3,
|
||||
title: "Read docs",
|
||||
commands: (
|
||||
<Link
|
||||
href="https://docs.netbird.io/how-to/getting-started#running-net-bird-in-docker"
|
||||
target="_blank"
|
||||
>
|
||||
Running NetBird in Docker
|
||||
</Link>
|
||||
),
|
||||
copied: false,
|
||||
showCopyButton: false,
|
||||
} as StepCommand,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div style={{marginTop: 10}}>
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { useState } from 'react';
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "antd";
|
||||
import TabSteps from "./TabSteps";
|
||||
import { StepCommand } from "./types"
|
||||
|
||||
import { StepCommand } from "./types";
|
||||
|
||||
export const OtherTab = () => {
|
||||
const [steps, _] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: "For other installation options check our documentation.",
|
||||
commands: (
|
||||
<Button
|
||||
type="primary"
|
||||
href={`https://docs.netbird.io/how-to/getting-started#binary-install`}
|
||||
target="_blank"
|
||||
>
|
||||
Documentation
|
||||
</Button>
|
||||
),
|
||||
copied: false,
|
||||
} as StepCommand,
|
||||
]);
|
||||
|
||||
const [steps, _] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: 'For other installation options check our documentation.',
|
||||
commands: (
|
||||
<Button type="primary" href={`https://netbird.io/docs/getting-started/installation#binary-install`} target="_blank">
|
||||
Documentation
|
||||
</Button>
|
||||
),
|
||||
copied: false,
|
||||
} as StepCommand,
|
||||
])
|
||||
return <TabSteps stepsItems={steps} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<TabSteps stepsItems={steps} />
|
||||
)
|
||||
}
|
||||
|
||||
export default OtherTab
|
||||
export default OtherTab;
|
||||
|
||||
@@ -1,113 +1,223 @@
|
||||
import React, {useState} from 'react';
|
||||
import React, { useState } from "react";
|
||||
|
||||
import {Button, Typography} from "antd";
|
||||
import { Button, Divider, Row, Tooltip, Typography } from "antd";
|
||||
import TabSteps from "./TabSteps";
|
||||
import {StepCommand} from "./types"
|
||||
import {formatNetBirdUP} from "./common"
|
||||
import {Collapse} from "antd";
|
||||
import { StepCommand } from "./types";
|
||||
import { formatNetBirdUP } from "./common";
|
||||
import { Collapse } from "antd";
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import { QuestionCircleOutlined } from "@ant-design/icons";
|
||||
import { CheckOutlined, CopyOutlined } from "@ant-design/icons";
|
||||
import { copyToClipboard } from "../../../../utils/common";
|
||||
import {getConfig} from "../../../../config";
|
||||
const { grpcApiOrigin } = getConfig();
|
||||
|
||||
const { Panel } = Collapse;
|
||||
|
||||
const {Text} = Typography;
|
||||
const { Text } = Typography;
|
||||
|
||||
export const LinuxTab = () => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [quickSteps, setQuickSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: (
|
||||
<Row>
|
||||
<Text>Download and run MacOS installer: </Text>
|
||||
<Tooltip
|
||||
title={
|
||||
<text>
|
||||
If you don't know what chip your Mac has, you can find out by
|
||||
clicking on the Apple logo in the top left corner of your screen
|
||||
and selecting 'About This Mac'. For more information click{" "}
|
||||
<a
|
||||
href="https://support.apple.com/en-us/HT211814"
|
||||
target="_blank"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
</text>
|
||||
}
|
||||
className={"ant-form-item-tooltip"}
|
||||
>
|
||||
<QuestionCircleOutlined
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.45)",
|
||||
cursor: "help",
|
||||
marginLeft: "3px",
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Row>
|
||||
),
|
||||
commands: (
|
||||
<Row style={{ paddingTop: "5px" }}>
|
||||
<Button
|
||||
data-testid="download-intel-button"
|
||||
style={{ marginRight: "10px" }}
|
||||
type="primary"
|
||||
href="https://pkgs.netbird.io/macos/amd64"
|
||||
>
|
||||
Download for Intel
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="download-m1-m2-button"
|
||||
style={{ marginRight: "10px" }}
|
||||
type="default"
|
||||
href="https://pkgs.netbird.io/macos/arm64"
|
||||
>
|
||||
Download for M1 & M2
|
||||
</Button>
|
||||
</Row>
|
||||
),
|
||||
copied: false,
|
||||
} as StepCommand,
|
||||
... grpcApiOrigin ? [{
|
||||
key: 2,
|
||||
title: 'Click on "Settings" from the NetBird icon in your system tray and enter the following "Management URL"',
|
||||
commands: grpcApiOrigin,
|
||||
commandsForCopy: grpcApiOrigin,
|
||||
copied: false,
|
||||
showCopyButton: false,
|
||||
}] : [],
|
||||
{
|
||||
key: 2 + (grpcApiOrigin ? 1 : 0),
|
||||
title: 'Click on "Connect" from the NetBird icon in your system tray',
|
||||
commands: "",
|
||||
copied: false,
|
||||
showCopyButton: false,
|
||||
},
|
||||
{
|
||||
key: 3 + (grpcApiOrigin) ? 1 : 0,
|
||||
title: "Sign up using your email address",
|
||||
commands: "",
|
||||
copied: false,
|
||||
showCopyButton: false,
|
||||
},
|
||||
]);
|
||||
|
||||
const [quickSteps, setQuickSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: 'Download and run installer:',
|
||||
commands: (
|
||||
<Button style={{marginTop: "5px"}} type="primary" href="https://pkgs.netbird.io/windows/x64"
|
||||
target="_blank">Download NetBird</Button>
|
||||
),
|
||||
copied: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: 'Click on "Connect" from the NetBird icon in your system tray',
|
||||
commands: '',
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
title: 'Sign up using your email address',
|
||||
commands: '',
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
}
|
||||
])
|
||||
const [steps, setSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: "Download and install Homebrew",
|
||||
commands: (
|
||||
<Button
|
||||
style={{ marginTop: "5px" }}
|
||||
type="primary"
|
||||
href="https://brew.sh/"
|
||||
target="_blank"
|
||||
>
|
||||
Download Brew
|
||||
</Button>
|
||||
),
|
||||
copied: false,
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: "Install NetBird:",
|
||||
commands: [
|
||||
`# for CLI only`,
|
||||
`brew install netbirdio/tap/netbird`,
|
||||
`# for GUI package`,
|
||||
`brew install --cask netbirdio/tap/netbird-ui`,
|
||||
].join("\n"),
|
||||
commandsForCopy: [
|
||||
`brew install netbirdio/tap/netbird`,
|
||||
`brew install --cask netbirdio/tap/netbird-ui`,
|
||||
].join("\n"),
|
||||
copied: false,
|
||||
showCopyButton: true,
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 3,
|
||||
title: "Start NetBird daemon:",
|
||||
commands: [
|
||||
`sudo netbird service install`,
|
||||
`sudo netbird service start`,
|
||||
].join("\n"),
|
||||
commandsForCopy: [
|
||||
`sudo netbird service install`,
|
||||
`sudo netbird service start`,
|
||||
].join("\n"),
|
||||
copied: false,
|
||||
showCopyButton: true,
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 4,
|
||||
title: "Run NetBird and log in the browser:",
|
||||
commands: formatNetBirdUP(),
|
||||
commandsForCopy: formatNetBirdUP(),
|
||||
copied: false,
|
||||
showCopyButton: true,
|
||||
} as StepCommand,
|
||||
]);
|
||||
|
||||
const [steps, setSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: 'Download and install Homebrew',
|
||||
commands: (
|
||||
<Button style={{marginTop: "5px"}} type="primary" href="https://brew.sh/" target="_blank">Download
|
||||
Brew</Button>
|
||||
),
|
||||
copied: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: 'Install NetBird:',
|
||||
commands: [
|
||||
`# for CLI only`,
|
||||
`brew install netbirdio/tap/netbird`,
|
||||
`# for GUI package`,
|
||||
`brew install --cask netbirdio/tap/netbird-ui`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 3,
|
||||
title: 'Start NetBird daemon:',
|
||||
commands: [
|
||||
`sudo netbird service install`,
|
||||
`sudo netbird service start`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 4,
|
||||
title: 'Run NetBird and log in the browser:',
|
||||
commands: formatNetBirdUP(),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand
|
||||
])
|
||||
|
||||
return (
|
||||
<div style={{marginTop: 10}}>
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Install with one command
|
||||
</Text>
|
||||
<div style={{fontSize: ".85em", marginTop: 5, marginBottom: 25}}>
|
||||
<SyntaxHighlighter language="bash">
|
||||
curl -fsSL https://pkgs.netbird.io/install.sh | sh
|
||||
</SyntaxHighlighter>
|
||||
const onCopyClick = () => {
|
||||
const stringToCopy = "curl -fsSL https://pkgs.netbird.io/install.sh | sh";
|
||||
copyToClipboard(stringToCopy);
|
||||
setCopied(true);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Text style={{ fontWeight: "bold" }}>Install on MacOS</Text>
|
||||
<div style={{ marginTop: 5, marginBottom: 5 }}>
|
||||
<TabSteps stepsItems={quickSteps} />
|
||||
</div>
|
||||
<div style={{ marginTop: 0 }} />
|
||||
{/*<Divider style={{marginTop: "5px"}} />*/}
|
||||
<Collapse bordered={false} style={{ backgroundColor: "unset" }}>
|
||||
<Panel
|
||||
className="CustomPopupCollapse"
|
||||
header={<Text strong={true}>Or install via command line</Text>}
|
||||
key="1"
|
||||
>
|
||||
<div style={{ marginLeft: "25px" }}>
|
||||
<Text style={{ fontWeight: "bold" }}>Install with one command</Text>
|
||||
<div
|
||||
style={{
|
||||
fontSize: ".85em",
|
||||
marginTop: 5,
|
||||
marginBottom: 25,
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{!copied ? (
|
||||
<Button
|
||||
type="text"
|
||||
size="middle"
|
||||
className="btn-copy-code peer"
|
||||
icon={<CopyOutlined />}
|
||||
style={{ color: "rgb(107, 114, 128)", top: "0", zIndex: "3" }}
|
||||
onClick={onCopyClick}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
type="text"
|
||||
size="middle"
|
||||
className="btn-copy-code peer"
|
||||
icon={<CheckOutlined />}
|
||||
style={{ color: "green", top: "0", zIndex: "3" }}
|
||||
/>
|
||||
)}
|
||||
<SyntaxHighlighter language="bash">
|
||||
curl -fsSL https://pkgs.netbird.io/install.sh | sh
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Or install manually with HomeBrew
|
||||
<Text style={{ fontWeight: "bold" }}>
|
||||
Or install manually with HomeBrew
|
||||
</Text>
|
||||
<div style={{marginTop: 5}}>
|
||||
<TabSteps stepsItems={steps}/>
|
||||
<div style={{ marginTop: 5 }}>
|
||||
<TabSteps stepsItems={steps} />
|
||||
</div>
|
||||
</div>
|
||||
/*<div style={{marginTop: 5}}>
|
||||
<TabSteps stepsItems={quickSteps}/>
|
||||
</div>*/
|
||||
/*<div style={{marginTop: 10}}>
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Install on macOS with Homebrew
|
||||
</Text>
|
||||
<div style={{marginTop: 5}}>
|
||||
<TabSteps stepsItems={steps}/>
|
||||
</div>
|
||||
</div>*/
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinuxTab
|
||||
export default LinuxTab;
|
||||
|
||||
@@ -1,63 +1,89 @@
|
||||
import "highlight.js/styles/mono-blue.css";
|
||||
import "highlight.js/lib/languages/bash";
|
||||
import { StepCommand } from './types'
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import {
|
||||
Typography,
|
||||
Space,
|
||||
Steps, Button, Popover, StepsProps
|
||||
} from "antd";
|
||||
import {copyToClipboard} from "../../../../utils/common";
|
||||
import {CheckOutlined, CopyOutlined} from "@ant-design/icons";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import { StepCommand } from "./types";
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import { Typography, Space, Steps, Button, Popover, StepsProps } from "antd";
|
||||
import { copyToClipboard } from "../../../../utils/common";
|
||||
import { CheckOutlined, CopyOutlined } from "@ant-design/icons";
|
||||
import React, { useEffect, useState } from "react";
|
||||
const { Step } = Steps;
|
||||
const {Text} = Typography;
|
||||
const { Text } = Typography;
|
||||
|
||||
type Props = {
|
||||
stepsItems: Array<StepCommand>
|
||||
stepsItems: Array<StepCommand>;
|
||||
};
|
||||
|
||||
const TabSteps:React.FC<Props> = ({stepsItems}) => {
|
||||
const TabSteps: React.FC<Props> = ({ stepsItems }) => {
|
||||
const [steps, setSteps] = useState(stepsItems);
|
||||
|
||||
const [steps, setSteps] = useState(stepsItems)
|
||||
useEffect(() => setSteps(stepsItems), [stepsItems]);
|
||||
|
||||
useEffect(() => setSteps(stepsItems), [stepsItems])
|
||||
const onCopyClick = (
|
||||
key: string | number,
|
||||
commands: React.ReactNode | string,
|
||||
copied: boolean
|
||||
) => {
|
||||
if (!(typeof commands === "string")) return;
|
||||
copyToClipboard(commands);
|
||||
const step = steps.find((s) => s.key === key);
|
||||
if (step) step.copied = copied;
|
||||
setSteps([...steps]);
|
||||
|
||||
const onCopyClick = (key: string | number, commands:React.ReactNode | string, copied: boolean) => {
|
||||
if (!(typeof commands === 'string')) return
|
||||
copyToClipboard(commands)
|
||||
const step = steps.find(s => s.key === key)
|
||||
if (step) step.copied = copied
|
||||
setSteps([...steps])
|
||||
|
||||
if (copied) {
|
||||
setTimeout(() => {
|
||||
onCopyClick(key, commands, false)
|
||||
}, 2000)
|
||||
}
|
||||
if (copied) {
|
||||
setTimeout(() => {
|
||||
onCopyClick(key, commands, false);
|
||||
}, 2000);
|
||||
}
|
||||
return (
|
||||
<Steps direction="vertical" size={"small"}>
|
||||
{steps.map(c =>
|
||||
<Step
|
||||
status={"process"}
|
||||
key={c.key}
|
||||
title={<Text>{c.title}</Text>}
|
||||
description={
|
||||
<Space className="nb-code" direction="vertical" size="small" style={{display: "flex", fontSize: ".85em"}}>
|
||||
{ (c.commands && (typeof c.commands === 'string')) ? (
|
||||
<SyntaxHighlighter language="bash">
|
||||
{c.commands}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
c.commands
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Steps>
|
||||
)
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Steps direction="vertical" size={"small"}>
|
||||
{steps.map((c) => (
|
||||
<Step
|
||||
status={"process"}
|
||||
key={c.key}
|
||||
title={<Text>{c.title}</Text>}
|
||||
description={
|
||||
<Space
|
||||
className="nb-code"
|
||||
direction="vertical"
|
||||
size="small"
|
||||
style={{ display: "flex", fontSize: ".85em" }}
|
||||
>
|
||||
{c.commands && typeof c.commands === "string" ? (
|
||||
<>
|
||||
{!c.copied ? (
|
||||
<Button
|
||||
type="text"
|
||||
size="middle"
|
||||
className="btn-copy-code peer"
|
||||
icon={<CopyOutlined />}
|
||||
style={{ color: "rgb(107, 114, 128)"}}
|
||||
onClick={() => {
|
||||
onCopyClick(c.key, c.commandsForCopy, true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
type="text"
|
||||
size="middle"
|
||||
className="btn-copy-code peer"
|
||||
icon={<CheckOutlined />}
|
||||
style={{ color: "green"}}
|
||||
/>
|
||||
)}
|
||||
<SyntaxHighlighter language="bash">
|
||||
{c.commands}
|
||||
</SyntaxHighlighter>
|
||||
</>
|
||||
) : (
|
||||
c.commands
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Steps>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabSteps;
|
||||
export default TabSteps;
|
||||
|
||||
@@ -1,68 +1,112 @@
|
||||
import React, {useState} from 'react';
|
||||
import {StepCommand} from "./types"
|
||||
import {formatNetBirdUP} from "./common"
|
||||
import React, { useState } from "react";
|
||||
import { StepCommand } from "./types";
|
||||
import { formatNetBirdUP } from "./common";
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import TabSteps from "./TabSteps";
|
||||
import {Typography} from "antd";
|
||||
import { Typography, Button } from "antd";
|
||||
import { CheckOutlined, CopyOutlined } from "@ant-design/icons";
|
||||
import { copyToClipboard } from "../../../../utils/common";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
|
||||
export const UbuntuTab = () => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const [steps, setSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: 'Add repository',
|
||||
commands: [
|
||||
`sudo apt install ca-certificates curl gnupg -y`,
|
||||
`curl -L https://pkgs.wiretrustee.com/debian/public.key | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/wiretrustee.gpg`,
|
||||
`echo 'deb https://pkgs.wiretrustee.com/debian stable main' | sudo tee /etc/apt/sources.list.d/wiretrustee.list`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: 'Install NetBird',
|
||||
commands: [
|
||||
`sudo apt-get update`,
|
||||
`# for CLI only`,
|
||||
`sudo apt-get install netbird`,
|
||||
`# for GUI package`,
|
||||
`sudo apt-get install netbird-ui`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 3,
|
||||
title: 'Run NetBird and log in the browser',
|
||||
commands: formatNetBirdUP(),
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
} as StepCommand
|
||||
])
|
||||
const [steps, setSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: "Add repository",
|
||||
commands: [
|
||||
`sudo apt-get update`,
|
||||
`sudo apt install ca-certificates curl gnupg -y`,
|
||||
`curl -sSL https://pkgs.netbird.io/debian/public.key | sudo gpg --dearmor --output /usr/share/keyrings/netbird-archive-keyring.gpg`,
|
||||
`echo 'deb [signed-by=/usr/share/keyrings/netbird-archive-keyring.gpg] https://pkgs.netbird.io/debian stable main' | sudo tee /etc/apt/sources.list.d/netbird.list`,
|
||||
].join("\n"),
|
||||
commandsForCopy: [
|
||||
`sudo apt-get update`,
|
||||
`sudo apt-get install ca-certificates curl gnupg -y`,
|
||||
`curl -sSL https://pkgs.netbird.io/debian/public.key | sudo gpg --dearmor --output /usr/share/keyrings/netbird-archive-keyring.gpg`,
|
||||
`echo 'deb [signed-by=/usr/share/keyrings/netbird-archive-keyring.gpg] https://pkgs.netbird.io/debian stable main' | sudo tee /etc/apt/sources.list.d/netbird.list`,
|
||||
].join("\n"),
|
||||
copied: false,
|
||||
showCopyButton: false,
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: "Install NetBird",
|
||||
commands: [
|
||||
`sudo apt-get update`,
|
||||
`# for CLI only`,
|
||||
`sudo apt-get install netbird`,
|
||||
`# for GUI package`,
|
||||
`sudo apt-get install netbird-ui`,
|
||||
].join("\n"),
|
||||
commandsForCopy: [
|
||||
`sudo apt-get update`,
|
||||
`sudo apt-get install netbird`,
|
||||
`sudo apt-get install netbird-ui`,
|
||||
].join("\n"),
|
||||
copied: false,
|
||||
showCopyButton: false,
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 3,
|
||||
title: "Run NetBird and log in the browser",
|
||||
commands: formatNetBirdUP(),
|
||||
commandsForCopy: formatNetBirdUP(),
|
||||
copied: false,
|
||||
showCopyButton: false,
|
||||
} as StepCommand,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div style={{marginTop: 10}}>
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Install with one command
|
||||
</Text>
|
||||
<div style={{fontSize: ".85em", marginTop: 5, marginBottom: 25}}>
|
||||
<SyntaxHighlighter language="bash">
|
||||
curl -fsSL https://pkgs.netbird.io/install.sh | sh
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Or install manually on Ubuntu
|
||||
</Text>
|
||||
<div style={{marginTop: 5}}>
|
||||
<TabSteps stepsItems={steps}/>
|
||||
</div>
|
||||
const onCopyClick = () => {
|
||||
const stringToCopy = "curl -fsSL https://pkgs.netbird.io/install.sh | sh";
|
||||
copyToClipboard(stringToCopy);
|
||||
setCopied(true);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
</div>
|
||||
return (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Text style={{ fontWeight: "bold" }}>Install with one command</Text>
|
||||
<div
|
||||
style={{
|
||||
fontSize: ".85em",
|
||||
marginTop: 5,
|
||||
marginBottom: 25,
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{!copied ? (
|
||||
<Button
|
||||
type="text"
|
||||
size="middle"
|
||||
className="btn-copy-code peer"
|
||||
icon={<CopyOutlined />}
|
||||
style={{ color: "rgb(107, 114, 128)", top: "0", zIndex: "3" }}
|
||||
onClick={onCopyClick}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
type="text"
|
||||
size="middle"
|
||||
className="btn-copy-code peer"
|
||||
icon={<CheckOutlined />}
|
||||
style={{ color: "green", top: "0", zIndex: "3" }}
|
||||
/>
|
||||
)}
|
||||
<SyntaxHighlighter language="bash">
|
||||
curl -fsSL https://pkgs.netbird.io/install.sh | sh
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
<Text style={{ fontWeight: "bold" }}>Or install manually on Ubuntu</Text>
|
||||
<div style={{ marginTop: 5 }}>
|
||||
<TabSteps stepsItems={steps} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default UbuntuTab
|
||||
export default UbuntuTab;
|
||||
|
||||
@@ -3,6 +3,9 @@ import React, {useState} from 'react';
|
||||
import {Button, Typography} from "antd";
|
||||
import TabSteps from "./TabSteps";
|
||||
import { StepCommand } from "./types"
|
||||
import {getConfig} from "../../../../config";
|
||||
const { grpcApiOrigin } = getConfig();
|
||||
|
||||
const {Text} = Typography;
|
||||
|
||||
export const WindowsTab = () => {
|
||||
@@ -12,19 +15,27 @@ export const WindowsTab = () => {
|
||||
key: 1,
|
||||
title: 'Download and run Windows installer:',
|
||||
commands: (
|
||||
<Button style={{marginTop: "5px"}} type="primary" href="https://pkgs.netbird.io/windows/x64" target="_blank">Download NetBird</Button>
|
||||
<Button data-testid="download-windows-button" style={{marginTop: "5px"}} type="primary" href="https://pkgs.netbird.io/windows/x64" target="_blank">Download NetBird</Button>
|
||||
),
|
||||
copied: false
|
||||
} as StepCommand,
|
||||
{
|
||||
... grpcApiOrigin ? [{
|
||||
key: 2,
|
||||
title: 'Click on "Settings" from the NetBird icon in your system tray and enter the following "Management URL"',
|
||||
commands: grpcApiOrigin,
|
||||
commandsForCopy: grpcApiOrigin,
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
}] : [],
|
||||
{
|
||||
key: 2 + (grpcApiOrigin ? 1 : 0),
|
||||
title: 'Click on "Connect" from the NetBird icon in your system tray',
|
||||
commands: '',
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
key: 3 + (grpcApiOrigin ? 1 : 0),
|
||||
title: 'Sign up using your email address',
|
||||
commands: '',
|
||||
copied: false,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as React from "react";
|
||||
|
||||
export interface StepCommand {
|
||||
key: number | string,
|
||||
title: React.ReactNode | string | null,
|
||||
commands: React.ReactNode | string | null,
|
||||
copied?: boolean,
|
||||
showCopyButton?: boolean
|
||||
key: number | string;
|
||||
title: React.ReactNode | string | null;
|
||||
commands: React.ReactNode | string | null;
|
||||
commandsForCopy?: React.ReactNode | string | null;
|
||||
copied?: boolean;
|
||||
showCopyButton?: boolean;
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"auth0Auth": "$USE_AUTH0",
|
||||
"authAuthority": "$AUTH_AUTHORITY",
|
||||
"authClientId": "$AUTH_CLIENT_ID",
|
||||
"authClientSecret": "$AUTH_CLIENT_SECRET",
|
||||
"authScopesSupported": "$AUTH_SUPPORTED_SCOPES",
|
||||
"authAudience": "$AUTH_AUDIENCE",
|
||||
|
||||
@@ -10,5 +11,6 @@
|
||||
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
|
||||
"redirectURI": "$AUTH_REDIRECT_URI",
|
||||
"silentRedirectURI": "$AUTH_SILENT_REDIRECT_URI",
|
||||
"tokenSource": "$NETBIRD_TOKEN_SOURCE"
|
||||
"tokenSource": "$NETBIRD_TOKEN_SOURCE",
|
||||
"dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS"
|
||||
}
|
||||
@@ -28,8 +28,9 @@ export function getConfig() {
|
||||
|
||||
return {
|
||||
auth0Auth: configJson.auth0Auth == "true", //due to substitution we can't use boolean in the config
|
||||
authority: configJson.authAuthority,
|
||||
authority: configJson.authAuthority.replace(/\/+$/, ''),
|
||||
clientId: configJson.authClientId,
|
||||
clientSecret: configJson.authClientSecret,
|
||||
scopesSupported: configJson.authScopesSupported,
|
||||
apiOrigin: configJson.apiOrigin,
|
||||
grpcApiOrigin: configJson.grpcApiOrigin,
|
||||
@@ -38,5 +39,7 @@ export function getConfig() {
|
||||
redirectURI: redirectURI,
|
||||
silentRedirectURI: silentRedirectURI,
|
||||
tokenSource: tokenSource,
|
||||
// drags all the query params to the auth layer specified in the URL when accessing dashboard.
|
||||
dragQueryParams: configJson.dragQueryParams == "true"
|
||||
};
|
||||
}
|
||||
|
||||
399
src/index.css
@@ -1,16 +1,35 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Arimo:wght@400;500;600&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inconsolata:wght@400;500&display=swap');
|
||||
@import 'antd/dist/reset.css';
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre *,
|
||||
code *,
|
||||
kbd *,
|
||||
samp * {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace !important;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
* {
|
||||
font-family: 'Arimo', sans-serif !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 16px;
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
|
||||
|
||||
.ant-layout-header {
|
||||
background: #fff;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
min-height: 64px;
|
||||
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
@@ -23,13 +42,14 @@ body {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ant-menu-horizontal > .ant-menu-item a {
|
||||
.ant-menu-horizontal>.ant-menu-item a {
|
||||
color: rgba(107, 114, 128, 1);
|
||||
font-weight: 500;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.ant-menu-horizontal > .ant-menu-item-selected a {
|
||||
.ant-menu-horizontal>.ant-menu-item-selected a {
|
||||
color: rgba(17, 24, 39, 1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,23 +67,25 @@ body {
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.space-align-block {
|
||||
flex: none;
|
||||
margin: 8px 4px;
|
||||
padding: 4px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.space-align-block .mock-block {
|
||||
display: inline-block;
|
||||
padding: 32px 8px 16px;
|
||||
background: rgba(150, 150, 150, 0.2);
|
||||
}
|
||||
|
||||
.bg-indigo-600{
|
||||
.bg-indigo-600 {
|
||||
background-color: rgb(79, 70, 229);
|
||||
}
|
||||
|
||||
.card-table-no-placeholder .ant-table-placeholder{
|
||||
.card-table-no-placeholder .ant-table-placeholder {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -75,9 +97,10 @@ body {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.card-table .ant-table-ping-left:not(.ant-table-has-fix-left) .ant-table-container::before {
|
||||
.card-table .ant-table-ping-left:not(.ant-table-has-fix-left) .ant-table-container::before {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.card-table .ant-table-ping-right:not(.ant-table-has-fix-right) .ant-table-container::after {
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -119,15 +142,17 @@ body {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.ant-steps-item-icon{
|
||||
.ant-steps-item-icon {
|
||||
background: #EBEBEB !important;
|
||||
border-color: #EBEBEB !important;
|
||||
}
|
||||
.ant-steps-icon-dot{
|
||||
|
||||
.ant-steps-icon-dot {
|
||||
background: #EBEBEB !important;
|
||||
border-color: #EBEBEB !important;
|
||||
}
|
||||
.ant-steps-icon {
|
||||
|
||||
.ant-steps-icon {
|
||||
background: #EBEBEB !important;
|
||||
border: none;
|
||||
color: black !important;
|
||||
@@ -145,10 +170,362 @@ td.non-highlighted-table-column {
|
||||
background-color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr.ant-table-row:hover > td {
|
||||
.ant-table-tbody>tr.ant-table-row:hover>td {
|
||||
background: #FAFAFA !important;
|
||||
}
|
||||
|
||||
.ant-table-thead .ant-table-cell {
|
||||
font-weight: normal !important;
|
||||
}
|
||||
|
||||
.CustomPopupCollapse .ant-collapse-content-box,
|
||||
.CustomPopupCollapse .ant-collapse-header {
|
||||
padding-left: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.system-info-panel .ant-collapse-header {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.system-info-panel .ant-collapse-content-box {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.peers-form .ant-layout-header {
|
||||
line-height: 25px;
|
||||
height: auto;
|
||||
min-height: auto;
|
||||
padding-bottom: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
border-radius: 0px !important;
|
||||
}
|
||||
|
||||
.ant-form-item-explain-error {
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.menlo-font,
|
||||
.menlo-font * {
|
||||
font-family: 'Menlo', monospace !important;
|
||||
}
|
||||
|
||||
.arimo-font,
|
||||
.arimo-font * {
|
||||
font-family: 'Arimo', sans-serif !important;
|
||||
}
|
||||
|
||||
.tag-box .ant-select-selector {
|
||||
padding: 0 5px !important;
|
||||
}
|
||||
|
||||
.tag-box .ant-select-selection-item {
|
||||
width: 100%;
|
||||
line-height: 20px;
|
||||
justify-content: center;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
opacity: 1;
|
||||
transition: all 0.2s;
|
||||
text-align: start;
|
||||
max-width: 40px;
|
||||
height: 25px;
|
||||
padding: 0 4px !important;
|
||||
align-items: center;
|
||||
margin-top: 3px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.w-100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.font-500 {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
font-weight: 500 !important;
|
||||
font-size: 22px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.ant-tag {
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.react-select__indicator-separator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.react-select__control,
|
||||
.react-select__value-container,
|
||||
.react-select__input-container {
|
||||
min-height: 32px !important;
|
||||
padding: 0 5px !important;
|
||||
max-height: 32px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.react-select__value-container {
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.react-select__indicator {
|
||||
padding: 0 5px !important;
|
||||
}
|
||||
|
||||
.ant-badge-status-dot {
|
||||
width: 8px !important;
|
||||
height: 8px !important;
|
||||
}
|
||||
|
||||
.routes-accordian {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.routes-accordian .ant-collapse-header {
|
||||
background: #fff;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content-box {
|
||||
background: #FAFAFA;
|
||||
padding: 5px 16px !important;
|
||||
}
|
||||
|
||||
.accordian-header p {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
margin: 0;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.headerInner {
|
||||
display: flex;
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
.headerInner p {
|
||||
margin: 0;
|
||||
width: 33.33%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.accordian-header {
|
||||
display: flex;
|
||||
padding: 10px 40px;
|
||||
}
|
||||
|
||||
.accordian-inner-header {
|
||||
display: flex;
|
||||
padding: 10px 40px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.accordian-inner-header p {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
margin: 0;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.accordian-inner-listing {
|
||||
padding: 3px 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.accordian-inner-listing p {
|
||||
font-size: 14px;
|
||||
align-items: center;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
width: 10%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.headerInner .text-right {
|
||||
display: block;
|
||||
text-align: right;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.accordian-inner-listing p:nth-child(5) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.accordian-inner-listing p:nth-child(1) {
|
||||
width: 25%;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.accordian-inner-header p:nth-child(1) {
|
||||
width: calc(20% + 50px);
|
||||
}
|
||||
|
||||
.accordian-inner-listing p:nth-child(2) {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.accordian-inner-header p:nth-child(2) {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.accordian-inner-listing p:nth-child(3) {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.accordian-inner-listing p:nth-child(4) {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.ant-list-item {
|
||||
padding: 5px 0 !important;
|
||||
margin-inline: 0 !important;
|
||||
}
|
||||
|
||||
ul.ant-list-items {
|
||||
margin-top: 5px !important;
|
||||
}
|
||||
|
||||
.container-spinner {
|
||||
margin: 20px 0;
|
||||
margin-bottom: 20px;
|
||||
padding: 30px 50px;
|
||||
text-align: center;
|
||||
background: rgb(0 0 0 / 2%);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.remove-bg .ant-collapse-content-box {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.route-form .ant-form-item {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.nb-code {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.btn-copy-code.peer {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
.blue-color {
|
||||
color: #1890FF;
|
||||
}
|
||||
|
||||
.style-like-text .ant-select-selector {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
color: rgba(0, 0, 0, 1) !important;
|
||||
}
|
||||
|
||||
.style-like-text .ant-select-selection-search::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.style-like-text .ant-select-selection-item {
|
||||
padding-inline-end: 0 !important;
|
||||
}
|
||||
|
||||
.style-like-text .ant-select-arrow {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
.ant-spin-nested-loading .ant-spin-spinning {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.route-form.edit-form-wrapper .ant-form-item {
|
||||
margin-bottom: 39px;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
cursor: auto !important;
|
||||
}
|
||||
|
||||
.groupsSelect {
|
||||
max-width: 200px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 1220px) {
|
||||
.groupsSelect {
|
||||
margin-left: 0;
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.setting-nav {
|
||||
flex-flow: column !important;
|
||||
}
|
||||
|
||||
.setting-nav>div {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.noborderPadding .ant-card-body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.noborderPadding {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.d-none{
|
||||
display: none!important;
|
||||
}
|
||||
.nohover {
|
||||
background: transparent!important;
|
||||
cursor: text;
|
||||
}
|
||||
.emp-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.emp-wrapper p {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -10,6 +10,8 @@ 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";
|
||||
import InstallPage from "./views/Install";
|
||||
import {ConfigProvider} from "antd";
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
@@ -17,12 +19,29 @@ const config = getConfig();
|
||||
// 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,
|
||||
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 buildExtras = (config: any) => {
|
||||
type Extras = { [key: string]: string }
|
||||
let extras: Extras = {};
|
||||
|
||||
if (config.dragQueryParams) {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
searchParams.forEach((value, key) => {
|
||||
extras[key] = value
|
||||
});
|
||||
}
|
||||
|
||||
if (config.audience) {
|
||||
extras.audience = config.audience
|
||||
}
|
||||
return extras
|
||||
}
|
||||
|
||||
const providerConfig = {
|
||||
authority: config.authority,
|
||||
client_id: config.clientId,
|
||||
@@ -34,7 +53,8 @@ const providerConfig = {
|
||||
// service_worker_relative_url:'/OidcServiceWorker.js',
|
||||
service_worker_only: false,
|
||||
authority_configuration: config.auth0Auth ? auth0AuthorityConfig : undefined,
|
||||
...(config.audience ? {extras: {audience: config.audience}} : null)
|
||||
extras: buildExtras(config),
|
||||
...(config.clientSecret ? {token_request_extras: {client_secret: config.clientSecret}} : null)
|
||||
};
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
@@ -43,8 +63,14 @@ const root = ReactDOM.createRoot(
|
||||
|
||||
const loadingComponent = () => <Loading padding="3em" width={50} height={50}/>
|
||||
|
||||
root.render(
|
||||
<OidcProvider
|
||||
const showApp = () => {
|
||||
if (window.location.pathname === "/install") {
|
||||
// We bypass authentication for pages that do not require auth.
|
||||
// E.g., when we just want to show installation steps for public.
|
||||
return <InstallPage/>
|
||||
}
|
||||
|
||||
return <OidcProvider
|
||||
configuration={providerConfig}
|
||||
callbackSuccessComponent={loadingComponent}
|
||||
authenticatingErrorComponent={LoginError}
|
||||
@@ -59,6 +85,21 @@ root.render(
|
||||
<App/>
|
||||
</BrowserRouter>
|
||||
</OidcProvider>
|
||||
}
|
||||
|
||||
root.render(
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
borderRadius: 4,
|
||||
colorPrimary: "#1890ff",
|
||||
fontFamily: "Arial"
|
||||
},
|
||||
components: {Badge: {fontSizeSM: 20}},
|
||||
}}
|
||||
>
|
||||
{showApp()}
|
||||
</ConfigProvider>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Method } from 'axios';
|
||||
import { Method } from "axios";
|
||||
|
||||
export interface RequestPayload<T> {
|
||||
getAccessTokenSilently: any | null;
|
||||
queryParams?: any | null;
|
||||
payload:T;
|
||||
payload: T;
|
||||
}
|
||||
|
||||
export interface RequestHeader {
|
||||
'content-type': string;
|
||||
"content-type": string;
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
@@ -28,16 +28,16 @@ export interface RequestConfig {
|
||||
export interface ApiRequestParams extends RequestConfig {
|
||||
method: Method;
|
||||
url: string;
|
||||
params?: any,
|
||||
params?: any;
|
||||
data: unknown;
|
||||
urlBase: string;
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
code:string;
|
||||
message:string;
|
||||
data?:any;
|
||||
statusCode:number;
|
||||
code: string;
|
||||
message: string;
|
||||
data?: any;
|
||||
statusCode: number;
|
||||
}
|
||||
|
||||
export interface DeleteResponse<T> {
|
||||
@@ -45,7 +45,7 @@ export interface DeleteResponse<T> {
|
||||
success: boolean;
|
||||
failure: boolean;
|
||||
error: ApiError | null;
|
||||
data : T;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface CreateResponse<T> {
|
||||
@@ -53,7 +53,7 @@ export interface CreateResponse<T> {
|
||||
success: boolean;
|
||||
failure: boolean;
|
||||
error: ApiError | null;
|
||||
data : T;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface ChangeResponse<T> {
|
||||
@@ -61,5 +61,5 @@ export interface ChangeResponse<T> {
|
||||
success: boolean;
|
||||
failure: boolean;
|
||||
error: ApiError | null;
|
||||
data : T;
|
||||
}
|
||||
data: T;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import {ExpiresInValue} from "../../views/ExpiresInInput";
|
||||
|
||||
export interface Account {
|
||||
id: string;
|
||||
settings: { peer_login_expiration_enabled: boolean, peer_login_expiration: number}
|
||||
id: string;
|
||||
settings: {
|
||||
peer_login_expiration_enabled: boolean;
|
||||
peer_login_expiration: number;
|
||||
jwt_groups_enabled: boolean;
|
||||
groups_propagation_enabled: boolean;
|
||||
jwt_groups_claim_name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FormAccount extends Account {
|
||||
peer_login_expiration_enabled: boolean
|
||||
peer_login_expiration_formatted : ExpiresInValue
|
||||
peer_login_expiration_enabled: boolean;
|
||||
jwt_groups_enabled: boolean;
|
||||
groups_propagation_enabled: boolean;
|
||||
jwt_groups_claim_name: string;
|
||||
peer_login_expiration_formatted: ExpiresInValue;
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
export interface Event {
|
||||
id: string;
|
||||
timestamp: string
|
||||
activity: string
|
||||
activity_code: string
|
||||
initiator_id: string
|
||||
target_id: string
|
||||
meta: { [key: string]: string }
|
||||
id: string;
|
||||
timestamp: string;
|
||||
activity: string;
|
||||
activity_code: string;
|
||||
initiator_id: string;
|
||||
initiator_email: string;
|
||||
initiator_name: string;
|
||||
target_id: string;
|
||||
meta: { [key: string]: string };
|
||||
}
|
||||
@@ -74,13 +74,13 @@ export function* setDeleteGroup(action: ReturnType<typeof actions.setDeleteGrou
|
||||
|
||||
export function* deleteGroup(action: ReturnType<typeof actions.deleteGroup.request>): Generator {
|
||||
try {
|
||||
yield call(actions.setDeleteGroup,{
|
||||
yield put(actions.setDeleteGroup({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>)
|
||||
} as DeleteResponse<string | null>))
|
||||
|
||||
const effect = yield call(service.deleteGroup, action.payload);
|
||||
const response = effect as ApiResponse<any>;
|
||||
@@ -93,8 +93,8 @@ export function* deleteGroup(action: ReturnType<typeof actions.deleteGroup.reque
|
||||
data: response.body
|
||||
} as DeleteResponse<string | null>));
|
||||
|
||||
const rules = (yield select(state => state.rule.data)) as Group[]
|
||||
yield put(actions.getGroups.success(rules.filter((p:Group) => p.id !== action.payload.payload)))
|
||||
const groups = (yield select(state => state.group.data)) as Group[]
|
||||
yield put(actions.getGroups.success(groups.filter((p:Group) => p.id !== action.payload.payload)))
|
||||
} catch (err) {
|
||||
yield put(actions.deleteGroup.failure({
|
||||
loading: false,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { composeWithDevTools } from 'redux-devtools-extension';
|
||||
import { sagas as peerSagas } from './peer';
|
||||
import { sagas as setupKeySagas } from './setup-key';
|
||||
import { sagas as userSagas } from './user';
|
||||
import { sagas as policySagas } from './policy';
|
||||
import { sagas as ruleSagas } from './rule';
|
||||
import { sagas as groupSagas } from './group';
|
||||
import { sagas as routeSagas } from './route';
|
||||
@@ -28,6 +29,7 @@ sagaMiddleware.run(peerSagas);
|
||||
sagaMiddleware.run(setupKeySagas);
|
||||
sagaMiddleware.run(userSagas);
|
||||
sagaMiddleware.run(ruleSagas);
|
||||
sagaMiddleware.run(policySagas);
|
||||
sagaMiddleware.run(groupSagas);
|
||||
sagaMiddleware.run(routeSagas);
|
||||
sagaMiddleware.run(nameserverGroupSagas);
|
||||
@@ -36,4 +38,4 @@ sagaMiddleware.run(dnsSettingsSagas);
|
||||
sagaMiddleware.run(accountSagas);
|
||||
sagaMiddleware.run(personalAccessTokenSagas);
|
||||
|
||||
export { apiClient, rootReducer, store };
|
||||
export { apiClient, rootReducer, store };
|
||||
|
||||
@@ -28,6 +28,7 @@ const actions = {
|
||||
|
||||
setNameServerGroup: createAction('SET_NameServerGroup')<NameServerGroup>(),
|
||||
setSetupNewNameServerGroupVisible: createAction('SET_SETUP_NEW_NameServerGroup_VISIBLE')<boolean>(),
|
||||
setSetupEditNameServerGroupVisible: createAction('SET_SETUP_EDIT_NameServerGroup_VISIBLE')<boolean>(),
|
||||
setSetupNewNameServerGroupHA: createAction('SET_SETUP_NEW_NameServerGroup_HA')<boolean>()
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ type StateType = Readonly<{
|
||||
deleteNameServerGroup: DeleteResponse<string | null>;
|
||||
savedNameServerGroup: CreateResponse<NameServerGroup | null>;
|
||||
setupNewNameServerGroupVisible: boolean;
|
||||
setupEditNameServerGroupVisible: boolean;
|
||||
setupNewNameServerGroupHA: boolean
|
||||
}>;
|
||||
|
||||
@@ -27,17 +28,18 @@ const initialState: StateType = {
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
data: null,
|
||||
},
|
||||
savedNameServerGroup: <CreateResponse<NameServerGroup | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
data: null,
|
||||
},
|
||||
setupNewNameServerGroupVisible: false,
|
||||
setupNewNameServerGroupHA: false
|
||||
setupEditNameServerGroupVisible: false,
|
||||
setupNewNameServerGroupHA: false,
|
||||
};
|
||||
|
||||
const data = createReducer<NameServerGroup[], ActionTypes>(initialState.data as NameServerGroup[])
|
||||
@@ -79,6 +81,9 @@ const savedNameServerGroup = createReducer<CreateResponse<NameServerGroup | null
|
||||
const setupNewNameServerGroupVisible = createReducer<boolean, ActionTypes>(initialState.setupNewNameServerGroupVisible)
|
||||
.handleAction(actions.setSetupNewNameServerGroupVisible, (store, action) => action.payload)
|
||||
|
||||
const setupEditNameServerGroupVisible = createReducer<boolean, ActionTypes>(initialState.setupEditNameServerGroupVisible)
|
||||
.handleAction(actions.setSetupEditNameServerGroupVisible, (store, action) => action.payload)
|
||||
|
||||
const setupNewNameServerGroupHA = createReducer<boolean, ActionTypes>(initialState.setupNewNameServerGroupHA)
|
||||
.handleAction(actions.setSetupNewNameServerGroupHA, (store, action) => action.payload)
|
||||
|
||||
@@ -91,5 +96,6 @@ export default combineReducers({
|
||||
deletedNameServerGroup,
|
||||
savedNameServerGroup,
|
||||
setupNewNameServerGroupVisible,
|
||||
setupNewNameServerGroupHA
|
||||
setupEditNameServerGroupVisible,
|
||||
setupNewNameServerGroupHA,
|
||||
});
|
||||
|
||||
@@ -118,13 +118,13 @@ export function* setDeleteNameServerGroup(action: ReturnType<typeof actions.set
|
||||
|
||||
export function* deleteNameServerGroup(action: ReturnType<typeof actions.deleteNameServerGroup.request>): Generator {
|
||||
try {
|
||||
yield call(actions.setDeletedNameServerGroup,{
|
||||
yield put(actions.setDeletedNameServerGroup({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>)
|
||||
}))
|
||||
|
||||
const effect = yield call(service.deletedNameServerGroup, action.payload);
|
||||
const response = effect as ApiResponse<any>;
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
export interface NameServerGroup {
|
||||
id?: string
|
||||
name: string
|
||||
description: string
|
||||
primary: boolean
|
||||
domains: string[]
|
||||
nameservers: NameServer[]
|
||||
groups: string[]
|
||||
enabled: boolean
|
||||
id?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
primary: boolean;
|
||||
domains: string[];
|
||||
nameservers: NameServer[];
|
||||
groups: string[];
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface NameServer {
|
||||
ip: string
|
||||
ns_type: string
|
||||
port: number
|
||||
ip: string;
|
||||
ns_type: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface NameServerGroupToSave extends NameServerGroup
|
||||
{
|
||||
groupsToCreate: string[]
|
||||
}
|
||||
export interface NameServerGroupToSave extends NameServerGroup {
|
||||
groupsToCreate?: string[];
|
||||
}
|
||||
|
||||
@@ -79,11 +79,14 @@ const deletedPeer = createReducer<DeleteResponse<string | null>, ActionTypes>(in
|
||||
const updateGroupsVisible = createReducer<boolean, ActionTypes>(initialState.setUpdateGroupsVisible)
|
||||
.handleAction(actions.setUpdateGroupsVisible, (store, action) => action.payload)
|
||||
|
||||
const savedGroups = createReducer<ChangeResponse<Group[] | null>, ActionTypes>(initialState.savedGroups)
|
||||
.handleAction(actions.saveGroups.request, () => initialState.savedGroups)
|
||||
.handleAction(actions.saveGroups.success, (store, action) => action.payload)
|
||||
.handleAction(actions.saveGroups.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.resetSavedGroups, () => initialState.savedGroups)
|
||||
const savedGroups = createReducer<ChangeResponse<Group[] | null>, ActionTypes>(
|
||||
initialState.savedGroups
|
||||
)
|
||||
.handleAction(actions.saveGroups.request, () => initialState.savedGroups)
|
||||
.handleAction(actions.saveGroups.success, (store, action) => action.payload)
|
||||
.handleAction(actions.saveGroups.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setSavedGroups, (store, action) => action.payload)
|
||||
.handleAction(actions.resetSavedGroups, () => initialState.savedGroups);
|
||||
|
||||
const updatedPeer = createReducer<CreateResponse<Peer | null>, ActionTypes>(initialState.updatedPeer)
|
||||
.handleAction(actions.updatePeer.request, () => initialState.updatedPeer)
|
||||
|
||||
@@ -1,198 +1,268 @@
|
||||
import {all, call, spawn, put, select, takeLatest} from 'redux-saga/effects';
|
||||
import { all, call, spawn, put, select, takeLatest } from "redux-saga/effects";
|
||||
import {
|
||||
ApiError,
|
||||
ApiResponse,
|
||||
ChangeResponse,
|
||||
CreateResponse,
|
||||
DeleteResponse
|
||||
} from '../../services/api-client/types';
|
||||
import { Peer } from './types'
|
||||
import service from './service';
|
||||
import actions from './actions';
|
||||
import {Group, GroupPeer} from "../group/types";
|
||||
DeleteResponse,
|
||||
} from "../../services/api-client/types";
|
||||
import { Peer } from "./types";
|
||||
import service from "./service";
|
||||
import actions from "./actions";
|
||||
import { Group, GroupPeer } from "../group/types";
|
||||
import serviceGroup from "../group/service";
|
||||
import {actions as groupActions} from "../group";
|
||||
|
||||
|
||||
export function* getPeers(action: ReturnType<typeof actions.getPeers.request>): Generator {
|
||||
import { actions as groupActions } from "../group";
|
||||
import userService from "../user/service";
|
||||
export function* getPeers(
|
||||
action: ReturnType<typeof actions.getPeers.request>
|
||||
): Generator {
|
||||
try {
|
||||
yield put(
|
||||
actions.setDeletePeer({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null,
|
||||
} as DeleteResponse<string | null>)
|
||||
);
|
||||
|
||||
yield put(actions.setDeletePeer({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>))
|
||||
|
||||
const users: any = yield call(userService.getUsers, action.payload);
|
||||
let currentUser = users.body.find((user: any) => user.is_current);
|
||||
const effect = yield call(service.getPeers, action.payload);
|
||||
const response = effect as ApiResponse<Peer[]>;
|
||||
yield put(actions.getPeers.success(response.body));
|
||||
let peersBody: any;
|
||||
if (currentUser.role === "user") {
|
||||
peersBody = response.body.filter((pe) => {
|
||||
return pe.user_id === currentUser.id;
|
||||
});
|
||||
} else {
|
||||
peersBody = response.body;
|
||||
}
|
||||
yield put(actions.getPeers.success(peersBody));
|
||||
} catch (err) {
|
||||
yield put(actions.getPeers.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export function* setDeletePeer(action: ReturnType<typeof actions.setDeletePeer>): Generator {
|
||||
yield put(actions.setDeletePeer(action.payload))
|
||||
export function* setDeletePeer(
|
||||
action: ReturnType<typeof actions.setDeletePeer>
|
||||
): Generator {
|
||||
yield put(actions.setDeletePeer(action.payload));
|
||||
}
|
||||
|
||||
export function* deletePeer(action: ReturnType<typeof actions.deletedPeer.request>): Generator {
|
||||
export function* deletePeer(
|
||||
action: ReturnType<typeof actions.deletedPeer.request>
|
||||
): Generator {
|
||||
try {
|
||||
yield call(actions.setDeletePeer,{
|
||||
yield call(actions.setDeletePeer, {
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>)
|
||||
data: null,
|
||||
} as DeleteResponse<string | null>);
|
||||
|
||||
const effect = yield call(service.deletedPeer, action.payload);
|
||||
const response = effect as ApiResponse<any>;
|
||||
|
||||
yield put(actions.deletedPeer.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as DeleteResponse<string | null>));
|
||||
yield put(
|
||||
actions.deletedPeer.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body,
|
||||
} as DeleteResponse<string | null>)
|
||||
);
|
||||
|
||||
const peers = (yield select(state => state.peer.data)) as Peer[]
|
||||
yield put(actions.getPeers.success(peers.filter((p:Peer) => p.id !== action.payload.payload)))
|
||||
const peers = (yield select((state) => state.peer.data)) as Peer[];
|
||||
yield put(
|
||||
actions.getPeers.success(
|
||||
peers.filter((p: Peer) => p.id !== action.payload.payload)
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
yield put(actions.deletedPeer.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>));
|
||||
yield put(
|
||||
actions.deletedPeer.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: err as ApiError,
|
||||
data: null,
|
||||
} as DeleteResponse<string | null>)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function* saveGroups(action: ReturnType<typeof actions.saveGroups.request>): Generator {
|
||||
export function* saveGroups(
|
||||
action: ReturnType<typeof actions.saveGroups.request>
|
||||
): Generator {
|
||||
try {
|
||||
yield put(actions.setSavedGroups({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
}))
|
||||
yield put(
|
||||
actions.setSavedGroups({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null,
|
||||
})
|
||||
);
|
||||
|
||||
const currentGroups = [...(yield select(state => state.group.data)) as Group[]]
|
||||
const currentGroups = [
|
||||
...((yield select((state) => state.group.data)) as Group[]),
|
||||
];
|
||||
|
||||
const peerGroupsToSave = action.payload.payload
|
||||
const peerGroupsToSave = action.payload.payload;
|
||||
|
||||
let groupsToSave = [] as Group[]
|
||||
let groupsNoId = [] as Group[]
|
||||
let groupsToSave = [] as Group[];
|
||||
let groupsNoId = [] as Group[];
|
||||
|
||||
groupsToSave = groupsToSave.concat(
|
||||
currentGroups
|
||||
.filter(g => peerGroupsToSave.groupsToRemove.includes(g.id || ''))
|
||||
.map(g => ({
|
||||
id: g.id,
|
||||
name: g.name,
|
||||
peers: (g.peers as GroupPeer[]).filter(p => p.id !== peerGroupsToSave.ID).map(p => p.id) as string[]
|
||||
}))
|
||||
)
|
||||
currentGroups
|
||||
.filter((g) => peerGroupsToSave.groupsToRemove.includes(g.id || ""))
|
||||
.map((g) => ({
|
||||
id: g.id,
|
||||
name: g.name,
|
||||
peers: (g.peers as GroupPeer[])
|
||||
.filter((p) => p.id !== peerGroupsToSave.ID)
|
||||
.map((p) => p.id) as string[],
|
||||
}))
|
||||
);
|
||||
|
||||
groupsToSave = groupsToSave.concat(
|
||||
currentGroups
|
||||
.filter(g => peerGroupsToSave.groupsToAdd.includes(g.id || ''))
|
||||
.map(g => ({
|
||||
id: g.id,
|
||||
name: g.name,
|
||||
Peers: g.peers ? [...(g.peers as GroupPeer[]).map((p:GroupPeer) => p.id), peerGroupsToSave.ID] : [peerGroupsToSave.ID]
|
||||
}))
|
||||
)
|
||||
currentGroups
|
||||
.filter((g) => peerGroupsToSave.groupsToAdd.includes(g.id || ""))
|
||||
.map((g) => ({
|
||||
id: g.id,
|
||||
name: g.name,
|
||||
Peers: g.peers
|
||||
? [
|
||||
...(g.peers as GroupPeer[]).map((p: GroupPeer) => p.id),
|
||||
peerGroupsToSave.ID,
|
||||
]
|
||||
: [peerGroupsToSave.ID],
|
||||
}))
|
||||
);
|
||||
|
||||
groupsNoId = peerGroupsToSave.groupsNoId.map(g => ({
|
||||
groupsNoId = peerGroupsToSave.groupsNoId.map((g) => ({
|
||||
name: g,
|
||||
peers: [peerGroupsToSave.ID]
|
||||
}))
|
||||
peers: [peerGroupsToSave.ID],
|
||||
}));
|
||||
|
||||
if (!groupsNoId.length && !groupsToSave.length) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const responsesGroup = yield all(groupsToSave.map(g => call(serviceGroup.editGroup, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: g
|
||||
})
|
||||
))
|
||||
const responsesGroup = yield all(
|
||||
groupsToSave.map((g) =>
|
||||
call(serviceGroup.editGroup, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: g,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const responsesGroupNoId = yield all(groupsNoId.map(g => call(serviceGroup.createGroup, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: g
|
||||
})
|
||||
))
|
||||
|
||||
yield put(actions.saveGroups.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: [...(responsesGroup as ApiResponse<Group>[]).map(r => r.body), ...(responsesGroupNoId as ApiResponse<Group>[]).map(r => r.body)]
|
||||
} as CreateResponse<Group[] | null>))
|
||||
const responsesGroupNoId = yield all(
|
||||
groupsNoId.map((g) =>
|
||||
call(serviceGroup.createGroup, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: g,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
yield put(groupActions.getGroups.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
|
||||
yield put(actions.getPeers.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
|
||||
yield put(
|
||||
actions.saveGroups.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: [
|
||||
...(responsesGroup as ApiResponse<Group>[]).map((r) => r.body),
|
||||
...(responsesGroupNoId as ApiResponse<Group>[]).map((r) => r.body),
|
||||
],
|
||||
} as CreateResponse<Group[] | null>)
|
||||
);
|
||||
|
||||
yield put(
|
||||
groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
yield put(
|
||||
actions.getPeers.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
yield put(actions.saveGroups.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: true,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as ChangeResponse<Group[] | null>));
|
||||
console.log(err);
|
||||
yield put(
|
||||
actions.saveGroups.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: true,
|
||||
error: err as ApiError,
|
||||
data: null,
|
||||
} as ChangeResponse<Group[] | null>)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function* updatePeer(action: ReturnType<typeof actions.updatePeer.request>): Generator {
|
||||
export function* updatePeer(
|
||||
action: ReturnType<typeof actions.updatePeer.request>
|
||||
): Generator {
|
||||
try {
|
||||
yield put(actions.setUpdatedPeer({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
}))
|
||||
yield put(
|
||||
actions.setUpdatedPeer({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null,
|
||||
})
|
||||
);
|
||||
|
||||
const peer = action.payload.payload
|
||||
const peerId = peer.id
|
||||
const peer = action.payload.payload;
|
||||
const peerId = peer.id;
|
||||
|
||||
const payloadToSave = {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: peer
|
||||
}
|
||||
payload: peer,
|
||||
};
|
||||
|
||||
const effect = yield call(service.updatePeer, payloadToSave)
|
||||
const effect = yield call(service.updatePeer, payloadToSave);
|
||||
const response = effect as ApiResponse<Peer>;
|
||||
|
||||
yield put(actions.updatePeer.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as ChangeResponse<Peer | null>));
|
||||
|
||||
const peers = (yield select(state => state.peer.data)) as Peer[]
|
||||
yield put(actions.getPeers.success(peers.filter((p:Peer) => p.id !== peerId).concat(response.body)))
|
||||
yield put(
|
||||
actions.updatePeer.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body,
|
||||
} as ChangeResponse<Peer | null>)
|
||||
);
|
||||
|
||||
const peers = (yield select((state) => state.peer.data)) as Peer[];
|
||||
yield put(
|
||||
actions.getPeers.success(
|
||||
peers.filter((p: Peer) => p.id !== peerId).concat(response.body)
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
yield put(actions.updatePeer.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: true,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as ChangeResponse<Peer | null>));
|
||||
console.log(err);
|
||||
yield put(
|
||||
actions.updatePeer.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: true,
|
||||
error: err as ApiError,
|
||||
data: null,
|
||||
} as ChangeResponse<Peer | null>)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +271,6 @@ export default function* sagas(): Generator {
|
||||
takeLatest(actions.getPeers.request, getPeers),
|
||||
takeLatest(actions.deletedPeer.request, deletePeer),
|
||||
takeLatest(actions.saveGroups.request, saveGroups),
|
||||
takeLatest(actions.updatePeer.request, updatePeer)
|
||||
takeLatest(actions.updatePeer.request, updatePeer),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
35
src/store/policy/actions.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
|
||||
import { Policy, PolicyToSave } from './types';
|
||||
import { ApiError, CreateResponse, DeleteResponse, RequestPayload } from '../../services/api-client/types';
|
||||
|
||||
const actions = {
|
||||
getPolicies: createAsyncAction(
|
||||
'GET_POLICIES_REQUEST',
|
||||
'GET_POLICIES_SUCCESS',
|
||||
'GET_POLICIES_FAILURE',
|
||||
)<RequestPayload<null>, Policy[], ApiError>(),
|
||||
|
||||
savePolicy: createAsyncAction(
|
||||
'SAVE_POLICY_REQUEST',
|
||||
'SAVE_POLICY_SUCCESS',
|
||||
'SAVE_POLICY_FAILURE',
|
||||
)<RequestPayload<PolicyToSave>, CreateResponse<Policy | null>, CreateResponse<Policy | null>>(),
|
||||
setSavedPolicy: createAction('SET_CREATE_POLICY')<CreateResponse<Policy | null>>(),
|
||||
resetSavedPolicy: createAction('RESET_CREATE_POLICY')<null>(),
|
||||
|
||||
deletePolicy: createAsyncAction(
|
||||
'DELETE_POLICY_REQUEST',
|
||||
'DELETE_POLICY_SUCCESS',
|
||||
'DELETE_POLICY_FAILURE'
|
||||
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
|
||||
setDeletedPolicy: createAction('SET_DELETED_POLICY')<DeleteResponse<string | null>>(),
|
||||
resetDeletedPolicy: createAction('RESET_DELETED_POLICY')<null>(),
|
||||
removePolicy: createAction('REMOVE_POLICY')<string>(),
|
||||
|
||||
setPolicy: createAction('SET_POLICY')<Policy>(),
|
||||
setSetupNewPolicyVisible: createAction('SET_SETUP_NEW_POLICY_VISIBLE')<boolean>(),
|
||||
setSetupEditPolicyVisible: createAction('SET_SETUP_EDIT_POLICY_VISIBLE')<boolean>()
|
||||
};
|
||||
|
||||
export type ActionTypes = ActionType<typeof actions>;
|
||||
export default actions;
|
||||
7
src/store/policy/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import actions, { ActionTypes as _actionTypes } from './actions';
|
||||
import reducer from './reducer';
|
||||
import sagas from './sagas';
|
||||
|
||||
export type ActionTypes = _actionTypes;
|
||||
|
||||
export { actions, reducer, sagas };
|
||||
95
src/store/policy/reducer.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { createReducer } from 'typesafe-actions';
|
||||
import { combineReducers } from 'redux';
|
||||
import { Policy } from './types';
|
||||
import actions, { ActionTypes } from './actions';
|
||||
import { ApiError, DeleteResponse, CreateResponse } from "../../services/api-client/types";
|
||||
|
||||
type StateType = Readonly<{
|
||||
data: Policy[] | null;
|
||||
policy: Policy | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
saving: boolean;
|
||||
deletePolicy: DeleteResponse<string | null>;
|
||||
savedPolicy: CreateResponse<Policy | null>;
|
||||
setupNewPolicyVisible: boolean;
|
||||
setupEditPolicyVisible: boolean;
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
data: [],
|
||||
policy: null,
|
||||
loading: false,
|
||||
failed: null,
|
||||
saving: false,
|
||||
deletePolicy: <DeleteResponse<string | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null,
|
||||
},
|
||||
savedPolicy: <CreateResponse<Policy | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null,
|
||||
},
|
||||
setupNewPolicyVisible: false,
|
||||
setupEditPolicyVisible: false,
|
||||
};
|
||||
|
||||
const data = createReducer<Policy[], ActionTypes>(initialState.data as Policy[])
|
||||
.handleAction(actions.getPolicies.success, (_, action) => action.payload)
|
||||
.handleAction(actions.getPolicies.failure, () => []);
|
||||
|
||||
const policy = createReducer<Policy, ActionTypes>(initialState.policy as Policy)
|
||||
.handleAction(actions.setPolicy, (store, action) => action.payload);
|
||||
|
||||
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
|
||||
.handleAction(actions.getPolicies.request, () => true)
|
||||
.handleAction(actions.getPolicies.success, () => false)
|
||||
.handleAction(actions.getPolicies.failure, () => false);
|
||||
|
||||
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
|
||||
.handleAction(actions.getPolicies.request, () => null)
|
||||
.handleAction(actions.getPolicies.success, () => null)
|
||||
.handleAction(actions.getPolicies.failure, (store, action) => action.payload);
|
||||
|
||||
const saving = createReducer<boolean, ActionTypes>(initialState.saving)
|
||||
.handleAction(actions.getPolicies.request, () => true)
|
||||
.handleAction(actions.getPolicies.success, () => false)
|
||||
.handleAction(actions.getPolicies.failure, () => false);
|
||||
|
||||
const deletedPolicy = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deletePolicy)
|
||||
.handleAction(actions.deletePolicy.request, () => initialState.deletePolicy)
|
||||
.handleAction(actions.deletePolicy.success, (store, action) => action.payload)
|
||||
.handleAction(actions.deletePolicy.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setDeletedPolicy, (store, action) => action.payload)
|
||||
.handleAction(actions.resetDeletedPolicy, () => initialState.deletePolicy)
|
||||
|
||||
const savedPolicy = createReducer<CreateResponse<Policy | null>, ActionTypes>(initialState.savedPolicy)
|
||||
.handleAction(actions.savePolicy.request, () => initialState.savedPolicy)
|
||||
.handleAction(actions.savePolicy.success, (store, action) => action.payload)
|
||||
.handleAction(actions.savePolicy.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setSavedPolicy, (store, action) => action.payload)
|
||||
.handleAction(actions.resetSavedPolicy, () => initialState.savedPolicy)
|
||||
|
||||
const setupNewPolicyVisible = createReducer<boolean, ActionTypes>(initialState.setupNewPolicyVisible)
|
||||
.handleAction(actions.setSetupNewPolicyVisible, (store, action) => action.payload)
|
||||
|
||||
const setupEditPolicyVisible = createReducer<boolean, ActionTypes>(initialState.setupEditPolicyVisible)
|
||||
.handleAction(actions.setSetupEditPolicyVisible, (store, action) => action.payload)
|
||||
|
||||
export default combineReducers({
|
||||
data,
|
||||
policy,
|
||||
loading,
|
||||
failed,
|
||||
saving,
|
||||
deletedPolicy,
|
||||
savedPolicy,
|
||||
setupNewPolicyVisible,
|
||||
setupEditPolicyVisible,
|
||||
});
|
||||
225
src/store/policy/sagas.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { all, call, put, select, takeLatest } from "redux-saga/effects";
|
||||
import {
|
||||
ApiError,
|
||||
ApiResponse,
|
||||
CreateResponse,
|
||||
DeleteResponse,
|
||||
} from "../../services/api-client/types";
|
||||
import { Policy, PolicyRule } from "./types";
|
||||
import service from "./service";
|
||||
import serviceGroup from "../group/service";
|
||||
import actions from "./actions";
|
||||
import { actions as groupActions } from "../group";
|
||||
import { Group } from "../group/types";
|
||||
|
||||
export function* getPolicies(
|
||||
action: ReturnType<typeof actions.getPolicies.request>
|
||||
): Generator {
|
||||
try {
|
||||
yield put(
|
||||
actions.setDeletedPolicy({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null,
|
||||
} as DeleteResponse<string | null>)
|
||||
);
|
||||
|
||||
const effect = yield call(service.getPolicies, action.payload);
|
||||
const response = effect as ApiResponse<Policy[]>;
|
||||
|
||||
yield put(actions.getPolicies.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getPolicies.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export function* setCreatedPolicy(
|
||||
action: ReturnType<typeof actions.setSavedPolicy>
|
||||
): Generator {
|
||||
yield put(actions.setSavedPolicy(action.payload));
|
||||
}
|
||||
|
||||
function getNewGroupIds(dataString: string[], responses: Group[]): string[] {
|
||||
return responses
|
||||
.filter((r) => dataString.includes(r.name))
|
||||
.map((r) => r.id || "");
|
||||
}
|
||||
|
||||
export function* savePolicy(
|
||||
action: ReturnType<typeof actions.savePolicy.request>
|
||||
): Generator {
|
||||
try {
|
||||
yield put(
|
||||
actions.setSavedPolicy({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null,
|
||||
} as CreateResponse<Policy | null>)
|
||||
);
|
||||
|
||||
const policyToSave = action.payload.payload;
|
||||
const groupsToSave = policyToSave.groupsToSave
|
||||
? policyToSave.groupsToSave
|
||||
: [];
|
||||
const responsesGroup = yield all(
|
||||
groupsToSave.map((g) =>
|
||||
call(serviceGroup.createGroup, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: { name: g },
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const resGroups = (responsesGroup as ApiResponse<Policy>[])
|
||||
.filter((r) => r.statusCode === 200)
|
||||
.map((r) => r.body as Group);
|
||||
|
||||
const currentGroups = [
|
||||
...((yield select((state) => state.group.data)) as Policy[]),
|
||||
];
|
||||
const newGroups = [...currentGroups, ...resGroups];
|
||||
yield put(groupActions.getGroups.success(newGroups));
|
||||
|
||||
const newSources = getNewGroupIds(
|
||||
policyToSave.sourcesNoId ? policyToSave.sourcesNoId : [],
|
||||
resGroups
|
||||
);
|
||||
const newDestinations = getNewGroupIds(
|
||||
policyToSave.destinationsNoId ? policyToSave.destinationsNoId : [],
|
||||
resGroups
|
||||
);
|
||||
|
||||
const payloadToSave = {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: {
|
||||
name: policyToSave.name,
|
||||
description: policyToSave.description,
|
||||
enabled: policyToSave.enabled,
|
||||
query: policyToSave.query,
|
||||
} as Policy,
|
||||
};
|
||||
if (policyToSave.rules.length > 0) {
|
||||
payloadToSave.payload.rules = [];
|
||||
}
|
||||
policyToSave.rules.forEach((r) => {
|
||||
payloadToSave.payload.rules.push({
|
||||
name: r.name,
|
||||
description: r.description,
|
||||
enabled: r.enabled,
|
||||
sources: [...(r.sources as string[]), ...newSources],
|
||||
destinations: [...(r.destinations as string[]), ...newDestinations],
|
||||
bidirectional: r.bidirectional,
|
||||
protocol: r.protocol,
|
||||
ports: r.ports,
|
||||
action: r.action,
|
||||
} as PolicyRule);
|
||||
});
|
||||
|
||||
let effect;
|
||||
if (!policyToSave.id) {
|
||||
effect = yield call(service.createPolicy, payloadToSave);
|
||||
} else {
|
||||
payloadToSave.payload.id = policyToSave.id;
|
||||
effect = yield call(service.editPolicy, payloadToSave);
|
||||
}
|
||||
|
||||
const response = effect as ApiResponse<Policy>;
|
||||
|
||||
yield put(
|
||||
actions.savePolicy.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body,
|
||||
} as CreateResponse<Policy | null>)
|
||||
);
|
||||
|
||||
yield put(
|
||||
groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
yield put(
|
||||
actions.getPolicies.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
yield put(
|
||||
actions.savePolicy.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: true,
|
||||
error: err as ApiError,
|
||||
data: null,
|
||||
} as CreateResponse<Policy | null>)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function* setDeletePolicy(
|
||||
action: ReturnType<typeof actions.setDeletedPolicy>
|
||||
): Generator {
|
||||
yield put(actions.setDeletedPolicy(action.payload));
|
||||
}
|
||||
|
||||
export function* deletePolicy(
|
||||
action: ReturnType<typeof actions.deletePolicy.request>
|
||||
): Generator {
|
||||
try {
|
||||
yield put(
|
||||
actions.setDeletedPolicy({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null,
|
||||
})
|
||||
);
|
||||
|
||||
const effect = yield call(service.deletedPolicy, action.payload);
|
||||
const response = effect as ApiResponse<any>;
|
||||
|
||||
yield put(
|
||||
actions.deletePolicy.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body,
|
||||
} as DeleteResponse<string | null>)
|
||||
);
|
||||
|
||||
const policies = (yield select((state) => state.policy.data)) as Policy[];
|
||||
yield put(
|
||||
actions.getPolicies.success(
|
||||
policies.filter((p: Policy) => p.id !== action.payload.payload)
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
yield put(
|
||||
actions.deletePolicy.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.getPolicies.request, getPolicies),
|
||||
takeLatest(actions.savePolicy.request, savePolicy),
|
||||
takeLatest(actions.deletePolicy.request, deletePolicy),
|
||||
]);
|
||||
}
|
||||
32
src/store/policy/service.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ApiResponse, RequestPayload } from '../../services/api-client/types';
|
||||
import { apiClient } from '../../services/api-client';
|
||||
import { Policy } from './types';
|
||||
|
||||
export default {
|
||||
async getPolicies(payload: RequestPayload<null>): Promise<ApiResponse<Policy[]>> {
|
||||
return apiClient.get<Policy[]>(
|
||||
`/api/policies`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async deletedPolicy(payload: RequestPayload<string>): Promise<ApiResponse<any>> {
|
||||
return apiClient.delete<any>(
|
||||
`/api/policies/` + payload.payload,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async createPolicy(payload: RequestPayload<Policy>): Promise<ApiResponse<Policy>> {
|
||||
return apiClient.post<Policy>(
|
||||
`/api/policies`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async editPolicy(payload: RequestPayload<Policy>): Promise<ApiResponse<Policy>> {
|
||||
const id = payload.payload.id
|
||||
delete payload.payload.id
|
||||
return apiClient.put<Policy>(
|
||||
`/api/policies/${id}`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
};
|
||||
29
src/store/policy/types.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Group } from "../group/types";
|
||||
|
||||
export interface PolicyRule {
|
||||
id?: string
|
||||
name: string
|
||||
description: string
|
||||
enabled: boolean
|
||||
sources: Group[] | string[] | null
|
||||
destinations: Group[] | string[] | null
|
||||
bidirectional: boolean
|
||||
action: string
|
||||
protocol: string
|
||||
ports: string[]
|
||||
}
|
||||
|
||||
export interface Policy {
|
||||
id?: string
|
||||
name: string
|
||||
description: string
|
||||
enabled: boolean
|
||||
query: string
|
||||
rules: PolicyRule[]
|
||||
};
|
||||
|
||||
export interface PolicyToSave extends Policy {
|
||||
sourcesNoId?: string[],
|
||||
destinationsNoId?: string[],
|
||||
groupsToSave?: string[]
|
||||
};
|
||||
@@ -5,6 +5,7 @@ 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 policy } from './policy';
|
||||
import { reducer as route } from './route';
|
||||
import { reducer as nameserverGroup } from './nameservers';
|
||||
import { reducer as event } from './event';
|
||||
@@ -13,15 +14,16 @@ import { reducer as account } from './account';
|
||||
import { reducer as personalAccessToken } from './personal-access-token';
|
||||
|
||||
export default combineReducers({
|
||||
peer,
|
||||
setupKey,
|
||||
user,
|
||||
group,
|
||||
rule,
|
||||
route,
|
||||
nameserverGroup,
|
||||
event,
|
||||
dnsSettings,
|
||||
account,
|
||||
personalAccessToken
|
||||
peer,
|
||||
setupKey,
|
||||
user,
|
||||
group,
|
||||
rule,
|
||||
policy,
|
||||
route,
|
||||
nameserverGroup,
|
||||
event,
|
||||
dnsSettings,
|
||||
account,
|
||||
personalAccessToken
|
||||
});
|
||||
|
||||
@@ -21,13 +21,15 @@ const actions = {
|
||||
'DELETE_ROUTE_REQUEST',
|
||||
'DELETE_ROUTE_SUCCESS',
|
||||
'DELETE_ROUTE_FAILURE'
|
||||
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
|
||||
)<RequestPayload<any>, 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>(),
|
||||
setSetupEditRouteVisible: createAction('SET_SETUP_EDIT_ROUTE_VISIBLE')<boolean>(),
|
||||
setSetupEditRoutePeerVisible: createAction('SET_SETUP_EDIT_ROUTE_PEER_VISIBLE')<boolean>(),
|
||||
setSetupNewRouteHA: createAction('SET_SETUP_NEW_ROUTE_HA')<boolean>()
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@ type StateType = Readonly<{
|
||||
deleteRoute: DeleteResponse<string | null>;
|
||||
savedRoute: CreateResponse<Route | null>;
|
||||
setupNewRouteVisible: boolean;
|
||||
setupNewRouteHA: boolean
|
||||
setupNewRouteHA: boolean;
|
||||
setupEditRouteVisible: boolean;
|
||||
setEditRoutePeerVisible: boolean;
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
@@ -27,17 +29,19 @@ const initialState: StateType = {
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
data: null,
|
||||
},
|
||||
savedRoute: <CreateResponse<Route | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
data: null,
|
||||
},
|
||||
setupNewRouteVisible: false,
|
||||
setupNewRouteHA: false
|
||||
setupNewRouteHA: false,
|
||||
setupEditRouteVisible: false,
|
||||
setEditRoutePeerVisible: false,
|
||||
};
|
||||
|
||||
const data = createReducer<Route[], ActionTypes>(initialState.data as Route[])
|
||||
@@ -79,6 +83,21 @@ const savedRoute = createReducer<CreateResponse<Route | null>, ActionTypes>(init
|
||||
const setupNewRouteVisible = createReducer<boolean, ActionTypes>(initialState.setupNewRouteVisible)
|
||||
.handleAction(actions.setSetupNewRouteVisible, (store, action) => action.payload)
|
||||
|
||||
const setupEditRouteVisible = createReducer<boolean, ActionTypes>(
|
||||
initialState.setupEditRouteVisible
|
||||
).handleAction(
|
||||
actions.setSetupEditRouteVisible,
|
||||
(store, action) => action.payload
|
||||
);
|
||||
|
||||
|
||||
const setEditRoutePeerVisible = createReducer<boolean, ActionTypes>(
|
||||
initialState.setEditRoutePeerVisible
|
||||
).handleAction(
|
||||
actions.setSetupEditRoutePeerVisible,
|
||||
(store, action) => action.payload
|
||||
);
|
||||
|
||||
const setupNewRouteHA = createReducer<boolean, ActionTypes>(initialState.setupNewRouteHA)
|
||||
.handleAction(actions.setSetupNewRouteHA, (store, action) => action.payload)
|
||||
|
||||
@@ -91,5 +110,7 @@ export default combineReducers({
|
||||
deletedRoute,
|
||||
savedRoute,
|
||||
setupNewRouteVisible,
|
||||
setupNewRouteHA
|
||||
setupNewRouteHA,
|
||||
setupEditRouteVisible,
|
||||
setEditRoutePeerVisible,
|
||||
});
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
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';
|
||||
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";
|
||||
import serviceGroup from "../group/service";
|
||||
import {Group} from "../group/types";
|
||||
import {actions as groupActions} from "../group";
|
||||
import { Group } from "../group/types";
|
||||
import { actions as groupActions } from "../group";
|
||||
|
||||
export function* getRoutes(action: ReturnType<typeof actions.getRoutes.request>): Generator {
|
||||
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>))
|
||||
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[]>;
|
||||
@@ -27,36 +35,48 @@ export function* getRoutes(action: ReturnType<typeof actions.getRoutes.request>)
|
||||
}
|
||||
}
|
||||
|
||||
export function* setCreatedRoute(action: ReturnType<typeof actions.setSavedRoute>): Generator {
|
||||
yield put(actions.setSavedRoute(action.payload))
|
||||
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 {
|
||||
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>))
|
||||
yield put(
|
||||
actions.setSavedRoute({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null,
|
||||
} as CreateResponse<Route | null>)
|
||||
);
|
||||
|
||||
const routeToSave = action.payload.payload
|
||||
const routeToSave = action.payload.payload;
|
||||
|
||||
let groupsToCreate = routeToSave.groupsToCreate
|
||||
let groupsToCreate = routeToSave.groupsToCreate;
|
||||
if (!groupsToCreate) {
|
||||
groupsToCreate = []
|
||||
groupsToCreate = [];
|
||||
}
|
||||
|
||||
// first, create groups that were newly added by user
|
||||
const responsesGroup = yield all(groupsToCreate.map(g => call(serviceGroup.createGroup, {
|
||||
const responsesGroup = yield all(
|
||||
groupsToCreate.map((g) =>
|
||||
call(serviceGroup.createGroup, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: {name: g}
|
||||
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 = [...routeToSave.groups, ...resGroups]
|
||||
const resGroups = (responsesGroup as ApiResponse<Group>[])
|
||||
.filter((r) => r.statusCode === 200)
|
||||
.map((g) => g.body as Group)
|
||||
.map((g) => g.id);
|
||||
const newGroups = [...routeToSave.groups, ...resGroups];
|
||||
|
||||
const payloadToSave = {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
@@ -69,86 +89,144 @@ export function* saveRoute(action: ReturnType<typeof actions.saveRoute.request>)
|
||||
network: routeToSave.network,
|
||||
network_id: routeToSave.network_id,
|
||||
peer: routeToSave.peer,
|
||||
groups: newGroups
|
||||
} as Route
|
||||
}
|
||||
groups: newGroups,
|
||||
} as Route,
|
||||
};
|
||||
|
||||
let effect
|
||||
let effect;
|
||||
if (!routeToSave.id) {
|
||||
effect = yield call(service.createRoute, payloadToSave);
|
||||
effect = yield call(service.createRoute, payloadToSave);
|
||||
} else {
|
||||
payloadToSave.payload.id = routeToSave.id
|
||||
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.saveRoute.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body,
|
||||
} as CreateResponse<Route | null>)
|
||||
);
|
||||
|
||||
yield put(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
|
||||
yield put(actions.getRoutes.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
|
||||
yield put(
|
||||
groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
|
||||
yield put(
|
||||
actions.getRoutes.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
yield put(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
yield put(
|
||||
groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
|
||||
yield put(actions.saveRoute.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: true,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as CreateResponse<Route | null>));
|
||||
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* setDeleteRoute(
|
||||
action: ReturnType<typeof actions.setDeletedRoute>
|
||||
): Generator {
|
||||
yield put(actions.setDeletedRoute(action.payload));
|
||||
}
|
||||
|
||||
export function* deleteRoute(action: ReturnType<typeof actions.deleteRoute.request>): Generator {
|
||||
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>)
|
||||
yield put(
|
||||
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>;
|
||||
const payloadType = typeof action.payload.payload;
|
||||
|
||||
yield put(actions.deleteRoute.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as DeleteResponse<string | null>));
|
||||
if (payloadType === "string") {
|
||||
const effect = yield call(service.deletedRoute, action.payload);
|
||||
const response = effect as ApiResponse<any>;
|
||||
|
||||
const routes = (yield select(state => state.route.data)) as Route[]
|
||||
yield put(actions.getRoutes.success(routes.filter((p:Route) => p.id !== action.payload.payload)))
|
||||
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)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const effect = yield all(
|
||||
action.payload.payload.map((g: string) =>
|
||||
call(service.deletedRoute, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: g,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const response = effect as Array<ApiResponse<any>>;
|
||||
yield put(
|
||||
actions.deleteRoute.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response[0].body,
|
||||
} as DeleteResponse<string | null>)
|
||||
);
|
||||
|
||||
const routes = (yield select((state) => state.route.data)) as Route[];
|
||||
|
||||
yield put(
|
||||
actions.getRoutes.success(
|
||||
routes.filter((p: Route) => !action.payload.payload.includes(p.id))
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
yield put(actions.deleteRoute.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>));
|
||||
yield put(
|
||||
actions.deleteRoute.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: err as ApiError,
|
||||
data: null,
|
||||
} as DeleteResponse<string | null>)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +234,6 @@ export default function* sagas(): Generator {
|
||||
yield all([
|
||||
takeLatest(actions.getRoutes.request, getRoutes),
|
||||
takeLatest(actions.saveRoute.request, saveRoute),
|
||||
takeLatest(actions.deleteRoute.request, deleteRoute)
|
||||
takeLatest(actions.deleteRoute.request, deleteRoute),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
export interface Route {
|
||||
id?: string
|
||||
id?: string | null
|
||||
description: string
|
||||
enabled: boolean
|
||||
peer: string
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
|
||||
import { ApiResponse, RequestPayload } from '../../services/api-client/types';
|
||||
import { apiClient } from '../../services/api-client';
|
||||
import { Rule } from './types';
|
||||
import {SetupKey} from "../setup-key/types";
|
||||
|
||||
export default {
|
||||
async getRules(payload:RequestPayload<null>): Promise<ApiResponse<Rule[]>> {
|
||||
return apiClient.get<Rule[]>(
|
||||
`/api/rules`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async deletedRule(payload:RequestPayload<string>): Promise<ApiResponse<any>> {
|
||||
return apiClient.delete<any>(
|
||||
`/api/rules/` + payload.payload,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async createRule(payload:RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
|
||||
return apiClient.post<Rule>(
|
||||
`/api/rules`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async editRule(payload:RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
|
||||
const id = payload.payload.id
|
||||
delete payload.payload.id
|
||||
return apiClient.put<Rule>(
|
||||
`/api/rules/${id}`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async getRules(payload: RequestPayload<null>): Promise<ApiResponse<Rule[]>> {
|
||||
return apiClient.get<Rule[]>(
|
||||
`/api/rules`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async deletedRule(payload: RequestPayload<string>): Promise<ApiResponse<any>> {
|
||||
return apiClient.delete<any>(
|
||||
`/api/rules/` + payload.payload,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async createRule(payload: RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
|
||||
return apiClient.post<Rule>(
|
||||
`/api/rules`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async editRule(payload: RequestPayload<Rule>): Promise<ApiResponse<Rule>> {
|
||||
const id = payload.payload.id
|
||||
delete payload.payload.id
|
||||
return apiClient.put<Rule>(
|
||||
`/api/rules/${id}`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Group} from "../group/types";
|
||||
import { Group } from "../group/types";
|
||||
|
||||
export interface Rule {
|
||||
id?: string
|
||||
@@ -7,6 +7,8 @@ export interface Rule {
|
||||
sources: Group[] | string[] | null
|
||||
destinations: Group[] | string[] | null
|
||||
flow: string
|
||||
protocol: string
|
||||
ports: string[]
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
@@ -14,4 +16,4 @@ export interface RuleToSave extends Rule {
|
||||
sourcesNoId: string[],
|
||||
destinationsNoId: string[],
|
||||
groupsToSave: string[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ const actions = {
|
||||
|
||||
removeSetupKey: createAction('REMOVE_SETUP_KEY')<string>(),
|
||||
setSetupKey: createAction('SET_SETUP_KEY')<SetupKey>(),
|
||||
setSetupNewKeyVisible: createAction('SET_SETUP_NEW_KEY_VISIBLE')<boolean>()
|
||||
setSetupNewKeyVisible: createAction('SET_SETUP_NEW_KEY_VISIBLE')<boolean>(),
|
||||
setSetupEditKeyVisible: createAction('SET_SETUP_EDIT_KEY_VISIBLE')<boolean>()
|
||||
};
|
||||
|
||||
export type ActionTypes = ActionType<typeof actions>;
|
||||
|
||||
@@ -5,15 +5,16 @@ import actions, { ActionTypes } from './actions';
|
||||
import {ApiError, DeleteResponse, CreateResponse, ChangeResponse} from "../../services/api-client/types";
|
||||
|
||||
type StateType = Readonly<{
|
||||
data: SetupKey[] | null;
|
||||
setupKey: SetupKey | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
saving: boolean;
|
||||
deletedSetupKey: DeleteResponse<string | null>;
|
||||
revokedSetupKey: ChangeResponse<SetupKey | null>;
|
||||
savedSetupKey: CreateResponse<SetupKey | null>;
|
||||
setupNewKeyVisible: boolean
|
||||
data: SetupKey[] | null;
|
||||
setupKey: SetupKey | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
saving: boolean;
|
||||
deletedSetupKey: DeleteResponse<string | null>;
|
||||
revokedSetupKey: ChangeResponse<SetupKey | null>;
|
||||
savedSetupKey: CreateResponse<SetupKey | null>;
|
||||
setupNewKeyVisible: boolean;
|
||||
setupEditKeyVisible: boolean;
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
@@ -43,7 +44,8 @@ const initialState: StateType = {
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
setupNewKeyVisible: false
|
||||
setupNewKeyVisible: false,
|
||||
setupEditKeyVisible: false
|
||||
};
|
||||
|
||||
const data = createReducer<SetupKey[], ActionTypes>(initialState.data as SetupKey[])
|
||||
@@ -85,13 +87,17 @@ const savedSetupKey = createReducer<CreateResponse<SetupKey | null>, ActionTypes
|
||||
const setupNewKeyVisible = createReducer<boolean, ActionTypes>(initialState.setupNewKeyVisible)
|
||||
.handleAction(actions.setSetupNewKeyVisible, (store, action) => action.payload)
|
||||
|
||||
const setupEditKeyVisible = createReducer<boolean, ActionTypes>(initialState.setupEditKeyVisible)
|
||||
.handleAction(actions.setSetupEditKeyVisible, (store, action) => action.payload)
|
||||
|
||||
export default combineReducers({
|
||||
data,
|
||||
setupKey,
|
||||
loading,
|
||||
failed,
|
||||
saving,
|
||||
deletedSetupKey,
|
||||
savedSetupKey: savedSetupKey,
|
||||
setupNewKeyVisible
|
||||
data,
|
||||
setupKey,
|
||||
loading,
|
||||
failed,
|
||||
saving,
|
||||
deletedSetupKey,
|
||||
savedSetupKey: savedSetupKey,
|
||||
setupNewKeyVisible,
|
||||
setupEditKeyVisible,
|
||||
});
|
||||
|
||||
@@ -63,7 +63,8 @@ export function* saveSetupKey(action: ReturnType<typeof actions.saveSetupKey.req
|
||||
auto_groups: newGroups,
|
||||
type: keyToSave.type,
|
||||
expires_in: keyToSave.expires_in,
|
||||
usage_limit: keyToSave.usage_limit
|
||||
usage_limit: keyToSave.usage_limit,
|
||||
ephemeral: keyToSave.ephemeral
|
||||
} as SetupKeyToSave
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import {ExpiresInValue} from "../../views/ExpiresInInput";
|
||||
import moment from "moment";
|
||||
|
||||
export interface SetupKey {
|
||||
@@ -14,12 +13,12 @@ export interface SetupKey {
|
||||
valid: boolean;
|
||||
auto_groups: string[]
|
||||
expires_in: number;
|
||||
usage_limit: number;
|
||||
usage_limit: any;
|
||||
ephemeral: boolean;
|
||||
}
|
||||
|
||||
export interface FormSetupKey extends SetupKey {
|
||||
autoGroupNames: string[]
|
||||
expiresInFormatted: ExpiresInValue
|
||||
exp: moment.Moment
|
||||
last: moment.Moment
|
||||
}
|
||||
|
||||
@@ -1,54 +1,72 @@
|
||||
import {ActionType, createAction, createAsyncAction} from 'typesafe-actions';
|
||||
import {User, UserToSave} from './types';
|
||||
import {ApiError, CreateResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
|
||||
import { ActionType, createAction, createAsyncAction } from "typesafe-actions";
|
||||
import { User, UserToSave } from "./types";
|
||||
import {
|
||||
ApiError,
|
||||
CreateResponse,
|
||||
DeleteResponse,
|
||||
RequestPayload,
|
||||
} from "../../services/api-client/types";
|
||||
|
||||
const actions = {
|
||||
getUsers: createAsyncAction(
|
||||
'GET_USERS_REQUEST',
|
||||
'GET_USERS_SUCCESS',
|
||||
'GET_USERS_FAILURE',
|
||||
"GET_USERS_REQUEST",
|
||||
"GET_USERS_SUCCESS",
|
||||
"GET_USERS_FAILURE"
|
||||
)<RequestPayload<null>, User[], ApiError>(),
|
||||
|
||||
getServiceUsers: createAsyncAction(
|
||||
'GET_SERVICE_USERS_REQUEST',
|
||||
'GET_SERVICE_USERS_SUCCESS',
|
||||
'GET_SERVICE_USERS_FAILURE',
|
||||
"GET_SERVICE_USERS_REQUEST",
|
||||
"GET_SERVICE_USERS_SUCCESS",
|
||||
"GET_SERVICE_USERS_FAILURE"
|
||||
)<RequestPayload<null>, User[], ApiError>(),
|
||||
|
||||
getRegularUsers: createAsyncAction(
|
||||
'GET_REGULAR_USERS_REQUEST',
|
||||
'GET_REGULAR_USERS_SUCCESS',
|
||||
'GET_REGULAR_USERS_FAILURE',
|
||||
"GET_REGULAR_USERS_REQUEST",
|
||||
"GET_REGULAR_USERS_SUCCESS",
|
||||
"GET_REGULAR_USERS_FAILURE"
|
||||
)<RequestPayload<null>, User[], ApiError>(),
|
||||
|
||||
deleteUser: createAsyncAction(
|
||||
'DELETE_USER_REQUEST',
|
||||
'DELETE_USER_SUCCESS',
|
||||
'DELETE_USER_FAILURE',
|
||||
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
|
||||
setDeletedUser: createAction('SET_DELETED_USER')<DeleteResponse<string | null>>(),
|
||||
resetDeletedUser: createAction('RESET_DELETED_USER')<null>(),
|
||||
"DELETE_USER_REQUEST",
|
||||
"DELETE_USER_SUCCESS",
|
||||
"DELETE_USER_FAILURE"
|
||||
)<
|
||||
RequestPayload<string>,
|
||||
DeleteResponse<string | null>,
|
||||
DeleteResponse<string | null>
|
||||
>(),
|
||||
setDeletedUser:
|
||||
createAction("SET_DELETED_USER")<DeleteResponse<string | null>>(),
|
||||
resetDeletedUser: createAction("RESET_DELETED_USER")<null>(),
|
||||
|
||||
// used to set a user object that was picked in the user table in the UserUpdate drawer (user update window on right-side).
|
||||
setUser: createAction('SET_USER')<User>(),
|
||||
setUser: createAction("SET_USER")<User>(),
|
||||
// used to make the UserUpdate drawer visible in the UI.
|
||||
setUpdateUserDrawerVisible: createAction('SET_UPDATE_USER_VISIBLE')<boolean>(),
|
||||
setUpdateUserDrawerVisible: createAction(
|
||||
"SET_UPDATE_USER_VISIBLE"
|
||||
)<boolean>(),
|
||||
// used to make the ViewUserPopup visible in the UI.
|
||||
setInviteUserPopupVisible: createAction('SET_INVITE_USER_VISIBLE')<boolean>(),
|
||||
setInviteUserPopupVisible: createAction("SET_INVITE_USER_VISIBLE")<boolean>(),
|
||||
// used to make the EditUserPopup visible in the UI.
|
||||
setEditUserPopupVisible: createAction('SET_EDIT_USER_VISIBLE')<boolean>(),
|
||||
setEditUserPopupVisible: createAction("SET_EDIT_USER_VISIBLE")<boolean>(),
|
||||
// used to make the AddServiceUserPopup visible in the UI.
|
||||
setAddServiceUserPopupVisible: createAction('SET_ADD_SERVICE_USER_VISIBLE')<boolean>(),
|
||||
setAddServiceUserPopupVisible: createAction(
|
||||
"SET_ADD_SERVICE_USER_VISIBLE"
|
||||
)<boolean>(),
|
||||
// used to remember what tab was open on users page
|
||||
setUserTabOpen: createAction('SET_USER_TAB_OPEN')<string>(),
|
||||
setUserTabOpen: createAction("SET_USER_TAB_OPEN")<string>(),
|
||||
|
||||
saveUser: createAsyncAction(
|
||||
'SAVE_USER_REQUEST',
|
||||
'SAVE_USER_SUCCESS',
|
||||
'SAVE_USER_FAILURE',
|
||||
)<RequestPayload<UserToSave>, CreateResponse<User | null>, CreateResponse<User | null>>(),
|
||||
setSavedUser: createAction('SET_SAVED_USER')<CreateResponse<User | null>>(),
|
||||
resetSavedUser: createAction('RESET_SAVED_USER')<null>(),
|
||||
"SAVE_USER_REQUEST",
|
||||
"SAVE_USER_SUCCESS",
|
||||
"SAVE_USER_FAILURE"
|
||||
)<
|
||||
RequestPayload<UserToSave>,
|
||||
CreateResponse<User | null>,
|
||||
CreateResponse<User | null>
|
||||
>(),
|
||||
setSavedUser: createAction("SET_SAVED_USER")<CreateResponse<User | null>>(),
|
||||
resetSavedUser: createAction("RESET_SAVED_USER")<null>(),
|
||||
};
|
||||
|
||||
export type ActionTypes = ActionType<typeof actions>;
|
||||
|
||||
@@ -1,159 +1,183 @@
|
||||
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
|
||||
import {ApiError, ApiResponse, CreateResponse, DeleteResponse} from '../../services/api-client/types';
|
||||
import {User, UserToSave} from './types'
|
||||
import service from './service';
|
||||
import actions from './actions';
|
||||
import { all, call, put, select, takeLatest } from "redux-saga/effects";
|
||||
import {
|
||||
ApiError,
|
||||
ApiResponse,
|
||||
CreateResponse,
|
||||
DeleteResponse,
|
||||
} from "../../services/api-client/types";
|
||||
import { User, UserToSave } from "./types";
|
||||
import service from "./service";
|
||||
import actions from "./actions";
|
||||
import serviceGroup from "../group/service";
|
||||
import {Group} from "../group/types";
|
||||
import { Group } from "../group/types";
|
||||
|
||||
export function* getUsers(action: ReturnType<typeof actions.getUsers.request>): Generator {
|
||||
try {
|
||||
const effect = yield call(service.getUsers, action.payload);
|
||||
const response = effect as ApiResponse<User[]>;
|
||||
export function* getUsers(
|
||||
action: ReturnType<typeof actions.getUsers.request>
|
||||
): Generator {
|
||||
try {
|
||||
const effect = yield call(service.getUsers, action.payload);
|
||||
const response = effect as ApiResponse<User[]>;
|
||||
|
||||
yield put(actions.getUsers.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getUsers.failure(err as ApiError));
|
||||
}
|
||||
yield put(actions.getUsers.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getUsers.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export function* getServiceUsers(action: ReturnType<typeof actions.getServiceUsers.request>): Generator {
|
||||
try {
|
||||
action.payload.queryParams = {service_user: true}
|
||||
const effect = yield call(service.getUsers, action.payload);
|
||||
const response = effect as ApiResponse<User[]>;
|
||||
export function* getServiceUsers(
|
||||
action: ReturnType<typeof actions.getServiceUsers.request>
|
||||
): Generator {
|
||||
try {
|
||||
action.payload.queryParams = { service_user: true };
|
||||
const effect = yield call(service.getUsers, action.payload);
|
||||
const response = effect as ApiResponse<User[]>;
|
||||
|
||||
yield put(actions.getServiceUsers.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getServiceUsers.failure(err as ApiError));
|
||||
}
|
||||
yield put(actions.getServiceUsers.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getServiceUsers.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export function* getRegularUsers(action: ReturnType<typeof actions.getRegularUsers.request>): Generator {
|
||||
try {
|
||||
action.payload.queryParams = {service_user: false}
|
||||
const effect = yield call(service.getUsers, action.payload);
|
||||
const response = effect as ApiResponse<User[]>;
|
||||
export function* getRegularUsers(
|
||||
action: ReturnType<typeof actions.getRegularUsers.request>
|
||||
): Generator {
|
||||
try {
|
||||
action.payload.queryParams = { service_user: false };
|
||||
const effect = yield call(service.getUsers, action.payload);
|
||||
const response = effect as ApiResponse<User[]>;
|
||||
|
||||
yield put(actions.getRegularUsers.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getRegularUsers.failure(err as ApiError));
|
||||
}
|
||||
yield put(actions.getRegularUsers.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getRegularUsers.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export function* saveUser(action: ReturnType<typeof actions.saveUser.request>): Generator {
|
||||
try {
|
||||
yield put(actions.setSavedUser({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as CreateResponse<User | null>))
|
||||
export function* saveUser(
|
||||
action: ReturnType<typeof actions.saveUser.request>
|
||||
): Generator {
|
||||
try {
|
||||
yield put(
|
||||
actions.setSavedUser({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null,
|
||||
} as CreateResponse<User | null>)
|
||||
);
|
||||
|
||||
const userToSave = action.payload.payload
|
||||
const userToSave = action.payload.payload;
|
||||
|
||||
let groupsToCreate = userToSave.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 = [...userToSave.auto_groups, ...resGroups]
|
||||
let payload = {
|
||||
name: userToSave.name,
|
||||
email: userToSave.email,
|
||||
role: userToSave.role,
|
||||
auto_groups: newGroups,
|
||||
is_service_user: userToSave.is_service_user,
|
||||
is_blocked: userToSave.is_blocked
|
||||
} as UserToSave
|
||||
|
||||
let effect
|
||||
if (!userToSave.id) {
|
||||
effect = yield call(service.createUser, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: payload
|
||||
});
|
||||
} else {
|
||||
payload.id = userToSave.id
|
||||
effect = yield call(service.editUser, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: payload
|
||||
});
|
||||
}
|
||||
const response = effect as ApiResponse<User>;
|
||||
|
||||
yield put(actions.saveUser.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as CreateResponse<User | null>));
|
||||
} catch (err) {
|
||||
yield put(actions.saveUser.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as CreateResponse<User | null>));
|
||||
let groupsToCreate = userToSave.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 = [...userToSave.auto_groups, ...resGroups];
|
||||
let payload = {
|
||||
name: userToSave.name,
|
||||
email: userToSave.email,
|
||||
role: userToSave.role,
|
||||
auto_groups: newGroups,
|
||||
is_service_user: userToSave.is_service_user,
|
||||
is_blocked: userToSave.is_blocked,
|
||||
} as UserToSave;
|
||||
|
||||
let effect;
|
||||
if (!userToSave.id) {
|
||||
effect = yield call(service.createUser, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: payload,
|
||||
});
|
||||
} else {
|
||||
payload.id = userToSave.id;
|
||||
effect = yield call(service.editUser, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: payload,
|
||||
});
|
||||
}
|
||||
const response = effect as ApiResponse<User>;
|
||||
|
||||
yield put(
|
||||
actions.saveUser.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body,
|
||||
} as CreateResponse<User | null>)
|
||||
);
|
||||
} catch (err) {
|
||||
yield put(
|
||||
actions.saveUser.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: err as ApiError,
|
||||
data: null,
|
||||
} as CreateResponse<User | null>)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function* deleteUser(action: ReturnType<typeof actions.deleteUser.request>): Generator {
|
||||
try {
|
||||
yield call(actions.setDeletedUser,{
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>)
|
||||
export function* deleteUser(
|
||||
action: ReturnType<typeof actions.deleteUser.request>
|
||||
): Generator {
|
||||
try {
|
||||
yield put(
|
||||
actions.setDeletedUser({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null,
|
||||
} as DeleteResponse<string | null>)
|
||||
);
|
||||
|
||||
const effect = yield call(service.deleteUser, action.payload);
|
||||
const response = effect as ApiResponse<any>;
|
||||
|
||||
yield put(actions.deleteUser.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as DeleteResponse<string | null>));
|
||||
|
||||
const users = (yield select(state => state.users.data)) as User[]
|
||||
const regularUsers = (yield select(state => state.users.regularUsers)) as User[]
|
||||
const serviceUsers = (yield select(state => state.users.serviceUsers)) as User[]
|
||||
yield put(actions.getUsers.success(users.filter((p:User) => p.id !== action.payload.payload)))
|
||||
yield put(actions.getRegularUsers.success(regularUsers.filter((p:User) => p.id !== action.payload.payload)))
|
||||
yield put(actions.getServiceUsers.success(serviceUsers.filter((p:User) => p.id !== action.payload.payload)))
|
||||
} catch (err) {
|
||||
yield put(actions.deleteUser.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>));
|
||||
}
|
||||
const effect = yield call(service.deleteUser, action.payload);
|
||||
const response = effect as ApiResponse<any>;
|
||||
yield put(
|
||||
actions.deleteUser.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body,
|
||||
} as DeleteResponse<string | null>)
|
||||
);
|
||||
} catch (err) {
|
||||
yield put(
|
||||
actions.deleteUser.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.getUsers.request, getUsers),
|
||||
takeLatest(actions.getServiceUsers.request, getServiceUsers),
|
||||
takeLatest(actions.getRegularUsers.request, getRegularUsers),
|
||||
takeLatest(actions.saveUser.request, saveUser),
|
||||
takeLatest(actions.deleteUser.request, deleteUser)
|
||||
]);
|
||||
yield all([
|
||||
takeLatest(actions.getUsers.request, getUsers),
|
||||
takeLatest(actions.getServiceUsers.request, getServiceUsers),
|
||||
takeLatest(actions.getRegularUsers.request, getRegularUsers),
|
||||
takeLatest(actions.saveUser.request, saveUser),
|
||||
takeLatest(actions.deleteUser.request, deleteUser),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface User {
|
||||
is_current?: boolean;
|
||||
is_service_user?: boolean;
|
||||
is_blocked?: boolean;
|
||||
last_login?: string;
|
||||
}
|
||||
|
||||
export interface FormUser extends User {
|
||||
|
||||
@@ -59,22 +59,22 @@ function getFormattedDate(date, preformattedDate = false, hideYear = false) {
|
||||
|
||||
if (minutes < 10) {
|
||||
// Adding leading zero to minutes
|
||||
minutes = `0${ minutes }`;
|
||||
minutes = `0${minutes}`;
|
||||
}
|
||||
|
||||
if (preformattedDate) {
|
||||
// Today
|
||||
// Yesterday
|
||||
return `${ preformattedDate }`;
|
||||
return `${preformattedDate}`;
|
||||
}
|
||||
|
||||
if (hideYear) {
|
||||
// 10. January
|
||||
return `${ day }. ${ month }`;
|
||||
return `${day}. ${month}`;
|
||||
}
|
||||
|
||||
// 10. January 2017.
|
||||
return `${ day }. ${ month } ${ year }`;
|
||||
return `${day}. ${month} ${year}`;
|
||||
}
|
||||
|
||||
export const fullDate = (dateParam) => {
|
||||
@@ -100,24 +100,26 @@ export const timeAgo = (dateParam) => {
|
||||
const isToday = today.toDateString() === date.toDateString();
|
||||
const isYesterday = yesterday.toDateString() === date.toDateString();
|
||||
const isThisYear = today.getFullYear() === date.getFullYear();
|
||||
|
||||
const never = date.getFullYear() === 1;
|
||||
|
||||
if (seconds < -1) {
|
||||
return getFormattedDate(date, false, true);
|
||||
} else if (seconds < 5) {
|
||||
return 'just now';
|
||||
} else if (seconds < 60) {
|
||||
return `${ seconds } seconds ago`;
|
||||
return `${seconds} seconds ago`;
|
||||
} else if (seconds < 90) {
|
||||
return 'about a minute ago';
|
||||
} else if (minutes < 60) {
|
||||
return `${ minutes } minutes ago`;
|
||||
return `${minutes} minutes ago`;
|
||||
} else if (isToday) {
|
||||
return getFormattedDate(date, 'today'); // Today at 10:20
|
||||
} else if (isYesterday) {
|
||||
return getFormattedDate(date, 'yesterday'); // Yesterday at 10:20
|
||||
} else if (isThisYear) {
|
||||
return getFormattedDate(date, false, true); // 10. January at 10:20
|
||||
} else if (never) {
|
||||
return 'never';
|
||||
}
|
||||
|
||||
return getFormattedDate(date); // 10. January 2017. at 10:20
|
||||
@@ -133,4 +135,19 @@ export const isNetBirdHosted = () => {
|
||||
|
||||
export const isLocalDev = () => {
|
||||
return window.location.hostname.includes("localhost")
|
||||
}
|
||||
}
|
||||
|
||||
const domainRegex =
|
||||
/^(?!.*\s)[a-zA-Z0-9](?!.*\s$)(?!.*\.$)(?:(?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.){1,126}(?!-)[a-zA-Z0-9-]{1,63}(?<!-)$/;
|
||||
|
||||
export const domainValidator = (_, domain) => {
|
||||
if (domainRegex.test(domain)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
// setIsPrimary(false);
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
"Please enter a valid domain, e.g. example.com or intra.example.com"
|
||||
)
|
||||
);
|
||||
};
|
||||
21
src/utils/filterState.js
Normal file
@@ -0,0 +1,21 @@
|
||||
export const storeFilterState = (page, filterName, filterValue) => {
|
||||
let filterStateObj = {}
|
||||
|
||||
const getValue = localStorage.getItem(page)
|
||||
if (getValue) {
|
||||
filterStateObj = JSON.parse(getValue)
|
||||
}
|
||||
|
||||
filterStateObj[filterName] = filterValue
|
||||
localStorage.setItem(page, JSON.stringify(filterStateObj))
|
||||
}
|
||||
|
||||
|
||||
export const getFilterState = (page, filterName) => {
|
||||
const getFilterObject = JSON.parse(localStorage.getItem(page))
|
||||
if (getFilterObject) {
|
||||
return getFilterObject[filterName]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,141 +1,182 @@
|
||||
import {CustomTagProps} from "rc-select/lib/BaseSelect";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {Col, Divider, Row, Tag} from "antd";
|
||||
import {useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import { CustomTagProps } from "rc-select/lib/BaseSelect";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Col, Divider, Row, Tag } from "antd";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import { RuleObject } from "antd/lib/form";
|
||||
|
||||
export const useGetGroupTagHelpers = () => {
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const groups = useSelector((state: RootState) => state.group.data);
|
||||
const [tagGroups, setTagGroups] = useState([] as any[]);
|
||||
const [groupTagFilterAll, setGroupTagFilterAll] = useState(false);
|
||||
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[]);
|
||||
|
||||
const [tagGroups, setTagGroups] = useState([] as string[])
|
||||
const [groupTagFilterAll, setGroupTagFilterAll] = useState(false)
|
||||
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[])
|
||||
const blueTagRender = (props: CustomTagProps) => {
|
||||
return tagRender(props, "blue");
|
||||
};
|
||||
const grayTagRender = (props: CustomTagProps) => {
|
||||
return tagRender(props, "");
|
||||
};
|
||||
|
||||
const tagRender = (props: CustomTagProps) => {
|
||||
const {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 handleChangeTags = (value: string[]) => {
|
||||
let validatedValues: string[] = []
|
||||
value.forEach(function (v) {
|
||||
if (v.trim().length) {
|
||||
validatedValues.push(v)
|
||||
}
|
||||
})
|
||||
setSelectedTagGroups(validatedValues)
|
||||
const tagRender = (props: CustomTagProps, color: string) => {
|
||||
const { value, closable, onClose } = props;
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
const groupName = getGroupNameFromID(value);
|
||||
return (
|
||||
<Tag
|
||||
color={color}
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{ marginRight: 3 }}
|
||||
>
|
||||
{groupName && groupName.length ? groupName[0] : value}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
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[]) => {
|
||||
const groupsName = getGroupNamesFromIDs(value);
|
||||
let validatedValues: string[] = [];
|
||||
groupsName.forEach(function (v) {
|
||||
if (v.trim().length) {
|
||||
validatedValues.push(v);
|
||||
}
|
||||
});
|
||||
setSelectedTagGroups(validatedValues);
|
||||
};
|
||||
|
||||
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 optionRender = (label: string, id: any) => {
|
||||
let peersCount = "";
|
||||
const g = groups.find((_g) => _g.id === id);
|
||||
if (g)
|
||||
peersCount = ` - ${g.peers_count || 0} ${
|
||||
!g.peers_count || parseInt(g.peers_count) !== 1 ? "peers" : "peer"
|
||||
} `;
|
||||
return (
|
||||
<div>
|
||||
<Tag color="blue" style={{ marginRight: 3 }}>
|
||||
{label}
|
||||
</Tag>
|
||||
<span style={{ fontSize: ".85em" }}>{peersCount}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getExistingAndToCreateGroupsLists = (
|
||||
groupIDsList: string[]
|
||||
): [string[], string[]] => {
|
||||
const groupIDList =
|
||||
groups
|
||||
?.filter((g) => groupIDsList.includes(g.id || ""))
|
||||
.map((g) => g.id || "") || [];
|
||||
|
||||
// find groups that do not yet exist (newly added by the user)
|
||||
const existingGroupsNames: any[] = groups?.map((g) => g.id);
|
||||
const groupNameListToCreate = groupIDsList.filter(
|
||||
(s) => !existingGroupsNames.includes(s)
|
||||
);
|
||||
return [groupIDList, groupNameListToCreate];
|
||||
};
|
||||
|
||||
const getGroupNamesFromIDs = (groupIDList: string[]): string[] => {
|
||||
if (!groupIDList) {
|
||||
return [];
|
||||
}
|
||||
return (
|
||||
groups
|
||||
?.filter((g) => groupIDList.includes(g.id!))
|
||||
.map((g) => g.name || "") || []
|
||||
);
|
||||
};
|
||||
|
||||
const getGroupNameFromID = (groupID: string) => {
|
||||
if (!groupID) {
|
||||
return "";
|
||||
}
|
||||
return (
|
||||
groups?.filter((g) => groupID === g.id).map((g) => g.name || "") || []
|
||||
);
|
||||
};
|
||||
|
||||
const selectValidator = (obj: RuleObject, value: string[]) => {
|
||||
if (!value.length) {
|
||||
return Promise.reject(new Error("Please enter at least one group"));
|
||||
}
|
||||
|
||||
const getExistingAndToCreateGroupsLists = (groupNameList: string[]): [string[], string[]] => {
|
||||
const groupIDList = groups?.filter(g => groupNameList.includes(g.name)).map(g => g.id || '') || []
|
||||
// find groups that do not yet exist (newly added by the user)
|
||||
const existingGroupsNames: string[] = groups?.map(g => g.name);
|
||||
const groupNameListToCreate = groupNameList.filter(s => !existingGroupsNames.includes(s))
|
||||
return [groupIDList, groupNameListToCreate]
|
||||
return selectValidatorEmptyStrings(obj, value);
|
||||
};
|
||||
|
||||
const selectValidatorEmptyStrings = (_: 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")
|
||||
);
|
||||
}
|
||||
|
||||
const getGroupNamesFromIDs = (groupIDList: string[]): string[] => {
|
||||
if (!groupIDList) {
|
||||
return []
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
return groups?.filter(g => groupIDList.includes(g.id!)).map(g => g.name || '') || []
|
||||
useEffect(() => {
|
||||
if (groupTagFilterAll) {
|
||||
setTagGroups(groups?.filter((g) => g.name !== "All").map((g) => g) || []);
|
||||
} else {
|
||||
setTagGroups(groups);
|
||||
}
|
||||
}, [groups, groupTagFilterAll]);
|
||||
|
||||
const selectValidator = (obj: RuleObject, value: string[]) => {
|
||||
if (!value.length) {
|
||||
return Promise.reject(new Error("Please enter at least one group"))
|
||||
}
|
||||
|
||||
return selectValidatorEmptyStrings(obj,value)
|
||||
}
|
||||
|
||||
const selectValidatorEmptyStrings = (_: 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()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (groupTagFilterAll) {
|
||||
setTagGroups(groups?.filter(g => g.name != "All").map(g => g.name) || [])
|
||||
} else {
|
||||
setTagGroups(groups?.map(g => g.name) || [])
|
||||
}
|
||||
}, [groups])
|
||||
|
||||
return {
|
||||
tagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
selectedTagGroups,
|
||||
setGroupTagFilterAll,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator,
|
||||
selectValidatorEmptyStrings
|
||||
}
|
||||
}
|
||||
return {
|
||||
tagRender,
|
||||
blueTagRender,
|
||||
grayTagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
selectedTagGroups,
|
||||
groupTagFilterAll,
|
||||
setGroupTagFilterAll,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator,
|
||||
selectValidatorEmptyStrings,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import {useState} from "react";
|
||||
import {storeFilterState} from "./filterState";
|
||||
|
||||
export const usePageSizeHelpers = () => {
|
||||
export const usePageSizeHelpers = (defaultSize = 10) => {
|
||||
const [pageSize, setPageSize] = useState(defaultSize);
|
||||
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const onChangePageSize = (value: string) => {
|
||||
setPageSize(parseInt(value.toString()))
|
||||
}
|
||||
const onChangePageSize = (value: string, pageName: string) => {
|
||||
setPageSize(parseInt(value.toString()));
|
||||
storeFilterState(pageName, "pageSize", parseInt(value.toString()));
|
||||
};
|
||||
|
||||
const pageSizeOptions = [
|
||||
{label: "10", value: "10"},
|
||||
{label: "25", value: "25"},
|
||||
{label: "50", value: "50"},
|
||||
{label: "100", value: "100"},
|
||||
{label: "1000", value: "1000"}
|
||||
]
|
||||
{label: "1000", value: "1000"},
|
||||
];
|
||||
|
||||
return {
|
||||
onChangePageSize,
|
||||
pageSize,
|
||||
pageSizeOptions
|
||||
}
|
||||
}
|
||||
pageSizeOptions,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@ export interface RouteDataTable extends Route {
|
||||
|
||||
export interface GroupedDataTable {
|
||||
key: string
|
||||
network_id: string
|
||||
network_id: any
|
||||
network: string
|
||||
enabled: boolean
|
||||
masquerade: boolean
|
||||
|
||||
@@ -1,311 +1,501 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as eventActions} from '../store/event';
|
||||
import {Container} from "../components/Container";
|
||||
import {Alert, Button, Card, Col, Input, Row, Select, Space, Table, Typography,} from "antd";
|
||||
import {Event} from "../store/event/types";
|
||||
import {filter} from "lodash";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import { actions as eventActions } from "../store/event";
|
||||
import { Container } from "../components/Container";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Input,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import { Event } from "../store/event/types";
|
||||
import { filter } from "lodash";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {useOidcUser} from "@axa-fr/react-oidc";
|
||||
import {capitalize, formatDateTime} from "../utils/common";
|
||||
import {User} from "../store/user/types";
|
||||
import {usePageSizeHelpers} from "../utils/pageSize";
|
||||
import {QuestionCircleFilled} from "@ant-design/icons";
|
||||
import { useGetTokenSilently } from "../utils/token";
|
||||
import { useOidcUser } from "@axa-fr/react-oidc";
|
||||
import { capitalize, formatDateTime } from "../utils/common";
|
||||
import { User } from "../store/user/types";
|
||||
import { usePageSizeHelpers } from "../utils/pageSize";
|
||||
import { QuestionCircleFilled } from "@ant-design/icons";
|
||||
import { storeFilterState, getFilterState } from "../utils/filterState";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
const {Column} = Table;
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
const { Column } = Table;
|
||||
|
||||
interface EventDataTable extends Event {
|
||||
}
|
||||
interface EventDataTable extends Event {}
|
||||
|
||||
export const Activity = () => {
|
||||
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const {oidcUser} = useOidcUser();
|
||||
const dispatch = useDispatch()
|
||||
const { onChangePageSize, pageSizeOptions, pageSize } = usePageSizeHelpers();
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const { oidcUser } = useOidcUser();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const events = useSelector((state: RootState) => state.event.data);
|
||||
const failed = useSelector((state: RootState) => state.event.failed);
|
||||
const loading = useSelector((state: RootState) => state.event.loading);
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
const setupKeys = useSelector((state: RootState) => state.setupKey.data);
|
||||
const events = useSelector((state: RootState) => state.event.data);
|
||||
const failed = useSelector((state: RootState) => state.event.failed);
|
||||
const loading = useSelector((state: RootState) => state.event.loading);
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
const setupKeys = useSelector((state: RootState) => state.setupKey.data);
|
||||
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [dataTable, setDataTable] = useState([] as EventDataTable[]);
|
||||
const [textToSearch, setTextToSearch] = useState("");
|
||||
const [dataTable, setDataTable] = useState([] as EventDataTable[]);
|
||||
|
||||
const transformDataTable = (d: Event[]): EventDataTable[] => {
|
||||
return d.map((p) => ({ key: p.id, ...p } as EventDataTable));
|
||||
};
|
||||
|
||||
const transformDataTable = (d: Event[]): EventDataTable[] => {
|
||||
return d.map(p => ({key: p.id, ...p} as EventDataTable))
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
eventActions.getEvents.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
// useEffect(() => {
|
||||
// setDataTable(transformDataTable(events));
|
||||
// }, [events]);
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable("")));
|
||||
}, [textToSearch]);
|
||||
|
||||
const filterDataTable = (searchText: string): Event[] => {
|
||||
const t = searchText
|
||||
? searchText.toLowerCase().trim()
|
||||
: textToSearch.toLowerCase().trim();
|
||||
let usrsMatch: User[] = filter(
|
||||
users,
|
||||
(u: User) =>
|
||||
u.name?.toLowerCase().includes(t) || u.email?.toLowerCase().includes(t)
|
||||
) as User[];
|
||||
let f: Event[] = filter(
|
||||
events,
|
||||
(f: Event) =>
|
||||
(f.activity || f.id).toLowerCase().includes(t) ||
|
||||
t === "" ||
|
||||
usrsMatch.find((u) => u.id === f.initiator_id)
|
||||
) as Event[];
|
||||
return f;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && events) {
|
||||
const searchText = getFilterState("activityFilter", "search");
|
||||
if (searchText) setTextToSearch(searchText);
|
||||
|
||||
const pageSize = getFilterState("activityFilter", "pageSize");
|
||||
if (pageSize) onChangePageSize(pageSize, "activityFilter");
|
||||
|
||||
if (searchText || pageSize) {
|
||||
setDataTable(transformDataTable(filterDataTable(searchText)));
|
||||
} else {
|
||||
setDataTable(transformDataTable(events));
|
||||
}
|
||||
}
|
||||
}, [loading, events]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(eventActions.getEvents.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(events))
|
||||
}, [events])
|
||||
const onChangeTextToSearch = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
setTextToSearch(e.target.value);
|
||||
storeFilterState("activityFilter", "search", e.target.value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [textToSearch])
|
||||
|
||||
const filterDataTable = (): Event[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let usrsMatch: User[] = filter(users, (u: User) => (u.name)?.toLowerCase().includes(t) || (u.email)?.toLowerCase().includes(t)) as User[]
|
||||
let f: Event[] = filter(events, (f: Event) =>
|
||||
((f.activity || f.id).toLowerCase().includes(t) || t === "" || usrsMatch.find(u => u.id === f.initiator_id))
|
||||
) as Event[]
|
||||
return f
|
||||
}
|
||||
|
||||
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setTextToSearch(e.target.value)
|
||||
};
|
||||
|
||||
const searchDataTable = () => {
|
||||
const data = filterDataTable()
|
||||
setDataTable(transformDataTable(data))
|
||||
}
|
||||
|
||||
const getActivityRow = (objectType: string, name:string,text:string) => {
|
||||
return <Row> <Text>{objectType} <Text type="secondary">{name}</Text> {text}</Text> </Row>
|
||||
}
|
||||
|
||||
const renderActivity = (event: EventDataTable) => {
|
||||
let body = <Text>{event.activity}</Text>
|
||||
switch (event.activity_code) {
|
||||
case "peer.group.add":
|
||||
return getActivityRow("Group", event.meta.group,"added to peer")
|
||||
case "peer.group.delete":
|
||||
return getActivityRow("Group", event.meta.group,"removed from peer")
|
||||
case "user.group.add":
|
||||
return getActivityRow("Group", event.meta.group,"added to user")
|
||||
case "user.group.delete":
|
||||
return getActivityRow("Group", event.meta.group,"removed from user")
|
||||
case "setupkey.group.add":
|
||||
return getActivityRow("Group", event.meta.group,"added to setup key")
|
||||
case "setupkey.group.delete":
|
||||
return getActivityRow("Group", event.meta.group,"removed setup key")
|
||||
case "dns.setting.disabled.management.group.add":
|
||||
return getActivityRow("Group", event.meta.group,"added to disabled management DNS setting")
|
||||
case "dns.setting.disabled.management.group.delete":
|
||||
return getActivityRow("Group", event.meta.group,"removed from disabled management DNS setting")
|
||||
case "personal.access.token.create":
|
||||
return getActivityRow("Personal access token", event.meta.name,"added to user")
|
||||
case "personal.access.token.delete":
|
||||
return getActivityRow("Personal access token", event.meta.name,"removed from user")
|
||||
}
|
||||
return body
|
||||
}
|
||||
const renderInitiator = (event: EventDataTable) => {
|
||||
let body = <></>
|
||||
const user = users?.find(u => u.id === event.initiator_id)
|
||||
switch (event.activity_code) {
|
||||
case "setupkey.peer.add":
|
||||
const key = setupKeys?.find(k => k.id === event.initiator_id)
|
||||
if (key) {
|
||||
body = <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
<Row> <Text>{key.name}</Text> </Row>
|
||||
<Row> <Text type="secondary">Setup Key</Text> </Row>
|
||||
</span>
|
||||
}
|
||||
break
|
||||
default:
|
||||
if (user) {
|
||||
body = <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
<Row> <Text>{user.name ? user.name : user.id}</Text> </Row>
|
||||
<Row> <Text type="secondary">{user.email ? user.email : user.is_service_user ? "Service User" : "User"}</Text> </Row>
|
||||
</span>
|
||||
return body
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
const renderMultiRowSpan = (primaryRowText:string,secondaryRowText:string) => {
|
||||
return <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
<Row> <Text>{primaryRowText}</Text> </Row>
|
||||
<Row> <Text type="secondary">{secondaryRowText}</Text> </Row>
|
||||
</span>
|
||||
}
|
||||
|
||||
const renderTarget = (event: EventDataTable) => {
|
||||
if (event.activity_code === "account.create" || event.activity_code === "user.join") {
|
||||
return "-"
|
||||
}
|
||||
const user = users?.find(u => u.id === event.target_id)
|
||||
switch (event.activity_code) {
|
||||
case "account.create":
|
||||
case "user.join":
|
||||
return "-"
|
||||
case "rule.add":
|
||||
case "rule.delete":
|
||||
case "rule.update":
|
||||
return renderMultiRowSpan(event.meta.name,"Rule")
|
||||
case "policy.add":
|
||||
case "policy.delete":
|
||||
case "policy.update":
|
||||
return renderMultiRowSpan(event.meta.name, "Policy")
|
||||
case "setupkey.add":
|
||||
case "setupkey.revoke":
|
||||
case "setupkey.update":
|
||||
case "setupkey.overuse":
|
||||
let cType:string
|
||||
cType = capitalize(event.meta.type)
|
||||
return renderMultiRowSpan(event.meta.name,cType+" setup key "+event.meta.key)
|
||||
case "group.add":
|
||||
case "group.update":
|
||||
return renderMultiRowSpan(event.meta.name,"Group")
|
||||
case "nameserver.group.add":
|
||||
case "nameserver.group.update":
|
||||
case "nameserver.group.delete":
|
||||
return renderMultiRowSpan(event.meta.name,"Nameserver group")
|
||||
case "setupkey.peer.add":
|
||||
case "user.peer.add":
|
||||
case "user.peer.delete":
|
||||
case "peer.ssh.enable":
|
||||
case "peer.ssh.disable":
|
||||
case "peer.rename":
|
||||
case "peer.login.expiration.disable":
|
||||
case "peer.login.expiration.enable":
|
||||
return renderMultiRowSpan(event.meta.fqdn,event.meta.ip)
|
||||
case "route.add":
|
||||
case "route.delete":
|
||||
case "route.update":
|
||||
return renderMultiRowSpan(event.meta.name, "Route for range " + event.meta.network_range)
|
||||
case "user.group.add":
|
||||
case "user.group.delete":
|
||||
case "user.role.update":
|
||||
if (user) {
|
||||
return renderMultiRowSpan((user.name ? user.name : user.id),user.email ? user.email : user.is_service_user ? "Service User" : "User")
|
||||
}
|
||||
if (event.meta.user_name) {
|
||||
return renderMultiRowSpan(event.meta.user_name, event.meta.is_service_user ? "Service User" : "User")
|
||||
}
|
||||
return "-"
|
||||
case "setupkey.group.add":
|
||||
case "setupkey.group.delete":
|
||||
return renderMultiRowSpan(event.meta.setupkey,"Setup Key")
|
||||
case "peer.group.add":
|
||||
case "peer.group.delete":
|
||||
return renderMultiRowSpan(event.meta.peer_fqdn,event.meta.peer_ip)
|
||||
case "dns.setting.disabled.management.group.add":
|
||||
case "dns.setting.disabled.management.group.delete":
|
||||
case "account.setting.peer.login.expiration.enable":
|
||||
case "account.setting.peer.login.expiration.disable":
|
||||
case "account.setting.peer.login.expiration.update":
|
||||
return renderMultiRowSpan("","System setting")
|
||||
case "personal.access.token.create":
|
||||
case "personal.access.token.delete":
|
||||
if(user) {
|
||||
return renderMultiRowSpan((user.name ? user.name : user.id), user.email ? user.email : user.is_service_user ? "Service User" : "User")
|
||||
}
|
||||
if (event.meta.user_name) {
|
||||
return renderMultiRowSpan(event.meta.user_name,event.meta.is_service_user ? "Service User" : "User")
|
||||
}
|
||||
return "-"
|
||||
case "service.user.create":
|
||||
case "service.user.delete":
|
||||
return renderMultiRowSpan(event.meta.name,"Service User")
|
||||
case "user.invite":
|
||||
case "user.block":
|
||||
case "user.unblock":
|
||||
if (user) {
|
||||
return renderMultiRowSpan(user.name ? user.name : user.id,user.email ? user.email : "User")
|
||||
}
|
||||
break
|
||||
default:
|
||||
console.error("unknown event - missing handling", event.activity_code)
|
||||
}
|
||||
|
||||
return event.target_id
|
||||
}
|
||||
// const searchDataTable = () => {
|
||||
// const data = filterDataTable();
|
||||
// setDataTable(transformDataTable(data));
|
||||
// };
|
||||
|
||||
const getActivityRow = (objectType: string, name: string, text: string) => {
|
||||
return (
|
||||
<>
|
||||
<Container style={{paddingTop: "40px"}}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Activity</Title>
|
||||
<Paragraph>Here you can see all the account and network activity events</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">
|
||||
<Select value={pageSize.toString()} options={pageSizeOptions}
|
||||
onChange={onChangePageSize} className="select-rows-per-page-en"/>
|
||||
<Row>
|
||||
{" "}
|
||||
<Text>
|
||||
{objectType} <Text type="secondary">{name}</Text> {text}
|
||||
</Text>{" "}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24}
|
||||
sm={24}
|
||||
md={5}
|
||||
lg={5}
|
||||
xl={5}
|
||||
xxl={5} span={5}>
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://netbird.io/docs/how-to-guides/activity-monitoring">Learn more about activity tracking</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{failed &&
|
||||
<Alert message={failed.message} description={failed.data ? failed.data.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} activity events`)
|
||||
}}
|
||||
className="card-table"
|
||||
showSorterTooltip={false}
|
||||
scroll={{x: true}}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}
|
||||
size="small"
|
||||
>
|
||||
<Column title="Timestamp" dataIndex="timestamp"
|
||||
render={(text, record, index) => {
|
||||
return formatDateTime(text)
|
||||
}}
|
||||
/>
|
||||
<Column title="Activity" dataIndex="activity"
|
||||
render={(text, record, index) => {
|
||||
return renderActivity(record as EventDataTable)
|
||||
}}
|
||||
/>
|
||||
<Column title="Initiated By" dataIndex="initiator_id"
|
||||
render={(text, record, index) => {
|
||||
return renderInitiator(record as EventDataTable)
|
||||
}}
|
||||
/>
|
||||
<Column title="Target" dataIndex="target_id"
|
||||
render={(text, record, index) => {
|
||||
return renderTarget(record as EventDataTable)
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
const renderActivity = (event: EventDataTable) => {
|
||||
let body = <Text>{event.activity}</Text>;
|
||||
switch (event.activity_code) {
|
||||
case "peer.group.add":
|
||||
return getActivityRow("Group", event.meta.group, "added to peer");
|
||||
case "peer.group.delete":
|
||||
return getActivityRow("Group", event.meta.group, "removed from peer");
|
||||
case "user.group.add":
|
||||
return getActivityRow("Group", event.meta.group, "added to user");
|
||||
case "user.group.delete":
|
||||
return getActivityRow("Group", event.meta.group, "removed from user");
|
||||
case "setupkey.group.add":
|
||||
return getActivityRow("Group", event.meta.group, "added to setup key");
|
||||
case "setupkey.group.delete":
|
||||
return getActivityRow("Group", event.meta.group, "removed setup key");
|
||||
case "dns.setting.disabled.management.group.add":
|
||||
return getActivityRow(
|
||||
"Group",
|
||||
event.meta.group,
|
||||
"added to disabled management DNS setting"
|
||||
);
|
||||
case "dns.setting.disabled.management.group.delete":
|
||||
return getActivityRow(
|
||||
"Group",
|
||||
event.meta.group,
|
||||
"removed from disabled management DNS setting"
|
||||
);
|
||||
case "personal.access.token.create":
|
||||
return getActivityRow(
|
||||
"Personal access token",
|
||||
event.meta.name,
|
||||
"added to user"
|
||||
);
|
||||
case "personal.access.token.delete":
|
||||
return getActivityRow(
|
||||
"Personal access token",
|
||||
event.meta.name,
|
||||
"removed from user"
|
||||
);
|
||||
}
|
||||
return body;
|
||||
};
|
||||
const renderInitiator = (event: EventDataTable) => {
|
||||
let body = <></>;
|
||||
if (event.initiator_id == "sys") {
|
||||
body = (
|
||||
<span
|
||||
style={{
|
||||
height: "auto",
|
||||
whiteSpace: "normal",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
<Row>
|
||||
<Text type="secondary">System</Text>
|
||||
</Row>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
switch (event.activity_code) {
|
||||
case "peer.login.expire":
|
||||
body = (
|
||||
<span
|
||||
style={{
|
||||
height: "auto",
|
||||
whiteSpace: "normal",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
<Row>
|
||||
<Text type="secondary">System</Text>
|
||||
</Row>
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
case "setupkey.peer.add":
|
||||
const key = setupKeys?.find((k) => k.id === event.initiator_id);
|
||||
if (key) {
|
||||
body = (
|
||||
<span
|
||||
style={{
|
||||
height: "auto",
|
||||
whiteSpace: "normal",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
<Row>
|
||||
{" "}
|
||||
<Text>{key.name}</Text>{" "}
|
||||
</Row>
|
||||
<Row>
|
||||
{" "}
|
||||
<Text type="secondary">Setup Key</Text>{" "}
|
||||
</Row>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (event.initiator_name || event.initiator_email) {
|
||||
body = (
|
||||
<span
|
||||
style={{
|
||||
height: "auto",
|
||||
whiteSpace: "normal",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
<Row>
|
||||
{" "}
|
||||
<Text>
|
||||
{event.initiator_name
|
||||
? event.initiator_name
|
||||
: event.initiator_id}
|
||||
</Text>{" "}
|
||||
</Row>
|
||||
<Row>
|
||||
{" "}
|
||||
<Text type="secondary">
|
||||
{event.initiator_email ? event.initiator_email : "User"}
|
||||
</Text>{" "}
|
||||
</Row>
|
||||
</span>
|
||||
);
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
const renderMultiRowSpan = (
|
||||
primaryRowText: string,
|
||||
secondaryRowText: string
|
||||
) => {
|
||||
return (
|
||||
<span style={{ height: "auto", whiteSpace: "normal", textAlign: "left" }}>
|
||||
<Row>
|
||||
{" "}
|
||||
<Text>{primaryRowText}</Text>{" "}
|
||||
</Row>
|
||||
<Row>
|
||||
{" "}
|
||||
<Text type="secondary">{secondaryRowText}</Text>{" "}
|
||||
</Row>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTarget = (event: EventDataTable) => {
|
||||
if (
|
||||
event.activity_code === "account.create" ||
|
||||
event.activity_code === "user.join"
|
||||
) {
|
||||
return "-";
|
||||
}
|
||||
switch (event.activity_code) {
|
||||
case "account.create":
|
||||
case "user.join":
|
||||
case "dashboard.login":
|
||||
return "-";
|
||||
case "rule.add":
|
||||
case "rule.delete":
|
||||
case "rule.update":
|
||||
return renderMultiRowSpan(event.meta.name, "Rule");
|
||||
case "policy.add":
|
||||
case "policy.delete":
|
||||
case "policy.update":
|
||||
return renderMultiRowSpan(event.meta.name, "Policy");
|
||||
case "setupkey.add":
|
||||
case "setupkey.revoke":
|
||||
case "setupkey.update":
|
||||
case "setupkey.overuse":
|
||||
let cType: string;
|
||||
cType = capitalize(event.meta.type);
|
||||
return renderMultiRowSpan(
|
||||
event.meta.name,
|
||||
cType + " setup key " + event.meta.key
|
||||
);
|
||||
case "group.add":
|
||||
case "group.update":
|
||||
case "group.delete":
|
||||
return renderMultiRowSpan(event.meta.name, "Group");
|
||||
case "nameserver.group.add":
|
||||
case "nameserver.group.update":
|
||||
case "nameserver.group.delete":
|
||||
return renderMultiRowSpan(event.meta.name, "Nameserver group");
|
||||
case "setupkey.peer.add":
|
||||
case "user.peer.add":
|
||||
case "user.peer.delete":
|
||||
case "peer.ssh.enable":
|
||||
case "peer.ssh.disable":
|
||||
case "peer.rename":
|
||||
case "peer.login.expiration.disable":
|
||||
case "peer.login.expiration.enable":
|
||||
case "user.peer.login":
|
||||
case "peer.login.expire":
|
||||
return renderMultiRowSpan(event.meta.fqdn, event.meta.ip);
|
||||
case "route.add":
|
||||
case "route.delete":
|
||||
case "route.update":
|
||||
return renderMultiRowSpan(
|
||||
event.meta.name,
|
||||
"Route for range " + event.meta.network_range
|
||||
);
|
||||
case "user.group.add":
|
||||
case "user.group.delete":
|
||||
case "user.role.update":
|
||||
if (event.meta.email || event.meta.username || event.target_id) {
|
||||
return renderMultiRowSpan(
|
||||
event.meta.username ? event.meta.username : event.target_id,
|
||||
event.meta.email ? event.meta.email : "User"
|
||||
);
|
||||
}
|
||||
if (event.meta.user_name) {
|
||||
return renderMultiRowSpan(
|
||||
event.meta.user_name,
|
||||
event.meta.is_service_user ? "Service User" : "User"
|
||||
);
|
||||
}
|
||||
return "-";
|
||||
case "setupkey.group.add":
|
||||
case "setupkey.group.delete":
|
||||
return renderMultiRowSpan(event.meta.setupkey, "Setup Key");
|
||||
case "peer.group.add":
|
||||
case "peer.group.delete":
|
||||
return renderMultiRowSpan(event.meta.peer_fqdn, event.meta.peer_ip);
|
||||
case "dns.setting.disabled.management.group.add":
|
||||
case "dns.setting.disabled.management.group.delete":
|
||||
case "account.setting.peer.login.expiration.enable":
|
||||
case "account.setting.peer.login.expiration.disable":
|
||||
case "account.setting.peer.login.expiration.update":
|
||||
return renderMultiRowSpan("", "System setting");
|
||||
case "personal.access.token.create":
|
||||
case "personal.access.token.delete":
|
||||
if (event.meta.email || event.meta.username || event.target_id) {
|
||||
return renderMultiRowSpan(
|
||||
event.meta.username ? event.meta.username : event.target_id,
|
||||
event.meta.email ? event.meta.email : "User"
|
||||
);
|
||||
}
|
||||
if (event.meta.user_name) {
|
||||
return renderMultiRowSpan(
|
||||
event.meta.user_name,
|
||||
event.meta.is_service_user ? "Service User" : "User"
|
||||
);
|
||||
}
|
||||
return "-";
|
||||
case "service.user.create":
|
||||
case "service.user.delete":
|
||||
return renderMultiRowSpan(event.meta.username, "Service User");
|
||||
case "user.invite":
|
||||
case "user.block":
|
||||
case "user.delete":
|
||||
case "user.unblock":
|
||||
if (event.meta.email || event.meta.username || event.target_id) {
|
||||
return renderMultiRowSpan(
|
||||
event.meta.username ? event.meta.username : event.target_id,
|
||||
event.meta.email ? event.meta.email : "User"
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.error("unknown event - missing handling", event.activity_code);
|
||||
}
|
||||
|
||||
return event.target_id;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container style={{ paddingTop: "40px" }}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title className="page-heading">Activity</Title>
|
||||
<Paragraph type="secondary">
|
||||
Here you can see all the account and network activity events.{" "}
|
||||
<a
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
href="https://docs.netbird.io/how-to/monitor-system-and-network-activity"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</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">
|
||||
<Select
|
||||
value={pageSize.toString()}
|
||||
options={pageSizeOptions}
|
||||
onChange={(value) => {
|
||||
onChangePageSize(value, "activityFilter");
|
||||
}}
|
||||
className="select-rows-per-page-en"
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
{failed && (
|
||||
<Alert
|
||||
message={failed.message}
|
||||
description={failed.data ? failed.data.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} activity events`,
|
||||
}}
|
||||
className="card-table"
|
||||
showSorterTooltip={false}
|
||||
scroll={{ x: true }}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}
|
||||
>
|
||||
<Column
|
||||
title="Timestamp"
|
||||
dataIndex="timestamp"
|
||||
render={(text, record, index) => {
|
||||
return formatDateTime(text);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
title="Activity"
|
||||
dataIndex="activity"
|
||||
render={(text, record, index) => {
|
||||
return renderActivity(record as EventDataTable);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
title="Initiated By"
|
||||
dataIndex="initiator_id"
|
||||
render={(text, record, index) => {
|
||||
return renderInitiator(record as EventDataTable);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
title="Target"
|
||||
dataIndex="target_id"
|
||||
render={(text, record, index) => {
|
||||
return renderTarget(record as EventDataTable);
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Activity;
|
||||
|
||||
@@ -1,78 +1,91 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Container} from "../components/Container";
|
||||
import {
|
||||
Col,
|
||||
Row,
|
||||
Tabs,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import type { TabsProps } from 'antd';
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Container } from "../components/Container";
|
||||
import { Col, Row, Tabs, Typography } from "antd";
|
||||
import type { TabsProps } from "antd";
|
||||
import NameServerGroupUpdate from "../components/NameServerGroupUpdate";
|
||||
import NameServerGroupAdd from "../components/NameServerGroupAdd";
|
||||
import Nameservers from "./Nameservers";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import { actions as groupActions } from "../store/group";
|
||||
import { useGetTokenSilently } from "../utils/token";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import DNSSettingsForm from "./DNSSettings";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as dnsSettingsActions} from '../store/dns-settings';
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import { actions as dnsSettingsActions } from "../store/dns-settings";
|
||||
import { useGetGroupTagHelpers } from "../utils/groups";
|
||||
|
||||
const {Title, Paragraph} = Typography;
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
export const DNS = () => {
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const {
|
||||
getGroupNamesFromIDs,
|
||||
} = useGetGroupTagHelpers()
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
const { getGroupNamesFromIDs } = useGetGroupTagHelpers();
|
||||
|
||||
const dnsSettingsData = useSelector((state: RootState) => state.dnsSettings.data)
|
||||
const dnsSettingsData = useSelector(
|
||||
(state: RootState) => state.dnsSettings.data
|
||||
);
|
||||
const setupEditNameServerGroupVisible = useSelector(
|
||||
(state: RootState) => state.nameserverGroup.setupEditNameServerGroupVisible
|
||||
);
|
||||
const setupNewNameServerGroupVisible = useSelector(
|
||||
(state: RootState) => state.nameserverGroup.setupNewNameServerGroupVisible
|
||||
);
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
groupActions.getGroups.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
}, [])
|
||||
const nsTabKey = "1";
|
||||
const items: TabsProps["items"] = [
|
||||
{
|
||||
key: nsTabKey,
|
||||
label: "Nameservers",
|
||||
children: <Nameservers />,
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: "Settings",
|
||||
children: <DNSSettingsForm />,
|
||||
},
|
||||
];
|
||||
|
||||
const nsTabKey = '1'
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
key: nsTabKey,
|
||||
label: 'Nameservers',
|
||||
children: <Nameservers/>,
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: 'Settings',
|
||||
children: <DNSSettingsForm/>,
|
||||
},
|
||||
]
|
||||
|
||||
const onTabClick = (key:string) => {
|
||||
if (key == nsTabKey) {
|
||||
if (!dnsSettingsData) return
|
||||
dispatch(dnsSettingsActions.setDNSSettings({
|
||||
disabled_management_groups: getGroupNamesFromIDs(dnsSettingsData.disabled_management_groups),
|
||||
}))
|
||||
}
|
||||
const onTabClick = (key: string) => {
|
||||
if (key == nsTabKey) {
|
||||
if (!dnsSettingsData) return;
|
||||
dispatch(
|
||||
dnsSettingsActions.setDNSSettings({
|
||||
disabled_management_groups: getGroupNamesFromIDs(
|
||||
dnsSettingsData.disabled_management_groups
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container style={{paddingTop: "40px"}}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Tabs
|
||||
defaultActiveKey={nsTabKey}
|
||||
items={items}
|
||||
onTabClick={onTabClick}
|
||||
animated={{ inkBar: true, tabPane: false }}
|
||||
tabPosition="top"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<NameServerGroupUpdate/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{!setupEditNameServerGroupVisible && (
|
||||
<Container style={{ paddingTop: "40px" }}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Tabs
|
||||
defaultActiveKey={nsTabKey}
|
||||
items={items}
|
||||
onTabClick={onTabClick}
|
||||
animated={{ inkBar: true, tabPane: false }}
|
||||
tabPosition="top"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)}
|
||||
{setupEditNameServerGroupVisible && <NameServerGroupUpdate />}
|
||||
{setupNewNameServerGroupVisible && <NameServerGroupAdd />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DNS;
|
||||
export default DNS;
|
||||
|
||||
@@ -1,176 +1,243 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Form,
|
||||
message,
|
||||
Select,
|
||||
Space,
|
||||
Typography,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Form,
|
||||
message,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Typography,
|
||||
SelectProps,
|
||||
} from "antd";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
import {actions as dnsSettingsActions} from '../store/dns-settings';
|
||||
import {DNSSettings, DNSSettingsToSave} from "../store/dns-settings/types";
|
||||
import {actions as nsGroupActions} from "../store/nameservers";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
const styleNotification = {marginTop: 85}
|
||||
import { useGetTokenSilently } from "../utils/token";
|
||||
import { useGetGroupTagHelpers } from "../utils/groups";
|
||||
import { actions as dnsSettingsActions } from "../store/dns-settings";
|
||||
import { DNSSettings, DNSSettingsToSave } from "../store/dns-settings/types";
|
||||
import { actions as nsGroupActions } from "../store/nameservers";
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
const styleNotification = { marginTop: 85 };
|
||||
const { Option } = Select;
|
||||
export const DNSSettingsForm = () => {
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
tagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidatorEmptyStrings
|
||||
} = useGetGroupTagHelpers()
|
||||
const {
|
||||
blueTagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidatorEmptyStrings,
|
||||
} = useGetGroupTagHelpers();
|
||||
|
||||
const dnsSettings = useSelector((state: RootState) => state.dnsSettings.dnsSettings)
|
||||
const dnsSettingsData = useSelector((state: RootState) => state.dnsSettings.data)
|
||||
const savedDNSSettings = useSelector((state: RootState) => state.dnsSettings.savedDNSSettings)
|
||||
const loading = useSelector((state: RootState) => state.dnsSettings.loading);
|
||||
const dnsSettings = useSelector(
|
||||
(state: RootState) => state.dnsSettings.dnsSettings
|
||||
);
|
||||
const dnsSettingsData = useSelector(
|
||||
(state: RootState) => state.dnsSettings.data
|
||||
);
|
||||
const savedDNSSettings = useSelector(
|
||||
(state: RootState) => state.dnsSettings.savedDNSSettings
|
||||
);
|
||||
const loading = useSelector((state: RootState) => state.dnsSettings.loading);
|
||||
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [form] = Form.useForm()
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
dnsSettingsActions.getDNSSettings.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null,
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(dnsSettingsActions.getDNSSettings.request({
|
||||
useEffect(() => {
|
||||
if (!dnsSettingsData) return;
|
||||
dispatch(
|
||||
dnsSettingsActions.setDNSSettings({
|
||||
disabled_management_groups: dnsSettingsData.disabled_management_groups,
|
||||
})
|
||||
);
|
||||
}, [dnsSettingsData]);
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(dnsSettings);
|
||||
}, [dnsSettings]);
|
||||
|
||||
const createKey = "saving";
|
||||
useEffect(() => {
|
||||
if (savedDNSSettings.loading) {
|
||||
message.loading({
|
||||
content: "Saving...",
|
||||
key: createKey,
|
||||
duration: 0,
|
||||
style: styleNotification,
|
||||
});
|
||||
} else if (savedDNSSettings.success) {
|
||||
message.success({
|
||||
content: "DNS settings has been successfully saved.",
|
||||
key: createKey,
|
||||
duration: 2,
|
||||
style: styleNotification,
|
||||
});
|
||||
dispatch(
|
||||
dnsSettingsActions.setSavedDNSSettings({
|
||||
...savedDNSSettings,
|
||||
success: false,
|
||||
})
|
||||
);
|
||||
dispatch(dnsSettingsActions.resetSavedDNSSettings(null));
|
||||
} else if (savedDNSSettings.error) {
|
||||
let errorMsg = "Failed to update DNS settings";
|
||||
switch (savedDNSSettings.error.statusCode) {
|
||||
case 403:
|
||||
errorMsg =
|
||||
"Failed to update DNS settings. You might not have enough permissions.";
|
||||
break;
|
||||
default:
|
||||
errorMsg = savedDNSSettings.error.data.message
|
||||
? savedDNSSettings.error.data.message
|
||||
: errorMsg;
|
||||
break;
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: createKey,
|
||||
duration: 5,
|
||||
style: styleNotification,
|
||||
});
|
||||
dispatch(
|
||||
dnsSettingsActions.setSavedDNSSettings({
|
||||
...savedDNSSettings,
|
||||
error: null,
|
||||
})
|
||||
);
|
||||
dispatch(nsGroupActions.resetSavedNameServerGroup(null));
|
||||
}
|
||||
}, [savedDNSSettings]);
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
let dnsSettingsToSave = createDNSSettingsToSave(values);
|
||||
dispatch(
|
||||
dnsSettingsActions.saveDNSSettings.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dnsSettingsData) return
|
||||
dispatch(dnsSettingsActions.setDNSSettings({
|
||||
disabled_management_groups: getGroupNamesFromIDs(dnsSettingsData.disabled_management_groups),
|
||||
}))
|
||||
}, [dnsSettingsData])
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(dnsSettings)
|
||||
}, [dnsSettings])
|
||||
|
||||
const createKey = 'saving';
|
||||
useEffect(() => {
|
||||
if (savedDNSSettings.loading) {
|
||||
message.loading({content: 'Saving...', key: createKey, duration: 0, style: styleNotification});
|
||||
} else if (savedDNSSettings.success) {
|
||||
message.success({
|
||||
content: 'DNS settings has been successfully saved.',
|
||||
key: createKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(dnsSettingsActions.setSavedDNSSettings({...savedDNSSettings, success: false}));
|
||||
dispatch(dnsSettingsActions.resetSavedDNSSettings(null))
|
||||
} else if (savedDNSSettings.error) {
|
||||
let errorMsg = "Failed to update DNS settings"
|
||||
switch (savedDNSSettings.error.statusCode) {
|
||||
case 403:
|
||||
errorMsg = "Failed to update DNS settings. You might not have enough permissions."
|
||||
break
|
||||
default:
|
||||
errorMsg = savedDNSSettings.error.data.message ? savedDNSSettings.error.data.message : errorMsg
|
||||
break
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: createKey,
|
||||
duration: 5,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(dnsSettingsActions.setSavedDNSSettings({...savedDNSSettings, error: null}));
|
||||
dispatch(nsGroupActions.resetSavedNameServerGroup(null))
|
||||
payload: dnsSettingsToSave,
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
let msg = "please check the fields and try again";
|
||||
if (errorInfo.errorFields) {
|
||||
msg = errorInfo.errorFields[0].errors[0];
|
||||
}
|
||||
}, [savedDNSSettings])
|
||||
message.error({
|
||||
content: msg,
|
||||
duration: 1,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
let dnsSettingsToSave = createDNSSettingsToSave(values)
|
||||
dispatch(dnsSettingsActions.saveDNSSettings.request({
|
||||
getAccessTokenSilently:getTokenSilently,
|
||||
payload: dnsSettingsToSave
|
||||
}))
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
let msg = "please check the fields and try again"
|
||||
if (errorInfo.errorFields) {
|
||||
msg = errorInfo.errorFields[0].errors[0]
|
||||
}
|
||||
message.error({
|
||||
content: msg,
|
||||
duration: 1,
|
||||
});
|
||||
});
|
||||
}
|
||||
const createDNSSettingsToSave = (values: DNSSettings): DNSSettingsToSave => {
|
||||
let [existingGroups, newGroups] = getExistingAndToCreateGroupsLists(
|
||||
values.disabled_management_groups
|
||||
);
|
||||
return {
|
||||
disabled_management_groups: existingGroups,
|
||||
groupsToCreate: newGroups,
|
||||
} as DNSSettingsToSave;
|
||||
};
|
||||
|
||||
const createDNSSettingsToSave = (values: DNSSettings): DNSSettingsToSave => {
|
||||
let [existingGroups, newGroups] = getExistingAndToCreateGroupsLists(values.disabled_management_groups)
|
||||
return {
|
||||
disabled_management_groups: existingGroups,
|
||||
groupsToCreate: newGroups
|
||||
} as DNSSettingsToSave
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paragraph>Manage your account's DNS settings</Paragraph>
|
||||
<Col>
|
||||
<Form
|
||||
name="basic"
|
||||
autoComplete="off"
|
||||
form={form}
|
||||
onFinish={handleFormSubmit}
|
||||
>
|
||||
<Space direction={"vertical"}
|
||||
style={{ display: 'flex' }}>
|
||||
<Card
|
||||
title="DNS Management"
|
||||
loading={loading}
|
||||
>
|
||||
<Form.Item
|
||||
label="Disable DNS management for these groups"
|
||||
name="disabled_management_groups"
|
||||
tooltip="Peers in these groups will have their DNS management disabled and require manual configuration for domain name resolution"
|
||||
rules={[{validator: selectValidatorEmptyStrings}]}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{width: '100%'}}
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
<Select.Option key={m}>{optionRender(m)}</Select.Option>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
<Form.Item style={{ textAlign:'center' }} >
|
||||
<Button type="primary" htmlType="submit">
|
||||
Save
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form>
|
||||
return (
|
||||
<>
|
||||
<Paragraph>Manage your account's DNS settings</Paragraph>
|
||||
<Col>
|
||||
<Form
|
||||
name="basic"
|
||||
autoComplete="off"
|
||||
form={form}
|
||||
onFinish={handleFormSubmit}
|
||||
>
|
||||
<Space direction={"vertical"} style={{ display: "flex" }}>
|
||||
<Card loading={loading}>
|
||||
<div
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontWeight: "500",
|
||||
fontSize: "22px",
|
||||
marginBottom: "20px",
|
||||
}}
|
||||
>
|
||||
DNS Management
|
||||
</div>
|
||||
<Row>
|
||||
<Col span={10}>
|
||||
<label
|
||||
style={{
|
||||
color: "rgba(0, 0, 0, 0.88)",
|
||||
fontSize: "14px",
|
||||
fontWeight: "500",
|
||||
}}
|
||||
>
|
||||
Disable DNS management for these groups
|
||||
</label>
|
||||
<Paragraph
|
||||
type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-2",
|
||||
fontWeight: "400",
|
||||
marginBottom: "5px",
|
||||
}}
|
||||
>
|
||||
Peers in these groups will require manual domain name
|
||||
resolution
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={8}>
|
||||
<Form.Item
|
||||
name="disabled_management_groups"
|
||||
rules={[{ validator: selectValidatorEmptyStrings }]}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
tagRender={blueTagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
optionFilterProp="serchValue"
|
||||
>
|
||||
{tagGroups.map((m, index) => (
|
||||
<Option key={index} value={m.id} serchValue={m.name}>
|
||||
{optionRender(m.name, m.id)}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item style={{ marginBottom: "0" }}>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Save
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Space>
|
||||
</Form>
|
||||
</Col>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DNSSettingsForm;
|
||||
export default DNSSettingsForm;
|
||||
|
||||