mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a3fc271da |
No files matched your search
@@ -98,24 +98,6 @@ jobs:
|
||||
type=ref,event=pr
|
||||
type=sha
|
||||
|
||||
- name: Docker meta (rootless)
|
||||
id: meta_rootless
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.DOCKERHUB_IMAGE }}
|
||||
${{ env.GHCR_IMAGE }}
|
||||
flavor: |
|
||||
latest=false
|
||||
suffix=-rootless,onlatest=true
|
||||
tags: |
|
||||
type=schedule
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
type=ref,event=pr
|
||||
type=sha
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: docker/login-action@v3
|
||||
@@ -141,18 +123,7 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Build and push rootless Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile.rootless
|
||||
push: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm
|
||||
tags: ${{ steps.meta_rootless.outputs.tags }}
|
||||
labels: ${{ steps.meta_rootless.outputs.labels }}
|
||||
|
||||
- if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
run: |
|
||||
echo '### Pushed tags' >> $GITHUB_STEP_SUMMARY
|
||||
echo '${{ steps.meta.outputs.tags }}' >> $GITHUB_STEP_SUMMARY
|
||||
echo '${{ steps.meta_rootless.outputs.tags }}' >> $GITHUB_STEP_SUMMARY
|
||||
@@ -11,7 +11,6 @@
|
||||
# ./build.sh # build + export netbird-dashboard.tar.gz
|
||||
# IMAGE_TAG=v1 ./build.sh # override the image tag
|
||||
# SKIP_BUILD=1 ./build.sh # reuse an existing out/ (skip npm build)
|
||||
# ROOTLESS=1 ./build.sh # build the amd64-rootless image variant
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
@@ -20,26 +19,8 @@ cd "$(dirname "$0")"
|
||||
|
||||
IMAGE_NAME="${IMAGE_NAME:-netbird-dashboard}"
|
||||
IMAGE_TAG="${IMAGE_TAG:-amd64}"
|
||||
DOCKERFILE="docker/Dockerfile"
|
||||
CONTAINER_PORT=80
|
||||
OUTPUT_SUFFIX=""
|
||||
|
||||
case "${ROOTLESS:-0}" in
|
||||
0) ;;
|
||||
1)
|
||||
DOCKERFILE="docker/Dockerfile.rootless"
|
||||
CONTAINER_PORT=8080
|
||||
OUTPUT_SUFFIX="-rootless"
|
||||
[[ "${IMAGE_TAG}" == *-rootless ]] || IMAGE_TAG="${IMAGE_TAG}-rootless"
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: ROOTLESS must be 0 or 1." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
IMAGE="${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
OUTPUT="${OUTPUT:-${IMAGE_NAME}${OUTPUT_SUFFIX}.tar.gz}"
|
||||
OUTPUT="${OUTPUT:-${IMAGE_NAME}.tar.gz}"
|
||||
|
||||
# Build the static export (out/) unless told to reuse an existing one. Next 16
|
||||
# no longer runs ESLint during `next build`; types are checked by the build via
|
||||
@@ -58,10 +39,17 @@ if [[ ! -d out ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The .dockerignore lives under docker/, but Docker reads it from the build
|
||||
# context root. Stage it for the duration of the build so node_modules/.next
|
||||
# don't get shipped into the build context.
|
||||
cleanup() { rm -f .dockerignore; }
|
||||
trap cleanup EXIT
|
||||
cp docker/.dockerignore .dockerignore
|
||||
|
||||
echo "==> Building ${IMAGE} for linux/amd64"
|
||||
docker build \
|
||||
--platform linux/amd64 \
|
||||
-f "${DOCKERFILE}" \
|
||||
-f docker/Dockerfile \
|
||||
-t "${IMAGE}" \
|
||||
.
|
||||
|
||||
@@ -72,13 +60,13 @@ echo "==> Done: ${OUTPUT} ($(du -h "${OUTPUT}" | cut -f1))"
|
||||
echo
|
||||
echo "Copy and run on the server:"
|
||||
echo " scp ${OUTPUT} user@your-server:/tmp/"
|
||||
echo " docker load < /tmp/${OUTPUT##*/}"
|
||||
echo " docker load < /tmp/${OUTPUT}"
|
||||
echo
|
||||
echo "Run (nginx serves on container port ${CONTAINER_PORT}). Set the runtime env vars the"
|
||||
echo "Run (nginx serves on container port 80). Set the runtime env vars the"
|
||||
echo "dashboard templates at startup — for the Agent Network onboarding test,"
|
||||
echo "flip NETBIRD_AGENT_NETWORK_ONLY=true:"
|
||||
echo " docker run -d --name ${IMAGE_NAME} --restart unless-stopped \\"
|
||||
echo " -p 8080:${CONTAINER_PORT} \\"
|
||||
echo " -p 8080:80 \\"
|
||||
echo " -e NETBIRD_MGMT_API_ENDPOINT=https://your-mgmt:443 \\"
|
||||
echo " -e AUTH_AUTHORITY=https://your-idp/realms/netbird \\"
|
||||
echo " -e AUTH_CLIENT_ID=netbird-dashboard \\"
|
||||
|
||||
File renamed without changes.
@@ -1,29 +0,0 @@
|
||||
FROM alpine:3.24
|
||||
|
||||
RUN apk add --no-cache bash ca-certificates gettext nginx tzdata && \
|
||||
rm -rf /var/cache/apk/* && \
|
||||
mkdir -p /tmp/nginx
|
||||
|
||||
STOPSIGNAL SIGINT
|
||||
EXPOSE 8080
|
||||
|
||||
WORKDIR /usr/share/nginx/html
|
||||
|
||||
COPY docker/default.conf /etc/nginx/http.d/default.conf
|
||||
COPY docker/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY docker/rootless.conf /etc/nginx/http.d/rootless.conf
|
||||
COPY docker/init_react_envs.sh /usr/local/init_react_envs.sh
|
||||
COPY out/ /usr/share/nginx/html/
|
||||
|
||||
RUN sed -i \
|
||||
-e 's/listen 80 default_server;/listen 8080 default_server;/' \
|
||||
-e 's/listen \[::\]:80 default_server;/listen [::]:8080 default_server;/' \
|
||||
/etc/nginx/http.d/default.conf && \
|
||||
sed -i '/^user nginx;$/d' /etc/nginx/nginx.conf && \
|
||||
chmod +x /usr/local/init_react_envs.sh && \
|
||||
chgrp -R 0 /etc/nginx/http.d /usr/share/nginx/html /tmp/nginx && \
|
||||
chmod -R g=u /etc/nginx/http.d /usr/share/nginx/html /tmp/nginx
|
||||
|
||||
USER 10001:0
|
||||
ENTRYPOINT ["/usr/local/init_react_envs.sh"]
|
||||
CMD ["/usr/sbin/nginx", "-e", "/dev/stderr", "-g", "pid /tmp/nginx.pid;"]
|
||||
+10
-46
@@ -1,59 +1,23 @@
|
||||
# NetBird Dashboard
|
||||
|
||||
NetBird Dashboard is NetBird's management server UI.
|
||||
|
||||
NetBird Dashboard is NetBirds Management server UI. It allows users to signin, view setup keys and manage peers. This image is **not ready** for production use.
|
||||
## Tags
|
||||
```latest``` ```vX.X.X``` not available yet.
|
||||
|
||||
Each published image tag also has a rootless variant with the `-rootless`
|
||||
suffix. For example, `main` is also published as `main-rootless`.
|
||||
|
||||
```main``` built on every PR being merged to the repository
|
||||
## How to use this image
|
||||
|
||||
HTTP:
|
||||
|
||||
HTTP run:
|
||||
```shell
|
||||
docker run -d --rm -p 80:80 netbirdio/dashboard:main
|
||||
docker run -d --rm -p 80:80 wiretrustee/dashboard:main
|
||||
```
|
||||
|
||||
Using an SSL certificate from Let's Encrypt:
|
||||
|
||||
Using SSL certificate from Let's Encrypt®:
|
||||
```shell
|
||||
docker run -d --rm -p 80:80 -p 443:443 \
|
||||
-e LETSENCRYPT_DOMAIN=app.mydomain.com \
|
||||
-e LETSENCRYPT_EMAIL=hello@mydomain.com \
|
||||
netbirdio/dashboard:main
|
||||
```
|
||||
|
||||
For certificate generation, the server needs a public IP and a domain name
|
||||
pointing to it.
|
||||
|
||||
### Rootless and OpenShift
|
||||
|
||||
The rootless image listens on port `8080`, runs as UID `10001` by default, and
|
||||
supports OpenShift's arbitrary UID when the container retains GID `0`:
|
||||
|
||||
```shell
|
||||
docker run -d --rm \
|
||||
--user 12345:0 \
|
||||
-p 8080:8080 \
|
||||
-e AUTH_AUTHORITY=https://idp.example.com \
|
||||
-e AUTH_CLIENT_ID=netbird-dashboard \
|
||||
-e AUTH_AUDIENCE=netbird-dashboard \
|
||||
-e AUTH_SUPPORTED_SCOPES='openid profile email' \
|
||||
-e USE_AUTH0=false \
|
||||
-e NETBIRD_MGMT_API_ENDPOINT=https://api.example.com \
|
||||
netbirdio/dashboard:main-rootless
|
||||
```
|
||||
|
||||
The rootless image does not include Certbot or expose HTTPS. Terminate TLS at
|
||||
the OpenShift Route, Ingress, or another reverse proxy.
|
||||
|
||||
> For SSL generation, you need to run this image in a server with proper public IP and a domain name pointing to it.
|
||||
## Environment variables
|
||||
|
||||
- `NGINX_SSL_PORT` changes the port that Nginx listens to. Defaults to `443`.
|
||||
- `LETSENCRYPT_DOMAIN` enables Certbot for the specified domain. Defaults to
|
||||
`none`.
|
||||
- `LETSENCRYPT_EMAIL` is used to register the certificate request. Defaults to
|
||||
`example@local`.
|
||||
|
||||
These certificate variables apply only to the standard image.
|
||||
* ```NGINX_SSL_PORT``` Changes the port that Nginx listens to. Defaults to ```443```
|
||||
* ```LETSENCRYPT_DOMAIN``` Enables Certbot`s client execution for the specified domain. Defaults to ```none```
|
||||
* ```LETSENCRYPT_EMAIL``` Email used in Certbot`s client execution to register the certificate request. Defaults to ```example@local```
|
||||
@@ -173,12 +173,6 @@ for f in $(grep -R -l AUTH_SUPPORTED_SCOPES /usr/share/nginx/html); do
|
||||
rm "$f".copy
|
||||
done
|
||||
|
||||
# When a command is provided, initialization happens before the server starts.
|
||||
# The rootless image uses this path so nginx never needs to be reloaded.
|
||||
if (( $# > 0 )); then
|
||||
exec "$@"
|
||||
fi
|
||||
|
||||
# Reload nginx so the patched CSP header takes effect.
|
||||
# supervisord starts nginx (priority 100) before this script (priority 201) and
|
||||
# never reloads it, so without this nginx keeps serving the static default.conf
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# nginx includes this file inside the http context.
|
||||
# All runtime state stays in a directory writable by the arbitrary UID used by OpenShift.
|
||||
client_body_temp_path /tmp/nginx/client_temp;
|
||||
proxy_temp_path /tmp/nginx/proxy_temp;
|
||||
fastcgi_temp_path /tmp/nginx/fastcgi_temp;
|
||||
uwsgi_temp_path /tmp/nginx/uwsgi_temp;
|
||||
scgi_temp_path /tmp/nginx/scgi_temp;
|
||||
Generated
+66
-52
@@ -33,7 +33,7 @@
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/lodash": "4.17.24",
|
||||
"@types/node": "20.19.43",
|
||||
"@types/node": "20.10.6",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/react-window": "^1.8.8",
|
||||
@@ -64,6 +64,7 @@
|
||||
"punycode": "^2.3.1",
|
||||
"react": "^19.2.4",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-confetti-explosion": "^3.0.3",
|
||||
"react-day-picker": "^9.13.0",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-ga4": "^2.1.0",
|
||||
@@ -82,6 +83,7 @@
|
||||
"typescript": "^5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^9.5.1",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
@@ -848,6 +850,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@faker-js/faker": {
|
||||
"version": "9.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.9.0.tgz",
|
||||
"integrity": "sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fakerjs"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz",
|
||||
@@ -887,40 +906,27 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
||||
"integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@humanfs/types": "^0.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/node": {
|
||||
"version": "0.16.8",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
|
||||
"integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
|
||||
"version": "0.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
|
||||
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@humanfs/core": "^0.19.2",
|
||||
"@humanfs/types": "^0.15.0",
|
||||
"@humanfs/core": "^0.19.1",
|
||||
"@humanwhocodes/retry": "^0.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/types": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
|
||||
"integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/module-importer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
||||
@@ -3681,12 +3687,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.43",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
|
||||
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
|
||||
"version": "20.10.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz",
|
||||
"integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
@@ -4610,9 +4616,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.8",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
|
||||
"integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==",
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
|
||||
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -4629,11 +4635,11 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.11.12",
|
||||
"caniuse-lite": "^1.0.30001809",
|
||||
"electron-to-chromium": "^1.5.402",
|
||||
"node-releases": "^2.0.53",
|
||||
"update-browserslist-db": "^1.3.0"
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
"electron-to-chromium": "^1.5.263",
|
||||
"node-releases": "^2.0.27",
|
||||
"update-browserslist-db": "^1.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
@@ -5639,9 +5645,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.420",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz",
|
||||
"integrity": "sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==",
|
||||
"version": "1.5.302",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
|
||||
"integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/elkjs": {
|
||||
@@ -8424,13 +8430,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.54",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz",
|
||||
"integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
"version": "2.0.27",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
||||
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
@@ -8779,6 +8782,7 @@
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -8937,9 +8941,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz",
|
||||
"integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==",
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
@@ -9074,6 +9078,16 @@
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-confetti-explosion": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-confetti-explosion/-/react-confetti-explosion-3.0.3.tgz",
|
||||
"integrity": "sha512-ow5ns/1ttzXsIlbbfJmWJNiyQK8lTHBL6lRSUXGaK44K/3NIMngR57Ja96l+D6txTeFhfe0BfXGvORMxhtRDng==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-day-picker": {
|
||||
"version": "9.13.2",
|
||||
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.13.2.tgz",
|
||||
@@ -10494,9 +10508,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unrs-resolver": {
|
||||
@@ -10535,9 +10549,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz",
|
||||
"integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==",
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
||||
+3
-7
@@ -48,7 +48,7 @@
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/lodash": "4.17.24",
|
||||
"@types/node": "20.19.43",
|
||||
"@types/node": "20.10.6",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/react-window": "^1.8.8",
|
||||
@@ -79,6 +79,7 @@
|
||||
"punycode": "^2.3.1",
|
||||
"react": "^19.2.4",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-confetti-explosion": "^3.0.3",
|
||||
"react-day-picker": "^9.13.0",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-ga4": "^2.1.0",
|
||||
@@ -100,6 +101,7 @@
|
||||
"minimatch": ">=10.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^9.5.1",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
@@ -112,11 +114,5 @@
|
||||
"prettier": "3.0.3",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"allowScripts": {
|
||||
"@axa-fr/oidc-client": false,
|
||||
"@axa-fr/react-oidc": false,
|
||||
"fsevents": false,
|
||||
"unrs-resolver": false
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,7 @@ import ReverseProxiesProvider, {
|
||||
import { ReverseProxyFlatTargetsTabContent } from "@/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetsTabContent";
|
||||
import { PeerEditIPModal } from "@/modules/peer/PeerEditIPModal";
|
||||
import { PeerSSHToggle } from "@/modules/peer/PeerSSHToggle";
|
||||
import { isSSHSupportedOnOS } from "@/modules/remote-access/osSupport";
|
||||
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
|
||||
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
|
||||
import { PeerExpirationSettings } from "@/modules/peer/PeerExpirationSettings";
|
||||
@@ -430,6 +431,7 @@ const PeerOverviewTabContent = () => {
|
||||
const { peer } = usePeer();
|
||||
const { permission } = usePermissions();
|
||||
const { selectedGroups, setSelectedGroups } = usePeerSettings();
|
||||
const isSSHSupported = isSSHSupportedOnOS(peer?.os);
|
||||
|
||||
return (
|
||||
<div className={"px-8"}>
|
||||
@@ -458,11 +460,14 @@ const PeerOverviewTabContent = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<PeerSSHToggle />
|
||||
{isSSHSupported && <PeerSSHToggle />}
|
||||
|
||||
<div>
|
||||
<Label>Remote Access</Label>
|
||||
<HelpText>Connect directly to this peer via SSH or RDP.</HelpText>
|
||||
<HelpText>
|
||||
Connect directly to this peer via{" "}
|
||||
{isSSHSupported ? "SSH or RDP" : "RDP"}.
|
||||
</HelpText>
|
||||
<div className="flex gap-3">
|
||||
<SSHButton peer={peer} />
|
||||
<RDPButton peer={peer} />
|
||||
|
||||
@@ -61,10 +61,7 @@ export default function NetBirdSettings() {
|
||||
<VerticalTabs.List>
|
||||
{permission.settings.read && (
|
||||
<>
|
||||
<VerticalTabs.Trigger
|
||||
value="authentication"
|
||||
data-testid="settings-tab-authentication"
|
||||
>
|
||||
<VerticalTabs.Trigger value="authentication" data-testid="settings-tab-authentication">
|
||||
<ShieldIcon size={14} />
|
||||
Authentication
|
||||
</VerticalTabs.Trigger>
|
||||
@@ -81,31 +78,19 @@ export default function NetBirdSettings() {
|
||||
Identity Providers
|
||||
</VerticalTabs.Trigger>
|
||||
)}
|
||||
<VerticalTabs.Trigger
|
||||
value="groups"
|
||||
data-testid="settings-tab-groups"
|
||||
>
|
||||
<VerticalTabs.Trigger value="groups" data-testid="settings-tab-groups">
|
||||
<FolderGit2Icon size={14} />
|
||||
Groups
|
||||
</VerticalTabs.Trigger>
|
||||
<VerticalTabs.Trigger
|
||||
value="permissions"
|
||||
data-testid="settings-tab-permissions"
|
||||
>
|
||||
<VerticalTabs.Trigger value="permissions" data-testid="settings-tab-permissions">
|
||||
<LockIcon size={14} />
|
||||
Permissions
|
||||
</VerticalTabs.Trigger>
|
||||
<VerticalTabs.Trigger
|
||||
value="networks"
|
||||
data-testid="settings-tab-networks"
|
||||
>
|
||||
<VerticalTabs.Trigger value="networks" data-testid="settings-tab-networks">
|
||||
<NetworkIcon size={14} />
|
||||
Networks
|
||||
</VerticalTabs.Trigger>
|
||||
<VerticalTabs.Trigger
|
||||
value="clients"
|
||||
data-testid="settings-tab-clients"
|
||||
>
|
||||
<VerticalTabs.Trigger value="clients" data-testid="settings-tab-clients">
|
||||
<MonitorSmartphoneIcon size={14} />
|
||||
Clients
|
||||
</VerticalTabs.Trigger>
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
useNetBirdClient,
|
||||
} from "@/modules/remote-access/useNetBirdClient";
|
||||
import {
|
||||
isNativeSSHSupported,
|
||||
usesStandardSSHPort,
|
||||
isNetbirdSSHProtocolSupported,
|
||||
} from "@utils/version";
|
||||
|
||||
@@ -95,7 +95,7 @@ function SSHTerminal({ username, port, peer, ipVersion }: Props) {
|
||||
if (isSSHConnected || isSSHConnecting) return;
|
||||
connected.current = false;
|
||||
try {
|
||||
const aclPort = isNativeSSHSupported(peer.version) ? "22022" : port;
|
||||
const aclPort = usesStandardSSHPort(peer.version) ? "22022" : port;
|
||||
const protocol = isNetbirdSSHProtocolSupported(peer.version)
|
||||
? "netbird-ssh"
|
||||
: "tcp";
|
||||
@@ -122,7 +122,7 @@ function SSHTerminal({ username, port, peer, ipVersion }: Props) {
|
||||
connected.current = true;
|
||||
|
||||
try {
|
||||
const aclPort = isNativeSSHSupported(peer.version) ? "22022" : port;
|
||||
const aclPort = usesStandardSSHPort(peer.version) ? "22022" : port;
|
||||
const protocol = isNetbirdSSHProtocolSupported(peer.version)
|
||||
? "netbird-ssh"
|
||||
: "tcp";
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 290 KiB |
@@ -1,7 +1,7 @@
|
||||
import { useOidcAccessToken, useOidcIdToken } from "@axa-fr/react-oidc";
|
||||
import loadConfig from "@utils/config";
|
||||
import { useMemo } from "react";
|
||||
import { decodeToken } from "react-jwt";
|
||||
import { useJwt } from "react-jwt";
|
||||
|
||||
const config = loadConfig();
|
||||
export const useDomainCategory = () => {
|
||||
@@ -9,12 +9,10 @@ export const useDomainCategory = () => {
|
||||
const { idToken } = useOidcIdToken();
|
||||
const { accessToken } = useOidcAccessToken();
|
||||
const token = tokenSource.toLowerCase() == "idtoken" ? idToken : accessToken;
|
||||
const { decodedToken } = useJwt<Record<any, any>>(token);
|
||||
|
||||
const domainCategory = useMemo(() => {
|
||||
try {
|
||||
const decodedToken = token
|
||||
? decodeToken<Record<any, any>>(token)
|
||||
: undefined;
|
||||
const key = decodedToken
|
||||
? Object.keys(decodedToken)
|
||||
.filter((key) => key.includes("wt_account_domain_category"))
|
||||
@@ -24,7 +22,7 @@ export const useDomainCategory = () => {
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}, [token]);
|
||||
}, [decodedToken]);
|
||||
|
||||
const isPrivate = useMemo(() => {
|
||||
return domainCategory === "private";
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import Card from "@components/Card";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import { SkeletonNotificationSettings } from "@components/skeletons/SkeletonNotificationSettings";
|
||||
import { SmallBadge } from "@components/ui/SmallBadge";
|
||||
import { VerticalTabs } from "@components/VerticalTabs";
|
||||
import * as Tabs from "@radix-ui/react-tabs";
|
||||
import { ExternalLinkIcon, MessageSquareDot } from "lucide-react";
|
||||
import React from "react";
|
||||
import SettingsIcon from "@/assets/icons/SettingsIcon";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { useIsLicensed } from "@/hooks/useIsLicensed";
|
||||
import { VerticalTabs } from "@components/VerticalTabs";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Card from "@components/Card";
|
||||
import { NotificationEmailChannel } from "@/cloud/notifications/channels/NotificationEmailChannel";
|
||||
import { NotificationSlackChannel } from "@/cloud/notifications/channels/NotificationSlackChannel";
|
||||
import { NotificationWebhookChannel } from "@/cloud/notifications/channels/NotificationWebhookChannel";
|
||||
import { NotificationChannelListItem } from "@/cloud/notifications/NotificationChannelListItem";
|
||||
import useUrlTab from "@/hooks/useUrlTab";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import NotificationProvider, {
|
||||
useNotifications,
|
||||
} from "@/cloud/notifications/NotificationProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { useIsLicensed } from "@/hooks/useIsLicensed";
|
||||
import useUrlTab from "@/hooks/useUrlTab";
|
||||
import { NotificationChannelListItem } from "@/cloud/notifications/NotificationChannelListItem";
|
||||
import {
|
||||
NOTIFICATION_CHANNELS_DOCS_LINK,
|
||||
NotificationChannelType,
|
||||
} from "@/interfaces/NotificationChannel";
|
||||
import { SkeletonNotificationSettings } from "@components/skeletons/SkeletonNotificationSettings";
|
||||
import { NotificationSlackChannel } from "@/cloud/notifications/channels/NotificationSlackChannel";
|
||||
import { SmallBadge } from "@components/ui/SmallBadge";
|
||||
|
||||
const NotificationsOverview = ({
|
||||
onSelectChannel,
|
||||
@@ -52,18 +52,23 @@ const NotificationsOverview = ({
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<div className={"flex items-start justify-between -mt-1"}>
|
||||
<div className={"flex items-start justify-between"}>
|
||||
<div>
|
||||
<h1>Notifications</h1>
|
||||
<Paragraph className={"block"}>
|
||||
Get notified when important events happen.{" "}
|
||||
<Paragraph>
|
||||
Choose how to be notified when important events occur in your
|
||||
account.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Learn more about{" "}
|
||||
<InlineLink
|
||||
href={NOTIFICATION_CHANNELS_DOCS_LINK}
|
||||
target={"_blank"}
|
||||
>
|
||||
Learn more
|
||||
Notification Channels
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
import Button from "@components/Button";
|
||||
import { Input } from "@components/Input";
|
||||
import {
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
} from "@components/modal/Modal";
|
||||
import ModalHeader from "@components/modal/ModalHeader";
|
||||
import { notify } from "@components/Notification";
|
||||
import { validator } from "@utils/helpers";
|
||||
import { PlusCircle } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useSignInDomains } from "@/cloud/sign-in-domains/useSignInDomains";
|
||||
import { DomainValidationStatus, SignInDomain } from "@/interfaces/Account";
|
||||
import { DomainVerificationModal } from "@/modules/integrations/sso/DomainVerificationModal";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export const AddSignInDomainModal = ({ open, onOpenChange }: Props) => {
|
||||
const { addDomain, verifyDomain, mutate } = useSignInDomains();
|
||||
const [name, setName] = useState("");
|
||||
const [verifyModal, setVerifyModal] = useState(false);
|
||||
const [addedDomain, setAddedDomain] = useState<SignInDomain>();
|
||||
|
||||
const validationError = useMemo(() => {
|
||||
const domain = name.trim();
|
||||
if (domain === "") return "";
|
||||
const valid = validator.isValidDomain(domain, {
|
||||
allowWildcard: false,
|
||||
allowOnlyTld: false,
|
||||
preventLeadingAndTrailingDots: true,
|
||||
});
|
||||
if (!valid) return "Please enter a valid domain, e.g. company.com";
|
||||
return "";
|
||||
}, [name]);
|
||||
|
||||
const setOpen = (next: boolean) => {
|
||||
if (!next) setName("");
|
||||
onOpenChange(next);
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
const domain = name.trim();
|
||||
if (!domain || validationError) return;
|
||||
|
||||
setOpen(false);
|
||||
|
||||
notify({
|
||||
title: "Sign-in Domains",
|
||||
description: `${domain} has been added`,
|
||||
promise: addDomain(domain).then((added) => {
|
||||
if (
|
||||
added &&
|
||||
added.validation_status !== DomainValidationStatus.VERIFIED
|
||||
) {
|
||||
setAddedDomain(added);
|
||||
setVerifyModal(true);
|
||||
}
|
||||
mutate().catch(() => {});
|
||||
return added;
|
||||
}),
|
||||
loadingMessage: "Adding domain...",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{addedDomain && (
|
||||
<DomainVerificationModal
|
||||
open={verifyModal}
|
||||
onOpenChange={setVerifyModal}
|
||||
domain={addedDomain.name}
|
||||
token={addedDomain.validation_token}
|
||||
onVerify={() =>
|
||||
verifyDomain(addedDomain.id).then((res) => {
|
||||
mutate().catch(() => {});
|
||||
return res;
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Modal open={open} onOpenChange={setOpen}>
|
||||
<ModalContent maxWidthClass={"max-w-md"}>
|
||||
<ModalHeader
|
||||
className={"pb-4 px-8"}
|
||||
title={"Add Sign-in Domain"}
|
||||
description={"Users on this domain are matched to your account."}
|
||||
/>
|
||||
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
submit();
|
||||
}}
|
||||
>
|
||||
<div className={"p-default flex flex-col gap-4"}>
|
||||
<div>
|
||||
<Input
|
||||
autoFocus
|
||||
data-testid={"add-domain-input"}
|
||||
placeholder={"e.g. company.com"}
|
||||
value={name}
|
||||
error={validationError}
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModalFooter className={"items-center"} separator={false}>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"} className={"w-full"}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"w-full"}
|
||||
type={"submit"}
|
||||
disabled={!name.trim() || !!validationError}
|
||||
data-testid={"add-domain-submit"}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
import Button from "@components/Button";
|
||||
import HelpText from "@components/HelpText";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import { Label } from "@components/Label";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { isNetBirdCloud } from "@utils/netbird";
|
||||
import { ExternalLinkIcon, PlusCircle } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { useDomainCategory } from "@/cloud/cloud-hooks/useDomainCategory";
|
||||
import { AddSignInDomainModal } from "@/cloud/sign-in-domains/AddSignInDomainModal";
|
||||
import SignInDomainsTable from "@/cloud/sign-in-domains/table/SignInDomainsTable";
|
||||
import { useSignInDomains } from "@/cloud/sign-in-domains/useSignInDomains";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
|
||||
const POLL_INTERVAL_MS = 15_000;
|
||||
|
||||
const SIGN_IN_DOMAINS_DOCS_LINK =
|
||||
"https://docs.netbird.io/manage/team/sign-in-domains";
|
||||
|
||||
export const useCanViewSignInDomains = () => {
|
||||
const { permission } = usePermissions();
|
||||
const { isPrivate } = useDomainCategory();
|
||||
return Boolean(permission?.settings?.read) && isNetBirdCloud() && isPrivate;
|
||||
};
|
||||
|
||||
export const SignInDomainsSettings = () => {
|
||||
const { permission } = usePermissions();
|
||||
const { domains, isLoading, isUnavailable } =
|
||||
useSignInDomains(POLL_INTERVAL_MS);
|
||||
const [addModal, setAddModal] = useState(false);
|
||||
|
||||
const showSkeleton = isLoading && !isUnavailable;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
isLoading && "animate-pulse pointer-events-none opacity-90",
|
||||
isUnavailable && "pointer-events-none opacity-70",
|
||||
)}
|
||||
data-testid={"sign-in-domains"}
|
||||
>
|
||||
<div className={"flex items-end justify-between gap-4 mb-4"}>
|
||||
<div className={"min-w-0"}>
|
||||
<Label>Sign-in Domains</Label>
|
||||
<HelpText className={"!mb-0"}>
|
||||
Users from these domains can join your account.{" "}
|
||||
<InlineLink href={SIGN_IN_DOMAINS_DOCS_LINK} target={"_blank"}>
|
||||
Learn more
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</HelpText>
|
||||
</div>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
size={"xs"}
|
||||
className={"shrink-0"}
|
||||
disabled={!permission?.settings?.update || isUnavailable}
|
||||
onClick={() => setAddModal(true)}
|
||||
data-testid={"add-domain"}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<SignInDomainsTable domains={domains} isLoading={showSkeleton} />
|
||||
|
||||
<AddSignInDomainModal open={addModal} onOpenChange={setAddModal} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,121 +0,0 @@
|
||||
import Button from "@components/Button";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { notify } from "@components/Notification";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { useSignInDomains } from "@/cloud/sign-in-domains/useSignInDomains";
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { DomainValidationStatus, SignInDomain } from "@/interfaces/Account";
|
||||
import { DomainVerificationModal } from "@/modules/integrations/sso/DomainVerificationModal";
|
||||
|
||||
type Props = {
|
||||
domain: SignInDomain;
|
||||
};
|
||||
|
||||
export default function DomainActionCell({ domain }: Readonly<Props>) {
|
||||
const { permission } = usePermissions();
|
||||
const { verifyDomain, deleteDomain, mutate } = useSignInDomains();
|
||||
const { confirm } = useDialog();
|
||||
const [modal, setModal] = useState(false);
|
||||
|
||||
const isVerified =
|
||||
domain.validation_status === DomainValidationStatus.VERIFIED;
|
||||
const connections = domain.connections ?? [];
|
||||
|
||||
const removeBlockedReason: React.ReactNode = domain.is_primary ? (
|
||||
<>
|
||||
This is the primary domain of your account
|
||||
<br />
|
||||
and cannot be removed.
|
||||
</>
|
||||
) : connections.length > 0 ? (
|
||||
<>
|
||||
This domain is used by an SSO integration.
|
||||
<br />
|
||||
Remove it there before deleting it.
|
||||
</>
|
||||
) : undefined;
|
||||
|
||||
const deleteDomainHandler = async () => {
|
||||
const choice = await confirm({
|
||||
title: `Remove ${domain.name}?`,
|
||||
description:
|
||||
"Users with an email address on this domain will no longer be matched to your account.",
|
||||
confirmText: "Remove",
|
||||
cancelText: "Cancel",
|
||||
type: "danger",
|
||||
maxWidthClass: "max-w-md",
|
||||
});
|
||||
if (!choice) return;
|
||||
notify({
|
||||
title: "Sign-in Domains",
|
||||
description: `${domain.name} has been removed`,
|
||||
promise: deleteDomain(domain.id).then((res) => {
|
||||
mutate().catch(() => {});
|
||||
return res;
|
||||
}),
|
||||
loadingMessage: "Removing domain...",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"relative top-[2px] flex gap-2 items-center justify-end ml-auto min-h-[34px]"
|
||||
}
|
||||
>
|
||||
<DomainVerificationModal
|
||||
open={modal}
|
||||
onOpenChange={setModal}
|
||||
domain={domain.name}
|
||||
token={domain.validation_token}
|
||||
onVerify={() =>
|
||||
verifyDomain(domain.id).then((res) => {
|
||||
mutate().catch(() => {});
|
||||
return res;
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
{!isVerified && (
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
size={"xs"}
|
||||
className={"!px-3"}
|
||||
disabled={!permission.settings.update}
|
||||
onClick={() => setModal(true)}
|
||||
data-testid={"verify-domain"}
|
||||
>
|
||||
Verify
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"text-xs max-w-xs"}>
|
||||
{removeBlockedReason ?? "Remove domain"}
|
||||
</div>
|
||||
}
|
||||
interactive={false}
|
||||
>
|
||||
<Button
|
||||
variant={"danger-outline"}
|
||||
size={"xs"}
|
||||
className={cn(
|
||||
"!p-0 !h-[34px] !w-[34px]",
|
||||
removeBlockedReason && "pointer-events-none",
|
||||
)}
|
||||
disabled={!!removeBlockedReason || !permission.settings.update}
|
||||
onClick={deleteDomainHandler}
|
||||
aria-label={"Remove domain"}
|
||||
data-testid={"remove-domain"}
|
||||
>
|
||||
<TrashIcon size={14} />
|
||||
</Button>
|
||||
</FullTooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { cn } from "@utils/helpers";
|
||||
import * as React from "react";
|
||||
import { DomainValidationStatus, SignInDomain } from "@/interfaces/Account";
|
||||
|
||||
type Props = {
|
||||
domain: SignInDomain;
|
||||
};
|
||||
|
||||
const FAILED_HINT =
|
||||
"This domain could not be verified. Check that the TXT record is published, then try again. If it keeps failing, contact support@netbird.io";
|
||||
|
||||
export default function DomainStatusCell({ domain }: Readonly<Props>) {
|
||||
const status = domain.validation_status;
|
||||
const isVerified = status === DomainValidationStatus.VERIFIED;
|
||||
const isPending = status === DomainValidationStatus.PENDING;
|
||||
const isFailed = status === DomainValidationStatus.FAILED;
|
||||
|
||||
return (
|
||||
<FullTooltip
|
||||
content={<div className={"text-xs max-w-xs"}>{FAILED_HINT}</div>}
|
||||
disabled={!isFailed}
|
||||
interactive={false}
|
||||
>
|
||||
<div
|
||||
className={"flex items-center gap-2 text-sm text-nb-gray-300 mr-auto"}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"h-2 w-2 rounded-full shrink-0",
|
||||
isVerified && "bg-green-400",
|
||||
isPending && "bg-yellow-400",
|
||||
!isVerified && !isPending && "bg-red-500",
|
||||
)}
|
||||
/>
|
||||
{statusLabel(status)}
|
||||
</div>
|
||||
</FullTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function statusLabel(status: DomainValidationStatus) {
|
||||
if (status === DomainValidationStatus.VERIFIED) return "Verified";
|
||||
if (status === DomainValidationStatus.PENDING) return "Pending";
|
||||
return "Unverified";
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import Card from "@components/Card";
|
||||
import { DataTable } from "@components/table/DataTable";
|
||||
import DataTableHeader from "@components/table/DataTableHeader";
|
||||
import NoResults from "@components/ui/NoResults";
|
||||
import { ColumnDef, SortingState } from "@tanstack/react-table";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
import DomainActionCell from "@/cloud/sign-in-domains/table/DomainActionCell";
|
||||
import DomainStatusCell from "@/cloud/sign-in-domains/table/DomainStatusCell";
|
||||
import { DomainValidationStatus, SignInDomain } from "@/interfaces/Account";
|
||||
|
||||
type Props = {
|
||||
domains?: SignInDomain[];
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
const PLACEHOLDER_ID = "__loading__";
|
||||
|
||||
const placeholderRow: SignInDomain = {
|
||||
id: PLACEHOLDER_ID,
|
||||
name: "",
|
||||
is_primary: false,
|
||||
validation_token: "",
|
||||
validation_status: DomainValidationStatus.PENDING,
|
||||
validation_last_updated: null,
|
||||
};
|
||||
|
||||
const isPlaceholder = (domain: SignInDomain) => domain.id === PLACEHOLDER_ID;
|
||||
|
||||
const SignInDomainsColumns: ColumnDef<SignInDomain>[] = [
|
||||
{
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Domain</DataTableHeader>;
|
||||
},
|
||||
accessorKey: "name",
|
||||
sortingFn: "text",
|
||||
cell: ({ row }) =>
|
||||
isPlaceholder(row.original) ? (
|
||||
<Skeleton height={10} width={"60%"} />
|
||||
) : (
|
||||
<div className={"text-sm text-nb-gray-100"}>{row.original.name}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Status</DataTableHeader>;
|
||||
},
|
||||
accessorKey: "validation_status",
|
||||
sortingFn: "text",
|
||||
cell: ({ row }) =>
|
||||
isPlaceholder(row.original) ? (
|
||||
<Skeleton height={10} width={"50%"} />
|
||||
) : (
|
||||
<DomainStatusCell domain={row.original} />
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: () => null,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) =>
|
||||
isPlaceholder(row.original) ? (
|
||||
<div className={"flex items-center justify-end ml-auto min-h-[34px]"} />
|
||||
) : (
|
||||
<DomainActionCell domain={row.original} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export default function SignInDomainsTable({
|
||||
domains,
|
||||
isLoading,
|
||||
}: Readonly<Props>) {
|
||||
const [sorting, setSorting] = useState<SortingState>([
|
||||
{
|
||||
id: "name",
|
||||
desc: false,
|
||||
},
|
||||
]);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
wrapperComponent={Card}
|
||||
wrapperProps={{ className: "w-full pb-1" }}
|
||||
useRowId={true}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
minimal={true}
|
||||
showSearchAndFilters={false}
|
||||
inset={false}
|
||||
tableClassName={"mt-0"}
|
||||
text={"Domains"}
|
||||
columns={SignInDomainsColumns}
|
||||
keepStateInLocalStorage={false}
|
||||
data={isLoading ? [placeholderRow] : domains}
|
||||
getStartedCard={
|
||||
<NoResults
|
||||
className={"py-4"}
|
||||
hideIcon
|
||||
title={"No sign-in domains yet"}
|
||||
description={
|
||||
"Add a domain, then verify ownership with a DNS record to start matching users to this account."
|
||||
}
|
||||
/>
|
||||
}
|
||||
paginationPaddingClassName={"px-0 pt-8"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Loaded 20 of 71 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user