Compare commits
91 Commits
v2.19.1
...
backup/pre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
635603d62b | ||
|
|
56c0ad9592 | ||
|
|
14d3fec84a | ||
|
|
056e5c867b | ||
|
|
b0421b64ac | ||
|
|
e8f0f20455 | ||
|
|
8c94090e3d | ||
|
|
1917df6f60 | ||
|
|
358b477ded | ||
|
|
f535fe2667 | ||
|
|
52f7020a0f | ||
|
|
a604643f9a | ||
|
|
42cd088c5d | ||
|
|
7400ac806e | ||
|
|
240ff5af9a | ||
|
|
dc86c30463 | ||
|
|
e58f75ae3c | ||
|
|
dc1adebd27 | ||
|
|
d76cbd1122 | ||
|
|
01330e0f58 | ||
|
|
e9ac1a1a23 | ||
|
|
b53802a5c5 | ||
|
|
9addc18956 | ||
|
|
9701e6503b | ||
|
|
0841caecbb | ||
|
|
c7846760d1 | ||
|
|
8c283b6ef9 | ||
|
|
34ae3b4da6 | ||
|
|
aff2365ef7 | ||
|
|
bad057d415 | ||
|
|
4d846e2c94 | ||
|
|
15fb6e0b05 | ||
|
|
55c5525626 | ||
|
|
c0c1f4688e | ||
|
|
b5a8f751ba | ||
|
|
10a8e7b745 | ||
|
|
60e8394010 | ||
|
|
9420214059 | ||
|
|
b949f60afe | ||
|
|
d498e4cc25 | ||
|
|
130dc0c32c | ||
|
|
f5824d6ddb | ||
|
|
829395f908 | ||
|
|
8eebec78b4 | ||
|
|
3e01a6dafd | ||
|
|
1555b94043 | ||
|
|
6c62127d42 | ||
|
|
b71d0fde89 | ||
|
|
84c239ce30 | ||
|
|
ba66201c64 | ||
|
|
c6341e000f | ||
|
|
750f660bcc | ||
|
|
ea148545e8 | ||
|
|
d2febbf27b | ||
|
|
615b4487ad | ||
|
|
a7c7800916 | ||
|
|
3d51e0893e | ||
|
|
d7d44b5817 | ||
|
|
f67f39b68b | ||
|
|
d2bc7a1f57 | ||
|
|
818ba5daa4 | ||
|
|
3a30f76629 | ||
|
|
34dc21c89d | ||
|
|
2e37703622 | ||
|
|
8aec338c43 | ||
|
|
f4f0c240fd | ||
|
|
04e22a3c7e | ||
|
|
54ef076303 | ||
|
|
92676b6c38 | ||
|
|
3affa8908f | ||
|
|
52fd984912 | ||
|
|
83e3159ee4 | ||
|
|
bf81aeb02d | ||
|
|
b058e66e32 | ||
|
|
8d6b617cbd | ||
|
|
47db655e9f | ||
|
|
0661cbf9f4 | ||
|
|
240a96fa8b | ||
|
|
43bc069a49 | ||
|
|
936de0f4f3 | ||
|
|
d81b75a946 | ||
|
|
a632eeeef0 | ||
|
|
e2219aeea0 | ||
|
|
63f4c69eb4 | ||
|
|
b1af256296 | ||
|
|
4027894a2e | ||
|
|
af90792595 | ||
|
|
9a401733b3 | ||
|
|
07b6895380 | ||
|
|
9e2e38764e | ||
|
|
d9fb379abf |
12
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
## Issue ticket number and link
|
||||
|
||||
## Documentation
|
||||
Select exactly one:
|
||||
|
||||
- [ ] I added/updated documentation for this change
|
||||
- [ ] Documentation is **not needed** for this change (explain why)
|
||||
|
||||
### Docs PR URL (required if "docs added" is checked)
|
||||
Paste the PR link from https://github.com/netbirdio/docs here:
|
||||
|
||||
https://github.com/netbirdio/docs/pull/__
|
||||
14
.github/workflows/build_and_push.yml
vendored
@@ -2,7 +2,6 @@ name: build and push
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "feature/**"
|
||||
- main
|
||||
tags:
|
||||
- "**"
|
||||
@@ -20,7 +19,7 @@ jobs:
|
||||
- name: setup-node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -55,8 +54,19 @@ jobs:
|
||||
fileName: "ironrdp_web_bg.wasm"
|
||||
out-file-path: 'public/ironrdp-pkg'
|
||||
|
||||
- name: Get version from tag
|
||||
id: version
|
||||
run: |
|
||||
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
||||
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "version=development" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
env:
|
||||
NEXT_PUBLIC_DASHBOARD_VERSION: ${{ steps.version.outputs.version }}
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
105
.github/workflows/docs-ack.yml
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
name: Docs Acknowledgement
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
docs-ack:
|
||||
name: Require docs PR URL or explicit "not needed"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Read PR body
|
||||
id: body
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BODY_B64=$(jq -r '.pull_request.body // "" | @base64' "$GITHUB_EVENT_PATH")
|
||||
{
|
||||
echo "body_b64=$BODY_B64"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate checkbox selection
|
||||
id: validate
|
||||
shell: bash
|
||||
env:
|
||||
BODY_B64: ${{ steps.body.outputs.body_b64 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! body="$(printf '%s' "$BODY_B64" | base64 -d)"; then
|
||||
echo "::error::Failed to decode PR body from base64. Data may be corrupted or missing."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
added_checked=$(printf '%s' "$body" | grep -Ei '^[[:space:]]*-\s*\[x\]\s*I added/updated documentation' | wc -l | tr -d '[:space:]' || true)
|
||||
noneed_checked=$(printf '%s' "$body" | grep -Ei '^[[:space:]]*-\s*\[x\]\s*Documentation is \*\*not needed\*\*' | wc -l | tr -d '[:space:]' || true)
|
||||
|
||||
total=$((added_checked + noneed_checked))
|
||||
if [ "$total" -ne 1 ]; then
|
||||
echo "::error::You must check exactly one docs option in the PR template (either 'docs added' OR 'not needed')."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$added_checked" -eq 1 ]; then
|
||||
echo "mode=added" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "mode=noneed" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Extract docs PR URL (when 'docs added')
|
||||
if: steps.validate.outputs.mode == 'added'
|
||||
id: extract
|
||||
shell: bash
|
||||
env:
|
||||
BODY_B64: ${{ steps.body.outputs.body_b64 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
body="$(printf '%s' "$BODY_B64" | base64 -d)"
|
||||
|
||||
# Strictly require HTTPS and that it's a PR in netbirdio/docs
|
||||
# e.g., https://github.com/netbirdio/docs/pull/1234
|
||||
url="$(printf '%s' "$body" | grep -Eo 'https://github\.com/netbirdio/docs/pull/[0-9]+' | head -n1 || true)"
|
||||
|
||||
if [ -z "${url:-}" ]; then
|
||||
echo "::error::You checked 'docs added' but didn't include a valid HTTPS PR link to netbirdio/docs (e.g., https://github.com/netbirdio/docs/pull/1234)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pr_number="$(printf '%s' "$url" | sed -E 's#.*/pull/([0-9]+)$#\1#')"
|
||||
{
|
||||
echo "url=$url"
|
||||
echo "pr_number=$pr_number"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Verify docs PR exists (and is open or merged)
|
||||
if: steps.validate.outputs.mode == 'added'
|
||||
uses: actions/github-script@v7
|
||||
id: verify
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.extract.outputs.pr_number }}
|
||||
with:
|
||||
script: |
|
||||
const prNumber = parseInt(process.env.PR_NUMBER, 10);
|
||||
const { data } = await github.rest.pulls.get({
|
||||
owner: 'netbirdio',
|
||||
repo: 'docs',
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
// Allow open or merged PRs
|
||||
const ok = data.state === 'open' || data.merged === true;
|
||||
core.setOutput('state', data.state);
|
||||
core.setOutput('merged', String(!!data.merged));
|
||||
if (!ok) {
|
||||
core.setFailed(`Docs PR #${prNumber} exists but is neither open nor merged (state=${data.state}, merged=${data.merged}).`);
|
||||
}
|
||||
result-encoding: string
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: All good
|
||||
run: echo "Documentation requirement satisfied ✅"
|
||||
2
.gitignore
vendored
@@ -37,6 +37,8 @@ next-env.d.ts
|
||||
|
||||
# config
|
||||
.local-config.json
|
||||
.test-config.json
|
||||
cypress.env.json
|
||||
.configs/.local-config.zitadel.json
|
||||
.configs/.staging-config.json
|
||||
.configs/.temp-config.json
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
## Contributor License Agreement
|
||||
|
||||
This Contributor License Agreement (referred to as the "Agreement") is entered into by the individual
|
||||
submitting this Agreement and NetBird GmbH, c/o Max-Beer-Straße 2-4 Münzstraße 12 10178 Berlin, Germany,
|
||||
submitting this Agreement and NetBird GmbH, Brunnenstraße 196, 10119 Berlin, Germany,
|
||||
referred to as "NetBird" (collectively, the "Parties"). The Agreement outlines the terms and conditions
|
||||
under which NetBird may utilize software contributions provided by the Contributor for inclusion in
|
||||
its software development projects. By submitting this Agreement, the Contributor confirms their acceptance
|
||||
|
||||
20
README.md
@@ -10,6 +10,7 @@ See [NetBird repo](https://github.com/netbirdio/netbird)
|
||||
|
||||
The purpose of this project is simple - make it easy to manage VPN built with [NetBird](https://github.com/netbirdio/netbird).
|
||||
The dashboard makes it possible to:
|
||||
|
||||
- track the status of your peers
|
||||
- remove peers
|
||||
- manage Setup Keys (to authenticate new peers)
|
||||
@@ -17,23 +18,25 @@ The dashboard makes it possible to:
|
||||
- define access controls
|
||||
|
||||
## Some Screenshots
|
||||
|
||||
<img src="./src/assets/screenshots/peers.png" alt="peers"/>
|
||||
<img src="./src/assets/screenshots/add-peer.png" alt="add-peer"/>
|
||||
|
||||
|
||||
## Technologies Used
|
||||
|
||||
- NextJS
|
||||
- ReactJS
|
||||
- Tailwind CSS
|
||||
- [React Flow](https://reactflow.dev/) for the Control Center
|
||||
- Auth0
|
||||
- Nginx
|
||||
- Docker
|
||||
- Let's Encrypt
|
||||
|
||||
## How to run
|
||||
|
||||
Disclaimer. We believe that proper user management system is not a trivial task and requires quite some effort to make it right. Therefore we decided to
|
||||
use Auth0 service that covers all our needs (user management, social login, JTW for the management API).
|
||||
use Auth0 service that covers all our needs (user management, social login, JWT for the management API).
|
||||
Auth0 so far is the only 3rd party dependency that can't be really self-hosted.
|
||||
|
||||
1. Install [Docker](https://docs.docker.com/get-docker/)
|
||||
@@ -42,9 +45,9 @@ Auth0 so far is the only 3rd party dependency that can't be really self-hosted.
|
||||
|
||||
`AUTH0_DOMAIN` `AUTH0_CLIENT_ID` `AUTH0_AUDIENCE`
|
||||
|
||||
To obtain these, please use [Auth0 React SDK Guide](https://auth0.com/docs/quickstart/spa/react/01-login#configure-auth0) up until "Configure Allowed Web Origins"
|
||||
To obtain these, please use [Auth0 React SDK Guide](https://auth0.com/docs/quickstart/spa/react) up until "Configure Allowed Web Origins"
|
||||
|
||||
4. NetBird UI Dashboard uses NetBirds Management Service HTTP API, so setting `NETBIRD_MGMT_API_ENDPOINT` is required. Most likely it will be `http://localhost:33071` if you are hosting Management API on the same server.
|
||||
4. NetBird UI Dashboard uses NetBird's Management Service HTTP API, so setting `NETBIRD_MGMT_API_ENDPOINT` is required. Most likely it will be `http://localhost:33071` if you are hosting Management API on the same server.
|
||||
5. Run docker container without SSL (Let's Encrypt):
|
||||
|
||||
```shell
|
||||
@@ -53,9 +56,10 @@ Auth0 so far is the only 3rd party dependency that can't be really self-hosted.
|
||||
-e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> \
|
||||
-e AUTH0_CLIENT_ID=<SET YOUR CLIENT ID> \
|
||||
-e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> \
|
||||
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> \
|
||||
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMENT API URL> \
|
||||
netbirdio/dashboard:main
|
||||
```
|
||||
|
||||
6. Run docker container with SSL (Let's Encrypt):
|
||||
|
||||
```shell
|
||||
@@ -67,7 +71,7 @@ Auth0 so far is the only 3rd party dependency that can't be really self-hosted.
|
||||
-e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> \
|
||||
-e AUTH0_CLIENT_ID=<SET YOUR CLEITN ID> \
|
||||
-e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> \
|
||||
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> \
|
||||
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMENT API URL> \
|
||||
netbirdio/dashboard:main
|
||||
```
|
||||
|
||||
@@ -83,11 +87,11 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the
|
||||
You can start editing by modifying the code inside `src/..`
|
||||
The page auto-updates as you edit the file.
|
||||
|
||||
## How to migrate from old dashboard (v1)
|
||||
## How to migrate from old dashboard (v1)
|
||||
|
||||
The new dashboard comes with a new docker image `netbirdio/dashboard:main`.
|
||||
To migrate from the old dashboard (v1) `wiretrustee/dashboard:main` to the new one, please follow the steps below.
|
||||
|
||||
1. Stop the dashboard container `docker compose down dashboard`
|
||||
2. Replace the docker image name in your `docker-compose.yml` with `netbirdio/dashboard:main`
|
||||
3. Recreate the dashboard container `docker compose up -d --force-recreate dashboard`
|
||||
3. Recreate the dashboard container `docker compose up -d --force-recreate dashboard`
|
||||
|
||||
12
announcements.json
Normal file
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"tag": "New",
|
||||
"text": "NetBird Reverse Proxy - Expose internal services to the public with automatic TLS and optional authentication.",
|
||||
"link": "https://docs.netbird.io/manage/reverse-proxy",
|
||||
"linkText": "Learn more",
|
||||
"variant": "important",
|
||||
"isExternal": true,
|
||||
"closeable": true,
|
||||
"isCloudOnly": false
|
||||
}
|
||||
]
|
||||
@@ -13,5 +13,6 @@
|
||||
"dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS",
|
||||
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
|
||||
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID",
|
||||
"googleTagManagerID": "$NETBIRD_GOOGLE_TAG_MANAGER_ID"
|
||||
}
|
||||
"googleTagManagerID": "$NETBIRD_GOOGLE_TAG_MANAGER_ID",
|
||||
"wasmPath": "$NETBIRD_WASM_PATH"
|
||||
}
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
FROM alpine:3.14
|
||||
|
||||
RUN apk add --no-cache bash curl less ca-certificates git tzdata zip gettext \
|
||||
nginx curl supervisor certbot-nginx && \
|
||||
rm -rf /var/cache/apk/* && mkdir -p /run/nginx
|
||||
|
||||
STOPSIGNAL SIGINT
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
ENTRYPOINT ["/usr/bin/supervisord","-c","/etc/supervisord.conf"]
|
||||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /usr/share/nginx/html
|
||||
# copy configuration files
|
||||
COPY docker/default.conf /etc/nginx/http.d/default.conf
|
||||
COPY docker/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY docker/init_cert.sh /usr/local/init_cert.sh
|
||||
COPY docker/init_react_envs.sh /usr/local/init_react_envs.sh
|
||||
RUN chmod +x /usr/local/init_cert.sh && rm /etc/crontabs/root
|
||||
RUN chmod +x /usr/local/init_react_envs.sh
|
||||
|
||||
# configure supervisor
|
||||
COPY docker/supervisord.conf /etc/supervisord.conf
|
||||
# copy build files
|
||||
COPY out/ /usr/share/nginx/html/
|
||||
# Copy build files
|
||||
COPY out/ /usr/share/nginx/html/
|
||||
|
||||
# Copy server script
|
||||
COPY docker/server.js /server.js
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["node", "/server.js"]
|
||||
|
||||
@@ -7,6 +7,10 @@ server {
|
||||
root /usr/share/nginx/html;
|
||||
default_type application/wasm;
|
||||
}
|
||||
location = /ironrdp-pkg/ironrdp_web_bg.wasm {
|
||||
root /usr/share/nginx/html;
|
||||
default_type application/wasm;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri.html $uri/ =404;
|
||||
|
||||
@@ -61,11 +61,12 @@ export NETBIRD_GOOGLE_ANALYTICS_ID=${NETBIRD_GOOGLE_ANALYTICS_ID}
|
||||
export NETBIRD_GOOGLE_TAG_MANAGER_ID=${NETBIRD_GOOGLE_TAG_MANAGER_ID}
|
||||
export NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken}
|
||||
export NETBIRD_DRAG_QUERY_PARAMS=${NETBIRD_DRAG_QUERY_PARAMS:-false}
|
||||
export NETBIRD_WASM_PATH=${NETBIRD_WASM_PATH}
|
||||
|
||||
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
|
||||
|
||||
# replace ENVs in the config
|
||||
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 \$\$NETBIRD_GOOGLE_ANALYTICS_ID \$\$NETBIRD_GOOGLE_TAG_MANAGER_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE \$\$NETBIRD_DRAG_QUERY_PARAMS"
|
||||
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 \$\$NETBIRD_GOOGLE_ANALYTICS_ID \$\$NETBIRD_GOOGLE_TAG_MANAGER_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE \$\$NETBIRD_DRAG_QUERY_PARAMS \$\$NETBIRD_WASM_PATH"
|
||||
|
||||
OIDC_TRUSTED_DOMAINS="/usr/share/nginx/html/OidcTrustedDomains.js"
|
||||
envsubst "$ENV_STR" < "$OIDC_TRUSTED_DOMAINS".tmpl > "$OIDC_TRUSTED_DOMAINS"
|
||||
|
||||
136
docker/server.js
Normal file
@@ -0,0 +1,136 @@
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const root = path.resolve('/usr/share/nginx/html');
|
||||
|
||||
const MIME = {
|
||||
'.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript',
|
||||
'.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg',
|
||||
'.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.wasm': 'application/wasm',
|
||||
'.ttf': 'font/ttf', '.woff': 'font/woff', '.woff2': 'font/woff2',
|
||||
'.txt': 'text/plain', '.xml': 'text/xml'
|
||||
};
|
||||
|
||||
// Replace both placeholder styles used by generated assets and templates.
|
||||
const ENV_KEYS = [
|
||||
'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',
|
||||
'NETBIRD_GOOGLE_ANALYTICS_ID',
|
||||
'NETBIRD_GOOGLE_TAG_MANAGER_ID',
|
||||
'AUTH_REDIRECT_URI',
|
||||
'AUTH_SILENT_REDIRECT_URI',
|
||||
'NETBIRD_TOKEN_SOURCE',
|
||||
'NETBIRD_DRAG_QUERY_PARAMS',
|
||||
'NETBIRD_WASM_PATH',
|
||||
'AUTH0_DOMAIN',
|
||||
'AUTH0_CLIENT_ID',
|
||||
'AUTH0_AUDIENCE',
|
||||
];
|
||||
|
||||
function substituteEnv(content) {
|
||||
let changed = false;
|
||||
for (const key of ENV_KEYS) {
|
||||
const val = process.env[key] || '';
|
||||
for (const pattern of ['$$' + key, '$' + key]) {
|
||||
if (content.includes(pattern)) {
|
||||
content = content.split(pattern).join(val);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { content, changed };
|
||||
}
|
||||
|
||||
function walkDir(dir) {
|
||||
try {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) walkDir(full);
|
||||
else if (entry.isFile() && /\.(js|html|txt|json)$/.test(entry.name)) {
|
||||
const result = substituteEnv(fs.readFileSync(full, 'utf8'));
|
||||
if (result.changed) fs.writeFileSync(full, result.content, 'utf8');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to substitute environment variables:', e);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Substituting environment variables...');
|
||||
try {
|
||||
const tmpl = path.join(root, 'OidcTrustedDomains.js.tmpl');
|
||||
if (fs.existsSync(tmpl)) {
|
||||
const result = substituteEnv(fs.readFileSync(tmpl, 'utf8'));
|
||||
fs.writeFileSync(path.join(root, 'OidcTrustedDomains.js'), result.content, 'utf8');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to create OidcTrustedDomains.js:', e);
|
||||
}
|
||||
|
||||
walkDir(root);
|
||||
console.log('Environment substitution complete.');
|
||||
|
||||
function isFile(p) {
|
||||
try { return fs.statSync(p).isFile(); } catch(e) { return false; }
|
||||
}
|
||||
|
||||
function safePath(p) {
|
||||
const abs = path.resolve(root, '.' + p);
|
||||
return abs === root || abs.startsWith(root + path.sep) ? abs : null;
|
||||
}
|
||||
|
||||
function resolvePath(url) {
|
||||
let p = url.split('?')[0];
|
||||
if (!p.startsWith('/')) p = '/' + p;
|
||||
if (p === '/' || p.endsWith('/')) p += 'index.html';
|
||||
const abs = safePath(p);
|
||||
if (abs && isFile(abs)) return abs;
|
||||
// Try .html suffix (Next.js static export uses path.html)
|
||||
const asHtml = safePath(p + '.html');
|
||||
if (asHtml && isFile(asHtml)) return asHtml;
|
||||
// Try path/index.html
|
||||
const asDirIndex = safePath(p + '/index.html');
|
||||
if (asDirIndex && isFile(asDirIndex)) return asDirIndex;
|
||||
// Try /zh prefix (next-intl locale)
|
||||
if (!p.startsWith('/zh')) {
|
||||
const zh = safePath('/zh' + p);
|
||||
if (zh && isFile(zh)) return zh;
|
||||
const zhHtml = safePath('/zh' + p + '.html');
|
||||
if (zhHtml && isFile(zhHtml)) return zhHtml;
|
||||
const zhDirIndex = safePath('/zh' + p + '/index.html');
|
||||
if (zhDirIndex && isFile(zhDirIndex)) return zhDirIndex;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
http.createServer((req, res) => {
|
||||
const filePath = resolvePath(req.url);
|
||||
if (filePath) {
|
||||
const ext = path.extname(filePath);
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if (err) { send404(res); return; }
|
||||
res.writeHead(200, {
|
||||
'Content-Type': MIME[ext] || 'application/octet-stream',
|
||||
'Cache-Control': ['.html', '.js'].includes(ext) ? 'no-store, no-cache, must-revalidate, max-age=0' : 'public, max-age=3600'
|
||||
});
|
||||
res.end(data);
|
||||
});
|
||||
} else {
|
||||
send404(res);
|
||||
}
|
||||
}).listen(80, () => console.log('NetBird Dashboard running on port 80'));
|
||||
|
||||
function send404(res) {
|
||||
fs.readFile(path.join(root, '404.html'), (err, data) => {
|
||||
res.writeHead(404, {'Content-Type': 'text/html', 'Cache-Control': 'no-store'});
|
||||
res.end(err ? '404 Not Found' : data);
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
const createNextIntlPlugin = require('next-intl/plugin');
|
||||
|
||||
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: "export",
|
||||
@@ -7,7 +11,9 @@ const nextConfig = {
|
||||
reactStrictMode: false,
|
||||
env: {
|
||||
APP_ENV: process.env.APP_ENV || "production",
|
||||
NEXT_PUBLIC_DASHBOARD_VERSION:
|
||||
process.env.NEXT_PUBLIC_DASHBOARD_VERSION || "development",
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
module.exports = withNextIntl(nextConfig);
|
||||
|
||||
10472
package-lock.json
generated
86
package.json
@@ -2,6 +2,9 @@
|
||||
"name": "netbird-dashboard",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"copy": "copyfiles -f ./node_modules/@axa-fr/react-oidc/dist/OidcServiceWorker.js ./public",
|
||||
"copytrusted": "copyfiles -f ./public/local/OidcTrustedDomains.js ./public",
|
||||
@@ -13,82 +16,91 @@
|
||||
"cypress:open": "cypress open"
|
||||
},
|
||||
"dependencies": {
|
||||
"@axa-fr/react-oidc": "^7.22.18",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-hover-card": "^1.1.4",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-radio-group": "^1.1.3",
|
||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-slider": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@tabler/icons-react": "^2.39.0",
|
||||
"@axa-fr/react-oidc": "^7.26.3",
|
||||
"@dagrejs/dagre": "^1.1.5",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toast": "^1.2.15",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tabler/icons-react": "^3.36.1",
|
||||
"@tanstack/match-sorter-utils": "^8.8.4",
|
||||
"@tanstack/react-table": "^8.10.7",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/lodash": "^4.14.200",
|
||||
"@types/node": "20.10.6",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"@xyflow/react": "^12.8.4",
|
||||
"autoprefixer": "^10",
|
||||
"chart.js": "^4.4.8",
|
||||
"chroma-js": "^3.1.2",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"classnames": "^2.5.1",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"cmdk": "^1.1.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"d3": "^7.9.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"eslint": "^8",
|
||||
"elkjs": "^0.10.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"flowbite": "^1.8.1",
|
||||
"flowbite-react": "^0.6.4",
|
||||
"framer-motion": "^10.16.4",
|
||||
"framer-motion": "^12.29.2",
|
||||
"ip-address": "^10.1.0",
|
||||
"ip-cidr": "^3.1.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.481.0",
|
||||
"next": "^14.2.28",
|
||||
"lodash": "^4.17.23",
|
||||
"lucide-react": "^0.566.0",
|
||||
"next": "16.1.7",
|
||||
"next-intl": "^4.13.0",
|
||||
"next-themes": "^0.2.1",
|
||||
"punycode": "^2.3.1",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^8.9.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react": "^19.2.4",
|
||||
"react-day-picker": "^9.13.0",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-ga4": "^2.1.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-hotjar": "^6.2.0",
|
||||
"react-hotjar": "^6.3.1",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-jwt": "^1.2.0",
|
||||
"react-loading-skeleton": "^3.3.1",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-virtuoso": "^4.9.0",
|
||||
"sonner": "^2.0.7",
|
||||
"swr": "^2.2.4",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"timescape": "^0.7.1",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"overrides": {
|
||||
"minimatch": ">=10.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^9.5.1",
|
||||
"@types/chroma-js": "^3.1.1",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"eslint-config-next": "^14.2.28",
|
||||
"cypress": "^13.13.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-next": "^16.1.6",
|
||||
"postcss": "^8",
|
||||
"prettier": "3.0.3",
|
||||
"tailwindcss": "^3"
|
||||
"tailwindcss": "^3.4.17"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@ import React from "react";
|
||||
|
||||
export default function Redirect() {
|
||||
useRedirect("/events/audit");
|
||||
return <FullScreenLoading height={"auto"} />;
|
||||
return <FullScreenLoading fullScreen={false} />;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
|
||||
import GroupsProvider from "@/contexts/GroupsProvider";
|
||||
@@ -17,60 +18,56 @@ import { Policy } from "@/interfaces/Policy";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
|
||||
const AccessControlTable = lazy(
|
||||
() => import("@/modules/access-control/table/AccessControlTable"),
|
||||
() => import("@/modules/access-control/table/AccessControlTable"),
|
||||
);
|
||||
export default function AccessControlPage() {
|
||||
const { permission } = usePermissions();
|
||||
const t = useTranslations("policies");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const { data: policies, isLoading } = useFetchApi<Policy[]>("/policies");
|
||||
const { data: policies, isLoading } = useFetchApi<Policy[]>("/policies");
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<GroupsProvider>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/policies"}
|
||||
label={"Access Control"}
|
||||
icon={<AccessControlIcon size={14} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>Access Control Policies</h1>
|
||||
<Paragraph>
|
||||
Create rules to manage access in your network and define what peers
|
||||
can connect.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/manage-network-access"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Access Controls
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
</div>
|
||||
return (
|
||||
<PageContainer>
|
||||
<GroupsProvider>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/access-control"}
|
||||
label={t("title")}
|
||||
icon={<AccessControlIcon size={14} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>{t("title")}</h1>
|
||||
<Paragraph>
|
||||
{t("accessControlDescription")}{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/manage-network-access"}
|
||||
target={"_blank"}
|
||||
>
|
||||
{t("learnMore")}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<RestrictedAccess
|
||||
page={"Access Control"}
|
||||
hasAccess={permission.policies.read}
|
||||
>
|
||||
<PoliciesProvider>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<AccessControlTable
|
||||
isLoading={isLoading}
|
||||
policies={policies}
|
||||
headingTarget={portalTarget}
|
||||
/>
|
||||
</Suspense>
|
||||
</PoliciesProvider>
|
||||
</RestrictedAccess>
|
||||
</GroupsProvider>
|
||||
</PageContainer>
|
||||
);
|
||||
<RestrictedAccess
|
||||
page={t("title")}
|
||||
hasAccess={permission.policies.read}
|
||||
>
|
||||
<PoliciesProvider>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<AccessControlTable
|
||||
isLoading={isLoading}
|
||||
policies={policies}
|
||||
headingTarget={portalTarget}
|
||||
/>
|
||||
</Suspense>
|
||||
</PoliciesProvider>
|
||||
</RestrictedAccess>
|
||||
</GroupsProvider>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
8
src/app/(dashboard)/control-center/layout.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Control Center - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
1878
src/app/(dashboard)/control-center/page.tsx
Normal file
@@ -7,7 +7,8 @@ import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { ExternalLinkIcon, ServerIcon } from "lucide-react";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import DNSIcon from "@/assets/icons/DNSIcon";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
@@ -15,63 +16,61 @@ import { NameserverGroup } from "@/interfaces/Nameserver";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
|
||||
const NameserverGroupTable = lazy(
|
||||
() => import("@/modules/dns-nameservers/table/NameserverGroupTable"),
|
||||
() => import("@/modules/dns/nameservers/table/NameserverGroupTable"),
|
||||
);
|
||||
|
||||
export default function NameServers() {
|
||||
const { permission } = usePermissions();
|
||||
const t = useTranslations("dns");
|
||||
const tCommon = useTranslations("common");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const { data: nameserverGroups, isLoading } =
|
||||
useFetchApi<NameserverGroup[]>("/dns/nameservers");
|
||||
const { data: nameserverGroups, isLoading } =
|
||||
useFetchApi<NameserverGroup[]>("/dns/nameservers");
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/dns"}
|
||||
label={"DNS"}
|
||||
icon={<DNSIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/dns/nameservers"}
|
||||
label={"Nameservers"}
|
||||
active
|
||||
icon={<ServerIcon size={13} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>Nameservers</h1>
|
||||
<Paragraph>
|
||||
Add nameservers for domain name resolution in your NetBird network.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/manage-dns-in-your-network"}
|
||||
target={"_blank"}
|
||||
>
|
||||
DNS
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
</div>
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/dns/nameservers"}
|
||||
label={t("title")}
|
||||
icon={<DNSIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/dns/nameservers"}
|
||||
label={t("nameservers")}
|
||||
active
|
||||
icon={<DNSIcon size={13} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>{t("nameservers")}</h1>
|
||||
<Paragraph>
|
||||
{t("nameserversDescription")}{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/manage-dns-in-your-network"}
|
||||
target={"_blank"}
|
||||
>
|
||||
{tCommon("learnMore")}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<RestrictedAccess
|
||||
page={"Nameservers"}
|
||||
hasAccess={permission.nameservers.read}
|
||||
>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<NameserverGroupTable
|
||||
nameserverGroups={nameserverGroups}
|
||||
isLoading={isLoading}
|
||||
headingTarget={portalTarget}
|
||||
/>
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</PageContainer>
|
||||
);
|
||||
<RestrictedAccess
|
||||
page={t("nameservers")}
|
||||
hasAccess={permission.nameservers.read}
|
||||
>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<NameserverGroupTable
|
||||
nameserverGroups={nameserverGroups}
|
||||
isLoading={isLoading}
|
||||
headingTarget={portalTarget}
|
||||
/>
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,5 +11,5 @@ export default function DNS() {
|
||||
router.push("/dns/nameservers");
|
||||
}, [router]);
|
||||
|
||||
return <FullScreenLoading height={"auto"} />;
|
||||
return <FullScreenLoading fullScreen={false} />;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { IconSettings2 } from "@tabler/icons-react";
|
||||
import useFetchApi, { useApiCall } from "@utils/api";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React from "react";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
import { useSWRConfig } from "swr";
|
||||
@@ -26,126 +27,128 @@ import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
import { useGroupIdsToGroups } from "@/modules/groups/useGroupIdsToGroups";
|
||||
|
||||
export default function NameServerSettings() {
|
||||
const { permission } = usePermissions();
|
||||
const t = useTranslations("dns");
|
||||
const tCommon = useTranslations("common");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const { data: settings, isLoading } =
|
||||
useFetchApi<NameserverSettings>("/dns/settings");
|
||||
const { data: settings, isLoading } =
|
||||
useFetchApi<NameserverSettings>("/dns/settings");
|
||||
|
||||
const initialDNSGroups = useGroupIdsToGroups(
|
||||
settings?.disabled_management_groups,
|
||||
);
|
||||
const initialDNSGroups = useGroupIdsToGroups(
|
||||
settings?.disabled_management_groups,
|
||||
);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/dns"}
|
||||
label={"DNS"}
|
||||
icon={<DNSIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/dns/settings"}
|
||||
label={"DNS Settings"}
|
||||
active
|
||||
icon={<IconSettings2 size={15} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1>DNS Settings</h1>
|
||||
<Paragraph>{"Manage your account's DNS settings."}</Paragraph>
|
||||
<Paragraph>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/manage-dns-in-your-network"}
|
||||
target={"_blank"}
|
||||
>
|
||||
DNS
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
<RestrictedAccess page={"DNS Settings"} hasAccess={permission.dns.read}>
|
||||
{!isLoading && initialDNSGroups !== undefined ? (
|
||||
<SettingDisabledManagementGroups initialGroups={initialDNSGroups} />
|
||||
) : (
|
||||
<div>
|
||||
<Skeleton
|
||||
width={"100%"}
|
||||
className={"mt-8 max-w-xl"}
|
||||
height={240}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</RestrictedAccess>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/dns"}
|
||||
label={t("title")}
|
||||
icon={<DNSIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/dns/settings"}
|
||||
label={t("dnsSettings")}
|
||||
active
|
||||
icon={<IconSettings2 size={15} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1>{t("dnsSettings")}</h1>
|
||||
<Paragraph>
|
||||
{t("dnsSettingsDescription")}{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/manage-dns-in-your-network"}
|
||||
target={"_blank"}
|
||||
>
|
||||
{tCommon("learnMore")}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
<RestrictedAccess
|
||||
page={t("dnsSettings")}
|
||||
hasAccess={permission.dns.read}
|
||||
>
|
||||
{!isLoading && initialDNSGroups !== undefined ? (
|
||||
<SettingDisabledManagementGroups initialGroups={initialDNSGroups} />
|
||||
) : (
|
||||
<div>
|
||||
<Skeleton
|
||||
width={"100%"}
|
||||
className={"mt-8 max-w-xl"}
|
||||
height={240}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</RestrictedAccess>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const SettingDisabledManagementGroups = ({
|
||||
initialGroups,
|
||||
initialGroups,
|
||||
}: {
|
||||
initialGroups: Group[];
|
||||
initialGroups: Group[];
|
||||
}) => {
|
||||
const settingRequest = useApiCall<NameserverSettings>("/dns/settings");
|
||||
const { mutate } = useSWRConfig();
|
||||
const { permission } = usePermissions();
|
||||
const t = useTranslations("dns");
|
||||
const settingRequest = useApiCall<NameserverSettings>("/dns/settings");
|
||||
const { mutate } = useSWRConfig();
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const [selectedGroups, setSelectedGroups, { save: saveGroups }] =
|
||||
useGroupHelper({
|
||||
initial: initialGroups,
|
||||
});
|
||||
const [selectedGroups, setSelectedGroups, { save: saveGroups }] =
|
||||
useGroupHelper({
|
||||
initial: initialGroups,
|
||||
});
|
||||
|
||||
const { hasChanges, updateRef: updateChangesRef } = useHasChanges([
|
||||
selectedGroups,
|
||||
]);
|
||||
const { hasChanges, updateRef: updateChangesRef } = useHasChanges([
|
||||
selectedGroups,
|
||||
]);
|
||||
|
||||
const saveSettings = async () => {
|
||||
const savedGroups = await saveGroups();
|
||||
notify({
|
||||
title: "DNS Settings",
|
||||
description: "Settings saved successfully.",
|
||||
promise: settingRequest
|
||||
.put({
|
||||
disabled_management_groups: savedGroups.map((g) => g.id),
|
||||
})
|
||||
.then(() => {
|
||||
mutate("/dns/settings");
|
||||
updateChangesRef([selectedGroups]);
|
||||
}),
|
||||
loadingMessage: "Saving the settings...",
|
||||
});
|
||||
};
|
||||
const saveSettings = async () => {
|
||||
const savedGroups = await saveGroups();
|
||||
notify({
|
||||
title: t("dnsSettings"),
|
||||
description: t("settingsSaved"),
|
||||
promise: settingRequest
|
||||
.put({
|
||||
disabled_management_groups: savedGroups.map((g) => g.id),
|
||||
})
|
||||
.then(() => {
|
||||
mutate("/dns/settings");
|
||||
updateChangesRef([selectedGroups]);
|
||||
}),
|
||||
loadingMessage: t("settingsSaving"),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={"mt-8 max-w-xl"}>
|
||||
<div className={"px-8 py-8"}>
|
||||
<Label>Disable DNS management for these groups</Label>
|
||||
<HelpText>
|
||||
Peers in these groups will require manual domain name resolution
|
||||
</HelpText>
|
||||
<PeerGroupSelector
|
||||
dataCy={"dns-groups-selector"}
|
||||
onChange={setSelectedGroups}
|
||||
values={selectedGroups}
|
||||
disabled={!permission.dns.update}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
"flex justify-end bg-nb-gray-900/20 border-t border-nb-gray-900 px-8 py-5"
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
size={"sm"}
|
||||
onClick={saveSettings}
|
||||
disabled={!hasChanges || !permission.dns.update}
|
||||
data-cy={"save-changes"}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
return (
|
||||
<Card className={"mt-8 max-w-xl"}>
|
||||
<div className={"px-8 py-8"}>
|
||||
<Label>{t("disabledManagementGroup")}</Label>
|
||||
<HelpText>{t("disabledManagementGroupHelp")}</HelpText>
|
||||
<PeerGroupSelector
|
||||
dataCy={"dns-groups-selector"}
|
||||
onChange={setSelectedGroups}
|
||||
values={selectedGroups}
|
||||
disabled={!permission.dns.update}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
"flex justify-end bg-nb-gray-900/20 border-t border-nb-gray-900 px-8 py-5"
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
size={"sm"}
|
||||
onClick={saveSettings}
|
||||
disabled={!hasChanges || !permission.dns.update}
|
||||
data-cy={"save-changes"}
|
||||
>
|
||||
{t("saveChanges")}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
8
src/app/(dashboard)/dns/zones/layout.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Zones - DNS - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
69
src/app/(dashboard)/dns/zones/page.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { lazy, Suspense } from "react";
|
||||
import DNSIcon from "@/assets/icons/DNSIcon";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { DNS_ZONE_DOCS_LINK, DNSZone } from "@/interfaces/DNS";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import { DNSZonesProvider } from "@/modules/dns/zones/DNSZonesProvider";
|
||||
import DNSZoneIcon from "@/assets/icons/DNSZoneIcon";
|
||||
|
||||
const DNSZonesTable = lazy(
|
||||
() => import("@/modules/dns/zones/table/DNSZonesTable"),
|
||||
);
|
||||
|
||||
export default function DNSZonePage() {
|
||||
const t = useTranslations("dns");
|
||||
const tCommon = useTranslations("common");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const { data: zones, isLoading } = useFetchApi<DNSZone[]>("/dns/zones");
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item label={t("title")} icon={<DNSIcon size={13} />} />
|
||||
<Breadcrumbs.Item
|
||||
href={"/dns/zones"}
|
||||
label={t("zones")}
|
||||
active
|
||||
icon={<DNSZoneIcon size={16} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>{t("zones")}</h1>
|
||||
<Paragraph>
|
||||
{t("zonesDescription")}{" "}
|
||||
<InlineLink href={DNS_ZONE_DOCS_LINK} target={"_blank"}>
|
||||
{tCommon("learnMore")}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<RestrictedAccess page={t("zones")} hasAccess={permission?.dns?.read}>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<DNSZonesProvider>
|
||||
<DNSZonesTable
|
||||
isLoading={isLoading}
|
||||
headingTarget={portalTarget}
|
||||
data={zones}
|
||||
/>
|
||||
</DNSZonesProvider>
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { ExternalLinkIcon, LogsIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React from "react";
|
||||
import ActivityIcon from "@/assets/icons/ActivityIcon";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
@@ -15,6 +16,8 @@ import PageContainer from "@/layouts/PageContainer";
|
||||
import ActivityTable from "@/modules/activity/ActivityTable";
|
||||
|
||||
export default function Activity() {
|
||||
const t = useTranslations("activity");
|
||||
const tCommon = useTranslations("common");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const { data: events, isLoading } =
|
||||
@@ -28,31 +31,29 @@ export default function Activity() {
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
label={"Activity"}
|
||||
label={t("title")}
|
||||
disabled={true}
|
||||
icon={<ActivityIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/events/audit"}
|
||||
label={"Audit Events"}
|
||||
label={t("auditEvents")}
|
||||
icon={<LogsIcon size={18} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>Audit Events</h1>
|
||||
<Paragraph>Here you can see all the audit activity events.</Paragraph>
|
||||
<h1 ref={headingRef}>{t("auditEvents")}</h1>
|
||||
<Paragraph>
|
||||
Learn more about{" "}
|
||||
{t("auditEventsDescription")}{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/audit-events-logging"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Audit Events
|
||||
{tCommon("learnMore")}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<RestrictedAccess page={"Activity"} hasAccess={permission.events.read}>
|
||||
<RestrictedAccess page={t("title")} hasAccess={permission.events.read}>
|
||||
<ActivityTable
|
||||
events={events}
|
||||
isLoading={isLoading}
|
||||
|
||||
5
src/app/(dashboard)/events/proxy/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function ProxyEventsPage() {
|
||||
redirect("/reverse-proxy/logs");
|
||||
}
|
||||
8
src/app/(dashboard)/group/layout.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Group - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
344
src/app/(dashboard)/group/page.tsx
Normal file
@@ -0,0 +1,344 @@
|
||||
"use client";
|
||||
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import { GroupBadgeIcon } from "@components/ui/GroupBadgeIcon";
|
||||
import { PageNotFound } from "@components/ui/PageNotFound";
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import useRedirect from "@hooks/useRedirect";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { cn, singularize } from "@utils/helpers";
|
||||
import { FolderGit2Icon, Layers3Icon, PencilIcon } from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { useState } from "react";
|
||||
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
|
||||
import DNSIcon from "@/assets/icons/DNSIcon";
|
||||
import DNSZoneIcon from "@/assets/icons/DNSZoneIcon";
|
||||
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
|
||||
import PeerIcon from "@/assets/icons/PeerIcon";
|
||||
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
|
||||
import TeamIcon from "@/assets/icons/TeamIcon";
|
||||
import { GroupProvider, useGroupContext } from "@/contexts/GroupProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import RoutesProvider from "@/contexts/RoutesProvider";
|
||||
import { Group, GROUP_TOOLTIP_TEXT } from "@/interfaces/Group";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import { GroupDNSZonesSection } from "@/modules/groups/details/GroupDNSZonesSection";
|
||||
import { GroupNameserversSection } from "@/modules/groups/details/GroupNameserversSection";
|
||||
import { GroupNetworkRoutesSection } from "@/modules/groups/details/GroupNetworkRoutesSection";
|
||||
import { GroupPeersSection } from "@/modules/groups/details/GroupPeersSection";
|
||||
import { GroupPoliciesSection } from "@/modules/groups/details/GroupPoliciesSection";
|
||||
import { GroupResourcesSection } from "@/modules/groups/details/GroupResourcesSection";
|
||||
import { GroupSetupKeysSection } from "@/modules/groups/details/GroupSetupKeysSection";
|
||||
import { GroupUsersSection } from "@/modules/groups/details/GroupUsersSection";
|
||||
import useGroupDetails from "@/modules/groups/details/useGroupDetails";
|
||||
|
||||
export default function GroupPage() {
|
||||
const t = useTranslations("groups");
|
||||
const queryParameter = useSearchParams();
|
||||
const { isRestricted } = usePermissions();
|
||||
const groupId = queryParameter.get("id");
|
||||
const {
|
||||
data: group,
|
||||
isLoading,
|
||||
error,
|
||||
} = useFetchApi<Group>(`/groups/${groupId}`, true);
|
||||
|
||||
useRedirect("/groups", false, !groupId || isRestricted);
|
||||
|
||||
if (isRestricted) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<RestrictedAccess page={t("title")} />
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (error)
|
||||
return (
|
||||
<PageNotFound
|
||||
title={error?.message}
|
||||
description={
|
||||
"The group you are attempting to access cannot be found. It may have been deleted, or you may not have permission to view it. Please verify the URL or return to the dashboard."
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return group && !isLoading ? (
|
||||
<PageContainer>
|
||||
<RoutesProvider>
|
||||
<GroupProvider group={group} isDetailPage={true}>
|
||||
<div className={"p-default py-6 pb-0 w-full mb-[6px]"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/groups"}
|
||||
label={t("title")}
|
||||
icon={<FolderGit2Icon size={14} />}
|
||||
/>
|
||||
<Breadcrumbs.Item label={group.name} active />
|
||||
</Breadcrumbs>
|
||||
<GroupDetailsName />
|
||||
</div>
|
||||
<GroupOverviewTabs group={group} />
|
||||
</GroupProvider>
|
||||
</RoutesProvider>
|
||||
</PageContainer>
|
||||
) : (
|
||||
<FullScreenLoading />
|
||||
);
|
||||
}
|
||||
|
||||
const GroupDetailsName = () => {
|
||||
const { group, isJWTGroup, isAllowedToRename, openGroupRenameModal } =
|
||||
useGroupContext();
|
||||
const { permission } = usePermissions();
|
||||
|
||||
return (
|
||||
<div className={"w-full"}>
|
||||
<h1 className={"flex items-center gap-3 w-full whitespace-nowrap"}>
|
||||
<GroupBadgeIcon id={group?.id} issued={group?.issued} size={20} />
|
||||
{group.name}
|
||||
{group.name !== "All" && permission?.groups?.update && (
|
||||
<div>
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"text-xs max-w-xs"}>
|
||||
{isJWTGroup
|
||||
? GROUP_TOOLTIP_TEXT.RENAME.JWT
|
||||
: GROUP_TOOLTIP_TEXT.RENAME.INTEGRATION}
|
||||
</div>
|
||||
}
|
||||
interactive={false}
|
||||
disabled={isAllowedToRename}
|
||||
className={"w-full block"}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-8 w-8 items-center justify-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer",
|
||||
!isAllowedToRename &&
|
||||
"opacity-40 cursor-not-allowed pointer-events-none",
|
||||
)}
|
||||
onClick={openGroupRenameModal}
|
||||
>
|
||||
<PencilIcon size={16} />
|
||||
</div>
|
||||
</FullTooltip>
|
||||
</div>
|
||||
)}
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const validAllGroupTabs = [
|
||||
"policies",
|
||||
"resources",
|
||||
"network-routes",
|
||||
"nameservers",
|
||||
"zones",
|
||||
];
|
||||
|
||||
const validOtherGroupTabs = ["users", "peers", "setup-keys"];
|
||||
|
||||
const GroupOverviewTabs = ({ group }: { group: Group }) => {
|
||||
const t = useTranslations("groups");
|
||||
const tNetworks = useTranslations("networks");
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const getInitialTab = () => {
|
||||
const isAllGroup = group.name === "All";
|
||||
const tabParam = searchParams.get("tab");
|
||||
const validTabs = isAllGroup
|
||||
? validAllGroupTabs
|
||||
: [...validAllGroupTabs, ...validOtherGroupTabs];
|
||||
if (tabParam === null) return isAllGroup ? "policies" : "users";
|
||||
if (isAllGroup) {
|
||||
return validTabs.includes(tabParam) ? tabParam : "policies";
|
||||
}
|
||||
return validTabs.includes(tabParam) ? tabParam : "users";
|
||||
};
|
||||
|
||||
const [tab, setTab] = useState(getInitialTab());
|
||||
const { groupDetails, isLoading } = useGroupDetails(group?.id || "");
|
||||
|
||||
const peersCount = groupDetails?.peers_count || 0;
|
||||
const usersCount = groupDetails?.users?.length || 0;
|
||||
const policiesCount = groupDetails?.policies?.length || 0;
|
||||
const resourcesCount = groupDetails?.resources_count || 0;
|
||||
const routesCount = groupDetails?.routes?.length || 0;
|
||||
const nameserversCount = groupDetails?.nameservers?.length || 0;
|
||||
const zonesCount = groupDetails?.zones?.length || 0;
|
||||
const setupKeysCount = groupDetails?.setupKeys?.length || 0;
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
defaultValue={tab}
|
||||
onValueChange={(v) => setTab(v)}
|
||||
value={tab}
|
||||
className={"pt-2 pb-0 mb-0"}
|
||||
>
|
||||
<TabsList justify={"start"} className={"px-8"}>
|
||||
{group.name !== "All" && (
|
||||
<TabsTrigger
|
||||
value={"users"}
|
||||
className={groupDetails === null ? "animate-pulse" : ""}
|
||||
>
|
||||
<TeamIcon
|
||||
size={12}
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
{singularize(t("users"), usersCount)}
|
||||
</TabsTrigger>
|
||||
)}
|
||||
|
||||
{group.name !== "All" && (
|
||||
<TabsTrigger
|
||||
value={"peers"}
|
||||
className={groupDetails === null ? "animate-pulse" : ""}
|
||||
>
|
||||
<PeerIcon
|
||||
size={12}
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
{singularize(t("peers"), peersCount)}
|
||||
</TabsTrigger>
|
||||
)}
|
||||
|
||||
<TabsTrigger
|
||||
value={"policies"}
|
||||
className={groupDetails === null ? "animate-pulse" : ""}
|
||||
>
|
||||
<AccessControlIcon
|
||||
size={12}
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
{singularize(t("policies"), policiesCount)}
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger
|
||||
value={"resources"}
|
||||
className={groupDetails === null ? "animate-pulse" : ""}
|
||||
>
|
||||
<Layers3Icon size={14} />
|
||||
{singularize(t("resources"), resourcesCount)}
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger
|
||||
value={"network-routes"}
|
||||
className={groupDetails === null ? "animate-pulse" : ""}
|
||||
>
|
||||
<NetworkRoutesIcon
|
||||
size={12}
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
{singularize(tNetworks("networkRoutes"), routesCount)}
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger
|
||||
value={"nameservers"}
|
||||
className={groupDetails === null ? "animate-pulse" : ""}
|
||||
>
|
||||
<DNSIcon
|
||||
size={12}
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
{singularize(t("nameservers"), nameserversCount)}
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger
|
||||
value={"zones"}
|
||||
className={groupDetails === null ? "animate-pulse" : ""}
|
||||
>
|
||||
<DNSZoneIcon
|
||||
size={16}
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
{singularize(t("zones"), zonesCount)}
|
||||
</TabsTrigger>
|
||||
|
||||
{group.name !== "All" && (
|
||||
<TabsTrigger
|
||||
value={"setup-keys"}
|
||||
className={groupDetails === null ? "animate-pulse" : ""}
|
||||
>
|
||||
<SetupKeysIcon
|
||||
size={12}
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
{singularize(t("setupKeys"), setupKeysCount)}
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value={"users"} className={"pb-8"}>
|
||||
<GroupUsersSection users={groupDetails?.users} isLoading={isLoading} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={"peers"} className={"pb-8"}>
|
||||
<GroupPeersSection
|
||||
peers={groupDetails?.peersOfGroup}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={"policies"} className={"pb-8"}>
|
||||
<GroupPoliciesSection
|
||||
policies={groupDetails?.policies}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={"resources"} className={"pb-8"}>
|
||||
<GroupResourcesSection
|
||||
resources={groupDetails?.networkResources}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={"network-routes"} className={"pb-8"}>
|
||||
<GroupNetworkRoutesSection
|
||||
routes={groupDetails?.routes}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={"nameservers"} className={"pb-8"}>
|
||||
<GroupNameserversSection
|
||||
nameserverGroups={groupDetails?.nameservers}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={"zones"} className={"pb-8"}>
|
||||
<GroupDNSZonesSection
|
||||
zones={groupDetails?.zones}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={"setup-keys"} className={"pb-8"}>
|
||||
<GroupSetupKeysSection
|
||||
setupKeys={groupDetails?.setupKeys}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
8
src/app/(dashboard)/groups/layout.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Groups - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
53
src/app/(dashboard)/groups/page.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import { ExternalLinkIcon, FolderGit2Icon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import Breadcrumbs from "@/components/Breadcrumbs";
|
||||
import InlineLink from "@/components/InlineLink";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
|
||||
const GroupsTable = lazy(() => import("@/modules/groups/table/GroupsTable"));
|
||||
|
||||
export default function GroupsPage() {
|
||||
const t = useTranslations("groups");
|
||||
const { permission } = usePermissions();
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/groups"}
|
||||
label={t("title")}
|
||||
icon={<FolderGit2Icon size={14} />}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>{t("title")}</h1>
|
||||
<Paragraph>
|
||||
{t("groupsDescription")}{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/manage-network-access"}
|
||||
target={"_blank"}
|
||||
>
|
||||
{t("learnMore")}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<RestrictedAccess hasAccess={permission.groups.read} page={t("title")}>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<GroupsTable headingTarget={portalTarget} />
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -7,8 +7,9 @@ import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import { ArrowUpRightIcon, ExternalLinkIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { lazy, Suspense } from "react";
|
||||
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
|
||||
import PeersProvider from "@/contexts/PeersProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
@@ -16,63 +17,74 @@ import RoutesProvider from "@/contexts/RoutesProvider";
|
||||
import { Route } from "@/interfaces/Route";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import useGroupedRoutes from "@/modules/route-group/useGroupedRoutes";
|
||||
import { Callout } from "@components/Callout";
|
||||
|
||||
const NetworkRoutesTable = lazy(
|
||||
() => import("@/modules/route-group/NetworkRoutesTable"),
|
||||
() => import("@/modules/route-group/NetworkRoutesTable"),
|
||||
);
|
||||
|
||||
export default function NetworkRoutes() {
|
||||
const { permission } = usePermissions();
|
||||
const { data: routes, isLoading } = useFetchApi<Route[]>("/routes");
|
||||
const groupedRoutes = useGroupedRoutes({ routes });
|
||||
const t = useTranslations("networks");
|
||||
const tCommon = useTranslations("common");
|
||||
const { permission } = usePermissions();
|
||||
const { data: routes, isLoading } = useFetchApi<Route[]>("/routes");
|
||||
const groupedRoutes = useGroupedRoutes({ routes });
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<RoutesProvider>
|
||||
<PeersProvider>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/network-routes"}
|
||||
label={"Network Routes"}
|
||||
icon={<NetworkRoutesIcon size={13} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>Network Routes</h1>
|
||||
<Paragraph>
|
||||
Network routes allow you to access other networks like LANs and
|
||||
VPCs without installing NetBird on every resource.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/routing-traffic-to-private-networks"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
Network Routes
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
</div>
|
||||
return (
|
||||
<PageContainer>
|
||||
<RoutesProvider>
|
||||
<PeersProvider>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
label={t("networkRoutes")}
|
||||
icon={<NetworkRoutesIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item href={"/network-routes"} label={t("routes")} />
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>{t("routes")}</h1>
|
||||
<Paragraph>
|
||||
{t("routesDescription")}{" "}
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/routing-traffic-to-private-networks"
|
||||
}
|
||||
target={"_blank"}
|
||||
aria-label={
|
||||
"Learn more about routing traffic to private networks"
|
||||
}
|
||||
>
|
||||
<>{tCommon("learnMore")}</>
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
|
||||
<RestrictedAccess hasAccess={permission.routes.read}>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<NetworkRoutesTable
|
||||
isLoading={isLoading}
|
||||
groupedRoutes={groupedRoutes}
|
||||
routes={routes}
|
||||
headingTarget={portalTarget}
|
||||
/>
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</PeersProvider>
|
||||
</RoutesProvider>
|
||||
</PageContainer>
|
||||
);
|
||||
<Callout className={"max-w-xl mt-5"} variant={"warning"}>
|
||||
<span>
|
||||
{t("newNetworksRecommendation")}{" "}
|
||||
<InlineLink href={"/networks"}>
|
||||
{t("goToNetworks")}
|
||||
<ArrowUpRightIcon size={14} />
|
||||
</InlineLink>
|
||||
</span>
|
||||
</Callout>
|
||||
</div>
|
||||
|
||||
<RestrictedAccess hasAccess={permission.routes.read}>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<NetworkRoutesTable
|
||||
isLoading={isLoading}
|
||||
groupedRoutes={groupedRoutes}
|
||||
routes={routes}
|
||||
headingTarget={portalTarget}
|
||||
/>
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</PeersProvider>
|
||||
</RoutesProvider>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,34 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import Button from "@components/Button";
|
||||
import Card from "@components/Card";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@components/DropdownMenu";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Separator from "@components/Separator";
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import useRedirect from "@hooks/useRedirect";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { cn, singularize } from "@utils/helpers";
|
||||
import {
|
||||
ArrowUpRightIcon,
|
||||
HelpCircle,
|
||||
Layers3Icon,
|
||||
MoreVertical,
|
||||
PencilLineIcon,
|
||||
ServerIcon,
|
||||
ShieldCheckIcon,
|
||||
ShieldXIcon,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { useMemo } from "react";
|
||||
import useUrlTab from "@/hooks/useUrlTab";
|
||||
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { Network } from "@/interfaces/Network";
|
||||
import { Network, NetworkResource, NetworkRouter } from "@/interfaces/Network";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import { NetworkInformationSquare } from "@/modules/networks/misc/NetworkInformationSquare";
|
||||
import NetworkModal from "@/modules/networks/NetworkModal";
|
||||
import { NetworkProvider } from "@/modules/networks/NetworkProvider";
|
||||
import { ResourcesSection } from "@/modules/networks/resources/ResourcesSection";
|
||||
import { NetworkRoutingPeersSection } from "@/modules/networks/routing-peers/NetworkRoutingPeersSection";
|
||||
import { NetworkAccessControlProvider } from "@/modules/networks/NetworkAccessControlProvider";
|
||||
import {
|
||||
NetworkProvider,
|
||||
useNetworksContext,
|
||||
} from "@/modules/networks/NetworkProvider";
|
||||
import { ResourcesTabContent } from "@/modules/networks/resources/ResourcesTabContent";
|
||||
import { NetworkRoutingPeersTabContent } from "@/modules/networks/routing-peers/NetworkRoutingPeersTabContent";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
|
||||
import PeerIcon from "@/assets/icons/PeerIcon";
|
||||
import ReverseProxyIcon from "@/assets/icons/ReverseProxyIcon";
|
||||
import { ReverseProxyFlatTargetsTabContent } from "@/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetsTabContent";
|
||||
import ReverseProxiesProvider, {
|
||||
flattenReverseProxies,
|
||||
useReverseProxies,
|
||||
} from "@/contexts/ReverseProxiesProvider";
|
||||
import { SkeletonNetwork } from "@components/skeletons/SkeletonNetwork";
|
||||
|
||||
export default function NetworkDetailPage() {
|
||||
const queryParameter = useSearchParams();
|
||||
@@ -41,17 +63,36 @@ export default function NetworkDetailPage() {
|
||||
useRedirect("/networks", false, !networkId);
|
||||
|
||||
return network && !isLoading ? (
|
||||
<NetworkOverview network={network} />
|
||||
<ReverseProxiesProvider initialNetwork={network}>
|
||||
<NetworkOverview network={network} />
|
||||
</ReverseProxiesProvider>
|
||||
) : (
|
||||
<FullScreenLoading />
|
||||
<SkeletonNetwork />
|
||||
);
|
||||
}
|
||||
|
||||
function NetworkOverview({ network }: Readonly<{ network: Network }>) {
|
||||
const t = useTranslations("networks");
|
||||
const tReverseProxy = useTranslations("reverseProxy");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const [networkModal, setNetworkModal] = useState(false);
|
||||
const { mutate } = useSWRConfig();
|
||||
const { data: resources, isLoading: isResourcesLoading } = useFetchApi<
|
||||
NetworkResource[]
|
||||
>(`/networks/${network.id}/resources`);
|
||||
const { data: routers, isLoading: isRoutersLoading } = useFetchApi<
|
||||
NetworkRouter[]
|
||||
>(`/networks/${network.id}/routers`);
|
||||
|
||||
const { reverseProxies, isLoading: isServicesLoading } = useReverseProxies();
|
||||
const services = useMemo(
|
||||
() => flattenReverseProxies({ reverseProxies, network }),
|
||||
[reverseProxies, network],
|
||||
);
|
||||
|
||||
const [tab, setTab] = useUrlTab(
|
||||
["resources", "routing-peers", "services"],
|
||||
"resources",
|
||||
);
|
||||
|
||||
const isActive = !!(
|
||||
network?.routing_peers_count && network.routing_peers_count > 0
|
||||
@@ -59,72 +100,160 @@ function NetworkOverview({ network }: Readonly<{ network: Network }>) {
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<NetworkProvider network={network}>
|
||||
<div className={"p-default py-6 mb-4"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/networks"}
|
||||
label={"Networks"}
|
||||
disabled={!permission.networks.read}
|
||||
icon={<NetworkRoutesIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/network"}
|
||||
label={network.name}
|
||||
active={true}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<NetworkAccessControlProvider>
|
||||
<NetworkProvider network={network}>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/networks"}
|
||||
label={t("title")}
|
||||
disabled={!permission.networks.read}
|
||||
icon={<NetworkRoutesIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/network"}
|
||||
label={network.name}
|
||||
active={true}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
|
||||
<div className={"flex justify-between max-w-6xl"}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center",
|
||||
!network.description && "gap-2",
|
||||
)}
|
||||
>
|
||||
<NetworkInformationSquare
|
||||
name={network.name}
|
||||
active={isActive}
|
||||
size={"lg"}
|
||||
description={network.description}
|
||||
/>
|
||||
{permission.networks.update && (
|
||||
<button
|
||||
className={
|
||||
"flex items-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 py-2 px-3 rounded-md cursor-pointer"
|
||||
}
|
||||
onClick={() => setNetworkModal(true)}
|
||||
<div className={"flex justify-between max-w-6xl"}>
|
||||
<div
|
||||
className={"w-full lg:w-1/2 flex justify-between items-center"}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center w-full",
|
||||
!network.description && "gap-2",
|
||||
)}
|
||||
>
|
||||
<PencilLineIcon size={18} />
|
||||
</button>
|
||||
)}
|
||||
<NetworkModal
|
||||
open={networkModal}
|
||||
setOpen={setNetworkModal}
|
||||
onUpdated={() => {
|
||||
mutate(`/networks/${network.id}`);
|
||||
}}
|
||||
network={network}
|
||||
/>
|
||||
<NetworkInformationSquare
|
||||
name={network.name}
|
||||
active={isActive}
|
||||
size={"lg"}
|
||||
description={network.description}
|
||||
/>
|
||||
</div>
|
||||
<NetworkActions />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={"flex gap-10 w-full mt-8 max-w-6xl items-start"}>
|
||||
<NetworkInformationCard network={network} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={"flex gap-10 w-full mt-8 max-w-6xl items-start"}>
|
||||
<NetworkInformationCard network={network} />
|
||||
</div>
|
||||
</div>
|
||||
<Tabs
|
||||
defaultValue={tab}
|
||||
onValueChange={setTab}
|
||||
value={tab}
|
||||
className={"pb-0 mb-0"}
|
||||
>
|
||||
<TabsList justify={"start"} className={"px-8"}>
|
||||
<TabsTrigger value={"resources"}>
|
||||
<Layers3Icon size={14} />
|
||||
{singularize(t("resources"), network?.resources?.length)}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={"routing-peers"}>
|
||||
<PeerIcon
|
||||
size={12}
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
{singularize(t("routingPeers"), network?.routing_peers_count)}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={"services"}>
|
||||
<ReverseProxyIcon
|
||||
size={16}
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
{singularize(tReverseProxy("services"), services.length)}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<Separator />
|
||||
<ResourcesSection network={network} />
|
||||
<div className={"h-3"} />
|
||||
<Separator />
|
||||
<NetworkRoutingPeersSection network={network} />
|
||||
</NetworkProvider>
|
||||
<TabsContent value={"resources"} className={"pb-8"}>
|
||||
<ResourcesTabContent
|
||||
data={resources}
|
||||
isLoading={isResourcesLoading}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={"routing-peers"} className={"pb-8"}>
|
||||
<NetworkRoutingPeersTabContent
|
||||
routers={routers}
|
||||
isLoading={isRoutersLoading}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={"services"} className={"pb-8"}>
|
||||
<ReverseProxyFlatTargetsTabContent
|
||||
targets={services}
|
||||
isLoading={isServicesLoading}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</NetworkProvider>
|
||||
</NetworkAccessControlProvider>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function NetworkActions() {
|
||||
const { permission } = usePermissions();
|
||||
const { deleteNetwork, openEditNetworkModal, network } = useNetworksContext();
|
||||
const router = useRouter();
|
||||
|
||||
if (!network) return;
|
||||
|
||||
return (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger
|
||||
asChild={true}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Button variant={"secondary"} className={"!px-3"}>
|
||||
<MoreVertical size={16} className={"shrink-0"} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-auto" align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => openEditNetworkModal(network)}
|
||||
disabled={!permission.networks.update}
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<PencilLineIcon size={14} className={"shrink-0"} />
|
||||
Rename
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
deleteNetwork(network).then(() => router.push("/networks"))
|
||||
}
|
||||
variant={"danger"}
|
||||
disabled={!permission.networks.delete}
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<Trash2 size={14} className={"shrink-0"} />
|
||||
Delete
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
function NetworkInformationCard({ network }: Readonly<{ network: Network }>) {
|
||||
const t = useTranslations("networks");
|
||||
const tCommon = useTranslations("common");
|
||||
const isHighlyAvailable = !!(
|
||||
network?.routing_peers_count && network?.routing_peers_count >= 2
|
||||
);
|
||||
@@ -133,28 +262,32 @@ function NetworkInformationCard({ network }: Readonly<{ network: Network }>) {
|
||||
() => (
|
||||
<>
|
||||
High availability is currently{" "}
|
||||
<span className={"text-yellow-400 font-medium"}>inactive</span> for this
|
||||
network.
|
||||
<span className={"text-yellow-400 font-medium"}>
|
||||
{tCommon("inactive")}
|
||||
</span>{" "}
|
||||
for this network.
|
||||
</>
|
||||
),
|
||||
[],
|
||||
[tCommon],
|
||||
);
|
||||
|
||||
const enabledText = useMemo(
|
||||
() => (
|
||||
<>
|
||||
High availability is{" "}
|
||||
<span className={"text-green-500 font-medium"}>active</span> for this
|
||||
network.
|
||||
<span className={"text-green-500 font-medium"}>
|
||||
{tCommon("active")}
|
||||
</span>{" "}
|
||||
for this network.
|
||||
</>
|
||||
),
|
||||
[],
|
||||
[tCommon],
|
||||
);
|
||||
|
||||
const policyCount = network.policies?.length ?? 0;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card className={"w-full lg:w-1/2"}>
|
||||
<Card.List>
|
||||
<Card.ListItem
|
||||
tooltip={false}
|
||||
@@ -195,7 +328,7 @@ function NetworkInformationCard({ network }: Readonly<{ network: Network }>) {
|
||||
!isHighlyAvailable ? "bg-yellow-400" : "bg-green-500",
|
||||
)}
|
||||
></span>
|
||||
{isHighlyAvailable ? "Active" : "Inactive"}
|
||||
{isHighlyAvailable ? tCommon("active") : tCommon("inactive")}
|
||||
<HelpCircle size={12} />
|
||||
</div>
|
||||
</FullTooltip>
|
||||
@@ -207,20 +340,19 @@ function NetworkInformationCard({ network }: Readonly<{ network: Network }>) {
|
||||
policyCount > 0 ? (
|
||||
<>
|
||||
<ShieldCheckIcon size={16} className={"text-green-500"} />
|
||||
{policyCount}{" "}
|
||||
{policyCount === 1 ? "Active Policy" : "Active Policies"}
|
||||
{t("activePoliciesCount", { count: policyCount })}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ShieldXIcon size={16} className={"text-red-500"} />
|
||||
No Active Policies
|
||||
{t("noActivePolicies")}
|
||||
</>
|
||||
)
|
||||
}
|
||||
value={
|
||||
policyCount > 0 ? (
|
||||
<InlineLink href={"/access-control"}>
|
||||
Go to Policies
|
||||
{t("goToPolicies")}
|
||||
<ArrowUpRightIcon size={14} />
|
||||
</InlineLink>
|
||||
) : null
|
||||
|
||||
@@ -8,6 +8,7 @@ import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { Suspense } from "react";
|
||||
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
@@ -16,6 +17,9 @@ import PageContainer from "@/layouts/PageContainer";
|
||||
import NetworksTable from "@/modules/networks/table/NetworksTable";
|
||||
|
||||
export default function Networks() {
|
||||
const t = useTranslations("networks");
|
||||
const tCommon = useTranslations("common");
|
||||
const tNavigation = useTranslations("navigation");
|
||||
const { data: networks, isLoading } = useFetchApi<Network[]>("/networks");
|
||||
const { permission } = usePermissions();
|
||||
const { ref: headingRef, portalTarget } =
|
||||
@@ -26,26 +30,21 @@ export default function Networks() {
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/networks"}
|
||||
label={"Networks"}
|
||||
label={tNavigation("networkRouting")}
|
||||
icon={<NetworkRoutesIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item href={"/networks"} label={t("title")} />
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>Networks</h1>
|
||||
<h1 ref={headingRef}>{t("title")}</h1>
|
||||
<Paragraph>
|
||||
Networks allow you to access internal resources in LANs and VPCs
|
||||
without installing NetBird on every machine.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Learn more about
|
||||
{t("pageDescription")}{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/networks"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Networks
|
||||
{tCommon("learnMore")}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,116 +1,5 @@
|
||||
"use client";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import PeerIcon from "@/assets/icons/PeerIcon";
|
||||
import PeersProvider, { usePeers } from "@/contexts/PeersProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { useUsers } from "@/contexts/UsersProvider";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import { SetupModalContent } from "@/modules/setup-netbird-modal/SetupModal";
|
||||
|
||||
const PeersTable = lazy(() => import("@/modules/peers/PeersTable"));
|
||||
|
||||
export default function Peers() {
|
||||
const { isRestricted } = usePermissions();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{isRestricted ? (
|
||||
<PeersBlockedView />
|
||||
) : (
|
||||
<PeersProvider>
|
||||
<PeersView />
|
||||
</PeersProvider>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function PeersView() {
|
||||
const { peers, isLoading } = usePeers();
|
||||
const { users } = useUsers();
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
const peersWithUser = peers?.map((peer) => {
|
||||
if (!users) return peer;
|
||||
return {
|
||||
...peer,
|
||||
user: users?.find((user) => user.id === peer.user_id),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/peers"}
|
||||
label={"Peers"}
|
||||
icon={<PeerIcon size={13} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>Peers</h1>
|
||||
<Paragraph>
|
||||
A list of all machines and devices connected to your private network.
|
||||
Use this view to manage peers.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Learn more about{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/add-machines-to-your-network"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Peers
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<PeersTable
|
||||
isLoading={isLoading}
|
||||
peers={peersWithUser}
|
||||
headingTarget={portalTarget}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function PeersBlockedView() {
|
||||
return (
|
||||
<div className={"flex items-center justify-center flex-col"}>
|
||||
<div className={"p-default py-6 max-w-3xl text-center"}>
|
||||
<h1>Add new device to your network</h1>
|
||||
<Paragraph className={"inline"}>
|
||||
To get started, install NetBird and log in using your email account.
|
||||
After that you should be connected. If you have further questions
|
||||
check out our{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/getting-started#installation"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Installation Guide
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"px-3 pt-1 pb-8 max-w-3xl w-full"}>
|
||||
<div
|
||||
className={
|
||||
"rounded-md border border-nb-gray-900/70 grid w-full bg-nb-gray-930/40"
|
||||
}
|
||||
>
|
||||
<SetupModalContent header={false} footer={false} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
export default function PeersIndex() {
|
||||
redirect("/peers/users");
|
||||
}
|
||||
|
||||
8
src/app/(dashboard)/peers/servers/layout.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Servers - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
125
src/app/(dashboard)/peers/servers/page.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
"use client";
|
||||
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { lazy, Suspense, useMemo } from "react";
|
||||
import PeerIcon from "@/assets/icons/PeerIcon";
|
||||
import PeersProvider, { usePeers } from "@/contexts/PeersProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { useUsers } from "@/contexts/UsersProvider";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import { SetupModalContent } from "@/modules/setup-netbird-modal/SetupModal";
|
||||
|
||||
const PeersTable = lazy(() => import("@/modules/peers/PeersTable"));
|
||||
|
||||
export default function ServersPage() {
|
||||
const { isRestricted } = usePermissions();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{isRestricted ? (
|
||||
<ServersBlockedView />
|
||||
) : (
|
||||
<PeersProvider>
|
||||
<ServersView />
|
||||
</PeersProvider>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function ServersView() {
|
||||
const t = useTranslations("peers");
|
||||
const { peers, isLoading: isPeersLoading } = usePeers();
|
||||
const { users, isLoading: isUsersLoading } = useUsers();
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
// The kind filter classifies peers by whether their owner is a real
|
||||
// user vs a service/no-user, so we must wait until both peers and
|
||||
// users have loaded before joining them — otherwise peers temporarily
|
||||
// render with peer.user === undefined and get misclassified.
|
||||
const isLoading = isPeersLoading || isUsersLoading;
|
||||
const peersWithUser = useMemo(() => {
|
||||
if (!peers || !users) return undefined;
|
||||
return peers.map((peer) => ({
|
||||
...peer,
|
||||
user: users.find((u) => u.id === peer.user_id),
|
||||
}));
|
||||
}, [peers, users]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item label={t("title")} icon={<PeerIcon size={13} />} />
|
||||
<Breadcrumbs.Item
|
||||
href={"/peers/servers"}
|
||||
label={t("servers")}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>{t("servers")}</h1>
|
||||
<Paragraph>
|
||||
{t("serversDescription")}{" "}
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/register-machines-using-setup-keys"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
{t("learnMore")}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<PeersTable
|
||||
isLoading={isLoading}
|
||||
peers={peersWithUser}
|
||||
headingTarget={portalTarget}
|
||||
kind={"servers"}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ServersBlockedView() {
|
||||
const t = useTranslations("peers");
|
||||
return (
|
||||
<div className={"flex items-center justify-center flex-col"}>
|
||||
<div className={"p-default py-6 max-w-3xl text-center"}>
|
||||
<h1>{t("addNewServerTitle")}</h1>
|
||||
<Paragraph className={"inline"}>
|
||||
{t("addNewServerDescription")}{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/getting-started#installation"}
|
||||
target={"_blank"}
|
||||
>
|
||||
{t("installationGuide")}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"px-3 pt-1 pb-8 max-w-3xl w-full"}>
|
||||
<div
|
||||
className={
|
||||
"rounded-md border border-nb-gray-900/70 grid w-full bg-nb-gray-930/40 stepper-bg-variant"
|
||||
}
|
||||
>
|
||||
<SetupModalContent
|
||||
header={false}
|
||||
footer={false}
|
||||
isUserDevice={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
8
src/app/(dashboard)/peers/users/layout.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `User Devices - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
119
src/app/(dashboard)/peers/users/page.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { lazy, Suspense, useMemo } from "react";
|
||||
import PeerIcon from "@/assets/icons/PeerIcon";
|
||||
import PeersProvider, { usePeers } from "@/contexts/PeersProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { useUsers } from "@/contexts/UsersProvider";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import { SetupModalContent } from "@/modules/setup-netbird-modal/SetupModal";
|
||||
|
||||
const PeersTable = lazy(() => import("@/modules/peers/PeersTable"));
|
||||
|
||||
export default function UserDevicesPage() {
|
||||
const { isRestricted } = usePermissions();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{isRestricted ? (
|
||||
<UserDevicesBlockedView />
|
||||
) : (
|
||||
<PeersProvider>
|
||||
<UserDevicesView />
|
||||
</PeersProvider>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function UserDevicesView() {
|
||||
const t = useTranslations("peers");
|
||||
const { peers, isLoading: isPeersLoading } = usePeers();
|
||||
const { users, isLoading: isUsersLoading } = useUsers();
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
// The kind filter classifies peers by whether their owner is a real
|
||||
// user vs a service/no-user, so we must wait until both peers and
|
||||
// users have loaded before joining them — otherwise peers temporarily
|
||||
// render with peer.user === undefined and get misclassified.
|
||||
const isLoading = isPeersLoading || isUsersLoading;
|
||||
const peersWithUser = useMemo(() => {
|
||||
if (!peers || !users) return undefined;
|
||||
return peers.map((peer) => ({
|
||||
...peer,
|
||||
user: users.find((u) => u.id === peer.user_id),
|
||||
}));
|
||||
}, [peers, users]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item label={t("title")} icon={<PeerIcon size={13} />} />
|
||||
<Breadcrumbs.Item
|
||||
href={"/peers/users"}
|
||||
label={t("userDevices")}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>{t("userDevices")}</h1>
|
||||
<Paragraph>
|
||||
{t("userDevicesDescription")}{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/add-machines-to-your-network"}
|
||||
target={"_blank"}
|
||||
>
|
||||
{t("learnMore")}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<PeersTable
|
||||
isLoading={isLoading}
|
||||
peers={peersWithUser}
|
||||
headingTarget={portalTarget}
|
||||
kind={"users"}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function UserDevicesBlockedView() {
|
||||
const t = useTranslations("peers");
|
||||
return (
|
||||
<div className={"flex items-center justify-center flex-col"}>
|
||||
<div className={"p-default py-6 max-w-3xl text-center"}>
|
||||
<h1>{t("addNewDeviceTitle")}</h1>
|
||||
<Paragraph className={"inline"}>
|
||||
{t("addNewDeviceDescription")}{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/getting-started#installation"}
|
||||
target={"_blank"}
|
||||
>
|
||||
{t("installationGuide")}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"px-3 pt-1 pb-8 max-w-3xl w-full"}>
|
||||
<div
|
||||
className={
|
||||
"rounded-md border border-nb-gray-900/70 grid w-full bg-nb-gray-930/40 stepper-bg-variant"
|
||||
}
|
||||
>
|
||||
<SetupModalContent header={false} footer={false} isUserDevice />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,76 +6,61 @@ import Paragraph from "@components/Paragraph";
|
||||
import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { ExternalLinkIcon, ShieldCheck } from "lucide-react";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { lazy, Suspense } from "react";
|
||||
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
|
||||
import GroupsProvider from "@/contexts/GroupsProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import PoliciesProvider from "@/contexts/PoliciesProvider";
|
||||
import { PostureCheck } from "@/interfaces/PostureCheck";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import useFetchApi from "@utils/api";
|
||||
|
||||
const PostureCheckTable = lazy(
|
||||
() => import("@/modules/posture-checks/table/PostureCheckTable"),
|
||||
() => import("@/modules/posture-checks/table/PostureCheckTable"),
|
||||
);
|
||||
|
||||
export default function PostureChecksPage() {
|
||||
const { permission } = usePermissions();
|
||||
const { data: postureChecks, isLoading } =
|
||||
useFetchApi<PostureCheck[]>("/posture-checks");
|
||||
const t = useTranslations("postureChecks");
|
||||
const tCommon = useTranslations("common");
|
||||
const { permission } = usePermissions();
|
||||
const { data: postureChecks, isLoading } =
|
||||
useFetchApi<PostureCheck[]>("/posture-checks");
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<GroupsProvider>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/access-control"}
|
||||
label={"Access Control"}
|
||||
icon={<AccessControlIcon size={14} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/posture-checks"}
|
||||
label={"Posture Checks"}
|
||||
active
|
||||
icon={<ShieldCheck size={15} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>Posture Checks</h1>
|
||||
<Paragraph>
|
||||
Use posture checks to further restrict access in your network.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/manage-posture-checks"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Posture Checks
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<RestrictedAccess
|
||||
page={"Posture Checks"}
|
||||
hasAccess={permission.policies.read}
|
||||
>
|
||||
<PoliciesProvider>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<PostureCheckTable
|
||||
headingTarget={portalTarget}
|
||||
isLoading={isLoading}
|
||||
postureChecks={postureChecks}
|
||||
/>
|
||||
</Suspense>
|
||||
</PoliciesProvider>
|
||||
</RestrictedAccess>
|
||||
</GroupsProvider>
|
||||
</PageContainer>
|
||||
);
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/posture-checks"}
|
||||
label={t("title")}
|
||||
icon={<ShieldCheck size={14} />}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>{t("title")}</h1>
|
||||
<Paragraph>
|
||||
{t("pageDescription")}{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/manage-posture-checks"}
|
||||
target={"_blank"}
|
||||
>
|
||||
{tCommon("learnMore")}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<RestrictedAccess page={t("title")} hasAccess={permission.policies.read}>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<PostureCheckTable
|
||||
postureChecks={postureChecks}
|
||||
isLoading={isLoading}
|
||||
headingTarget={portalTarget}
|
||||
/>
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
8
src/app/(dashboard)/reverse-proxy/clusters/layout.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Clusters - Reverse Proxy - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
66
src/app/(dashboard)/reverse-proxy/clusters/page.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { lazy, Suspense } from "react";
|
||||
import ReverseProxyIcon from "@/assets/icons/ReverseProxyIcon";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import ReverseProxiesProvider from "@/contexts/ReverseProxiesProvider";
|
||||
import { REVERSE_PROXY_CLUSTERS_DOCS_LINK } from "@/interfaces/ReverseProxy";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
|
||||
const ClustersTable = lazy(
|
||||
() => import("@/modules/reverse-proxy/clusters/ClustersTable"),
|
||||
);
|
||||
|
||||
export default function ReverseProxyClustersPage() {
|
||||
const t = useTranslations("reverseProxy");
|
||||
const tCommon = useTranslations("common");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/reverse-proxy/services"}
|
||||
label={t("title")}
|
||||
icon={<ReverseProxyIcon size={16} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/reverse-proxy/clusters"}
|
||||
label={t("clusters")}
|
||||
active={true}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>{t("clusters")}</h1>
|
||||
<Paragraph>
|
||||
{t("clustersDescription")}{" "}
|
||||
<InlineLink href={REVERSE_PROXY_CLUSTERS_DOCS_LINK} target={"_blank"}>
|
||||
{tCommon("learnMore")}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
<RestrictedAccess
|
||||
page={t("clusters")}
|
||||
hasAccess={permission.services?.read}
|
||||
>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<ReverseProxiesProvider>
|
||||
<ClustersTable headingTarget={portalTarget} />
|
||||
</ReverseProxiesProvider>
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Custom Domains - Reverse Proxy - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
69
src/app/(dashboard)/reverse-proxy/custom-domains/page.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { lazy, Suspense } from "react";
|
||||
import ReverseProxyIcon from "@/assets/icons/ReverseProxyIcon";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import ReverseProxiesProvider from "@/contexts/ReverseProxiesProvider";
|
||||
import { REVERSE_PROXY_CUSTOM_DOMAINS_DOCS_LINK } from "@/interfaces/ReverseProxy";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
|
||||
const CustomDomainsTable = lazy(
|
||||
() => import("@/modules/reverse-proxy/domain/CustomDomainsTable"),
|
||||
);
|
||||
|
||||
export default function ReverseProxyCustomDomainsPage() {
|
||||
const t = useTranslations("reverseProxy");
|
||||
const tCommon = useTranslations("common");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/reverse-proxy/services"}
|
||||
label={t("title")}
|
||||
icon={<ReverseProxyIcon size={16} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/reverse-proxy/custom-domains"}
|
||||
label={t("customDomains")}
|
||||
active={true}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>{t("customDomains")}</h1>
|
||||
<Paragraph>
|
||||
{t("customDomainsDescription")}{" "}
|
||||
<InlineLink
|
||||
href={REVERSE_PROXY_CUSTOM_DOMAINS_DOCS_LINK}
|
||||
target={"_blank"}
|
||||
>
|
||||
{tCommon("learnMore")}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
<RestrictedAccess
|
||||
page={t("customDomains")}
|
||||
hasAccess={permission.services?.read}
|
||||
>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<ReverseProxiesProvider>
|
||||
<CustomDomainsTable headingTarget={portalTarget} />
|
||||
</ReverseProxiesProvider>
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
8
src/app/(dashboard)/reverse-proxy/logs/layout.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Access Logs - Reverse Proxy - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
63
src/app/(dashboard)/reverse-proxy/logs/page.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { lazy, Suspense } from "react";
|
||||
import ReverseProxyIcon from "@/assets/icons/ReverseProxyIcon";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { REVERSE_PROXY_EVENTS_DOCS_LINK } from "@/interfaces/ReverseProxy";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
|
||||
const ReverseProxyEventsTable = lazy(
|
||||
() => import("@/modules/reverse-proxy/events/ReverseProxyEventsTable"),
|
||||
);
|
||||
|
||||
export default function ProxyLogsPage() {
|
||||
const t = useTranslations("reverseProxy");
|
||||
const tCommon = useTranslations("common");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/reverse-proxy/services"}
|
||||
label={t("title")}
|
||||
icon={<ReverseProxyIcon size={16} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/reverse-proxy/logs"}
|
||||
label={t("accessLogs")}
|
||||
active={true}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>{t("accessLogs")}</h1>
|
||||
<Paragraph>
|
||||
{t("accessLogsDescription")}{" "}
|
||||
<InlineLink href={REVERSE_PROXY_EVENTS_DOCS_LINK} target={"_blank"}>
|
||||
{tCommon("learnMore")}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
<RestrictedAccess
|
||||
page={t("accessLogs")}
|
||||
hasAccess={permission.services?.read}
|
||||
>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<ReverseProxyEventsTable headingTarget={portalTarget} />
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
15
src/app/(dashboard)/reverse-proxy/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function ReverseProxyRedirectPage() {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
router.replace("/reverse-proxy/services");
|
||||
}, [router]);
|
||||
|
||||
return <FullScreenLoading fullScreen={false} />;
|
||||
}
|
||||
8
src/app/(dashboard)/reverse-proxy/services/layout.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Services - Reverse Proxy - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
79
src/app/(dashboard)/reverse-proxy/services/page.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import { Callout } from "@components/Callout";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import { isNetBirdHosted } from "@utils/netbird";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import ReverseProxyIcon from "@/assets/icons/ReverseProxyIcon";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import ReverseProxiesProvider from "@/contexts/ReverseProxiesProvider";
|
||||
import { REVERSE_PROXY_DOCS_LINK } from "@/interfaces/ReverseProxy";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
|
||||
const ReverseProxyTable = lazy(
|
||||
() => import("@/modules/reverse-proxy/table/ReverseProxyTable"),
|
||||
);
|
||||
|
||||
export default function ReverseProxyServicesPage() {
|
||||
const t = useTranslations("reverseProxy");
|
||||
const tCommon = useTranslations("common");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/reverse-proxy/services"}
|
||||
label={t("title")}
|
||||
icon={<ReverseProxyIcon size={16} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/reverse-proxy/services"}
|
||||
label={t("services")}
|
||||
active={true}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>{t("services")}</h1>
|
||||
<Paragraph>
|
||||
{t("servicesDescription")}{" "}
|
||||
<InlineLink href={REVERSE_PROXY_DOCS_LINK} target={"_blank"}>
|
||||
{tCommon("learnMore")}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
|
||||
{isNetBirdHosted() ? (
|
||||
<Callout className={"max-w-xl mt-5"} variant={"info"}>
|
||||
{t("betaNoticeCloud")}
|
||||
</Callout>
|
||||
) : (
|
||||
<Callout className={"max-w-xl mt-5"} variant={"info"}>
|
||||
{t("betaNoticeSelfHosted")}
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
<RestrictedAccess
|
||||
page={t("services")}
|
||||
hasAccess={permission.services?.read}
|
||||
>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<ReverseProxiesProvider>
|
||||
<ReverseProxyTable headingTarget={portalTarget} />
|
||||
</ReverseProxiesProvider>
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -3,13 +3,16 @@
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { VerticalTabs } from "@components/VerticalTabs";
|
||||
import {
|
||||
AlertOctagonIcon,
|
||||
FolderGit2Icon,
|
||||
LockIcon,
|
||||
MonitorSmartphoneIcon,
|
||||
NetworkIcon,
|
||||
ShieldIcon,
|
||||
AlertOctagonIcon,
|
||||
FingerprintIcon,
|
||||
FolderGit2Icon,
|
||||
KeyRound,
|
||||
LockIcon,
|
||||
MonitorSmartphoneIcon,
|
||||
NetworkIcon,
|
||||
ShieldIcon,
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
@@ -19,88 +22,108 @@ import { useAccount } from "@/modules/account/useAccount";
|
||||
import AuthenticationTab from "@/modules/settings/AuthenticationTab";
|
||||
import ClientSettingsTab from "@/modules/settings/ClientSettingsTab";
|
||||
import DangerZoneTab from "@/modules/settings/DangerZoneTab";
|
||||
import GroupsTab from "@/modules/settings/GroupsTab";
|
||||
import IdentityProvidersTab from "@/modules/settings/IdentityProvidersTab";
|
||||
import NetworkSettingsTab from "@/modules/settings/NetworkSettingsTab";
|
||||
import PermissionsTab from "@/modules/settings/PermissionsTab";
|
||||
import SetupKeysTab from "@/modules/settings/SetupKeysTab";
|
||||
import GroupsSettings from "@/modules/settings/GroupsSettings";
|
||||
|
||||
export default function NetBirdSettings() {
|
||||
const queryParams = useSearchParams();
|
||||
const queryTab = queryParams.get("tab");
|
||||
const { permission } = usePermissions();
|
||||
const t = useTranslations("settings");
|
||||
const queryParams = useSearchParams();
|
||||
const queryTab = queryParams.get("tab");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const initialTab = useMemo(() => {
|
||||
if (permission.settings.read) return "authentication";
|
||||
return "authentication";
|
||||
}, [permission]);
|
||||
const initialTab = useMemo(() => {
|
||||
if (permission.settings.read) return "authentication";
|
||||
return "authentication";
|
||||
}, [permission]);
|
||||
|
||||
const [tab, setTab] = useState(queryTab ?? initialTab);
|
||||
const [tab, setTab] = useState(queryTab ?? initialTab);
|
||||
|
||||
const account = useAccount();
|
||||
const account = useAccount();
|
||||
|
||||
useEffect(() => {
|
||||
if (queryTab) {
|
||||
setTab(queryTab);
|
||||
}
|
||||
}, [queryTab]);
|
||||
useEffect(() => {
|
||||
if (queryTab) {
|
||||
setTab(queryTab);
|
||||
}
|
||||
}, [queryTab]);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<VerticalTabs value={tab} onChange={setTab}>
|
||||
<VerticalTabs.List>
|
||||
{permission.settings.read && (
|
||||
<>
|
||||
<VerticalTabs.Trigger value="authentication">
|
||||
<ShieldIcon size={14} />
|
||||
Authentication
|
||||
</VerticalTabs.Trigger>
|
||||
<VerticalTabs.Trigger value="groups">
|
||||
<FolderGit2Icon size={14} />
|
||||
Groups
|
||||
</VerticalTabs.Trigger>
|
||||
<VerticalTabs.Trigger value="permissions">
|
||||
<LockIcon size={14} />
|
||||
Permissions
|
||||
</VerticalTabs.Trigger>
|
||||
<VerticalTabs.Trigger value="networks">
|
||||
<NetworkIcon size={14} />
|
||||
Networks
|
||||
</VerticalTabs.Trigger>
|
||||
<VerticalTabs.Trigger value="clients">
|
||||
<MonitorSmartphoneIcon size={14} />
|
||||
Clients
|
||||
</VerticalTabs.Trigger>
|
||||
</>
|
||||
)}
|
||||
return (
|
||||
<PageContainer>
|
||||
<VerticalTabs value={tab} onChange={setTab}>
|
||||
<VerticalTabs.List>
|
||||
{permission.settings.read && (
|
||||
<>
|
||||
<VerticalTabs.Trigger value="authentication">
|
||||
<ShieldIcon size={14} />
|
||||
{t("authentication")}
|
||||
</VerticalTabs.Trigger>
|
||||
{permission.setup_keys.read && (
|
||||
<VerticalTabs.Trigger value="setup-keys">
|
||||
<KeyRound size={14} />
|
||||
{t("setupKeys")}
|
||||
</VerticalTabs.Trigger>
|
||||
)}
|
||||
{account?.settings?.embedded_idp_enabled &&
|
||||
permission?.identity_providers?.read && (
|
||||
<VerticalTabs.Trigger value="identity-providers">
|
||||
<FingerprintIcon size={14} />
|
||||
{t("identityProviders")}
|
||||
</VerticalTabs.Trigger>
|
||||
)}
|
||||
<VerticalTabs.Trigger value="groups">
|
||||
<FolderGit2Icon size={14} />
|
||||
{t("groupsTab")}
|
||||
</VerticalTabs.Trigger>
|
||||
<VerticalTabs.Trigger value="permissions">
|
||||
<LockIcon size={14} />
|
||||
{t("permissions")}
|
||||
</VerticalTabs.Trigger>
|
||||
<VerticalTabs.Trigger value="networks">
|
||||
<NetworkIcon size={14} />
|
||||
{t("networksTab")}
|
||||
</VerticalTabs.Trigger>
|
||||
<VerticalTabs.Trigger value="clients">
|
||||
<MonitorSmartphoneIcon size={14} />
|
||||
{t("clients")}
|
||||
</VerticalTabs.Trigger>
|
||||
</>
|
||||
)}
|
||||
|
||||
<DangerZoneTabTrigger />
|
||||
</VerticalTabs.List>
|
||||
<RestrictedAccess
|
||||
page={"Settings"}
|
||||
hasAccess={permission.settings.read}
|
||||
>
|
||||
<div className={"border-l border-nb-gray-930 w-full"}>
|
||||
{account && <AuthenticationTab account={account} />}
|
||||
{account && <PermissionsTab account={account} />}
|
||||
{account && <GroupsTab account={account} />}
|
||||
{account && <NetworkSettingsTab account={account} />}
|
||||
{account && <ClientSettingsTab account={account} />}
|
||||
{account && <DangerZoneTab account={account} />}
|
||||
</div>
|
||||
</RestrictedAccess>
|
||||
</VerticalTabs>
|
||||
</PageContainer>
|
||||
);
|
||||
<DangerZoneTabTrigger />
|
||||
</VerticalTabs.List>
|
||||
<RestrictedAccess
|
||||
page={t("title")}
|
||||
hasAccess={permission.settings.read}
|
||||
>
|
||||
<div className={"border-l border-nb-gray-930 w-full"}>
|
||||
{account && <AuthenticationTab account={account} />}
|
||||
{permission.setup_keys.read && <SetupKeysTab />}
|
||||
{account?.settings?.embedded_idp_enabled &&
|
||||
permission.identity_providers.read && <IdentityProvidersTab />}
|
||||
{account && <PermissionsTab account={account} />}
|
||||
{account && <GroupsSettings account={account} />}
|
||||
{account && <NetworkSettingsTab account={account} />}
|
||||
{account && <ClientSettingsTab account={account} />}
|
||||
{account && <DangerZoneTab account={account} />}
|
||||
</div>
|
||||
</RestrictedAccess>
|
||||
</VerticalTabs>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const DangerZoneTabTrigger = () => {
|
||||
const { isOwner } = useLoggedInUser();
|
||||
const t = useTranslations("settings");
|
||||
const { isOwner } = useLoggedInUser();
|
||||
|
||||
return (
|
||||
isOwner && (
|
||||
<VerticalTabs.Trigger value="danger-zone" disabled={!isOwner}>
|
||||
<AlertOctagonIcon size={14} />
|
||||
Danger zone
|
||||
</VerticalTabs.Trigger>
|
||||
)
|
||||
);
|
||||
return (
|
||||
isOwner && (
|
||||
<VerticalTabs.Trigger value="danger-zone" disabled={!isOwner}>
|
||||
<AlertOctagonIcon size={14} />
|
||||
{t("dangerZone")}
|
||||
</VerticalTabs.Trigger>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,90 +1,5 @@
|
||||
"use client";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import React, { lazy, Suspense, useMemo } from "react";
|
||||
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
|
||||
import { useGroups } from "@/contexts/GroupsProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
import { SetupKey } from "@/interfaces/SetupKey";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
|
||||
const SetupKeysTable = lazy(
|
||||
() => import("@/modules/setup-keys/SetupKeysTable"),
|
||||
);
|
||||
|
||||
export default function SetupKeys() {
|
||||
const { data: setupKeys, isLoading } = useFetchApi<SetupKey[]>("/setup-keys");
|
||||
const { permission } = usePermissions();
|
||||
const { groups } = useGroups();
|
||||
|
||||
const setupKeysWithGroups = useMemo(() => {
|
||||
if (!setupKeys) return [];
|
||||
return setupKeys?.map((setupKey) => {
|
||||
if (!setupKey.auto_groups) return setupKey;
|
||||
if (!groups) return setupKey;
|
||||
return {
|
||||
...setupKey,
|
||||
groups: setupKey.auto_groups
|
||||
?.map((group) => {
|
||||
return groups.find((g) => g.id === group) || undefined;
|
||||
})
|
||||
.filter((group) => group !== undefined) as Group[],
|
||||
};
|
||||
});
|
||||
}, [setupKeys, groups]);
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/setup-keys"}
|
||||
label={"Setup Keys"}
|
||||
icon={<SetupKeysIcon size={13} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>Setup Keys</h1>
|
||||
<Paragraph>
|
||||
Setup keys are pre-authentication keys that allow to register new
|
||||
machines in your network.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/register-machines-using-setup-keys"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
Setup Keys
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<RestrictedAccess
|
||||
page={"Setup Keys"}
|
||||
hasAccess={permission.setup_keys.read}
|
||||
>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<SetupKeysTable
|
||||
headingTarget={portalTarget}
|
||||
setupKeys={setupKeysWithGroups}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
export default function SetupKeysIndex() {
|
||||
redirect("/settings?tab=setup-keys");
|
||||
}
|
||||
@@ -11,5 +11,5 @@ export default function Team() {
|
||||
router.push("/team/users");
|
||||
}, [router]);
|
||||
|
||||
return <FullScreenLoading height={"auto"} />;
|
||||
return <FullScreenLoading fullScreen={false} />;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import { IconSettings2 } from "@tabler/icons-react";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import TeamIcon from "@/assets/icons/TeamIcon";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
@@ -16,63 +17,57 @@ import { User } from "@/interfaces/User";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
|
||||
const ServiceUsersTable = lazy(
|
||||
() => import("@/modules/users/ServiceUsersTable"),
|
||||
() => import("@/modules/users/ServiceUsersTable"),
|
||||
);
|
||||
|
||||
export default function ServiceUsers() {
|
||||
const { permission } = usePermissions();
|
||||
const { data: users, isLoading } = useFetchApi<User[]>(
|
||||
"/users?service_user=true",
|
||||
);
|
||||
const t = useTranslations("serviceUsers");
|
||||
const tUsers = useTranslations("users");
|
||||
const { permission } = usePermissions();
|
||||
const { data: users, isLoading } = useFetchApi<User[]>(
|
||||
"/users?service_user=true",
|
||||
);
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/team"}
|
||||
label={"Team"}
|
||||
icon={<TeamIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/team/service-users"}
|
||||
label={"Service Users"}
|
||||
active
|
||||
icon={<IconSettings2 size={17} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>Service Users</h1>
|
||||
<Paragraph>
|
||||
Use service users to create API tokens and avoid losing automated
|
||||
access.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/access-netbird-public-api"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Service Users
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<RestrictedAccess
|
||||
page={"Service Users"}
|
||||
hasAccess={permission.users.read}
|
||||
>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<ServiceUsersTable
|
||||
users={users}
|
||||
isLoading={isLoading}
|
||||
headingTarget={portalTarget}
|
||||
/>
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</PageContainer>
|
||||
);
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/team"}
|
||||
label={tUsers("team")}
|
||||
icon={<TeamIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/team/service-users"}
|
||||
label={t("title")}
|
||||
active
|
||||
icon={<IconSettings2 size={17} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>{t("title")}</h1>
|
||||
<Paragraph>
|
||||
{t("serviceUsersDescription")}{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/access-netbird-public-api"}
|
||||
target={"_blank"}
|
||||
>
|
||||
{tUsers("learnMore")}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<RestrictedAccess page={t("title")} hasAccess={permission.users.read}>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<ServiceUsersTable
|
||||
users={users}
|
||||
isLoading={isLoading}
|
||||
headingTarget={portalTarget}
|
||||
/>
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { notify } from "@components/Notification";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import { PeerGroupSelector } from "@components/PeerGroupSelector";
|
||||
import Separator from "@components/Separator";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import useRedirect from "@hooks/useRedirect";
|
||||
@@ -16,7 +17,15 @@ import { IconCirclePlus, IconSettings2 } from "@tabler/icons-react";
|
||||
import useFetchApi, { useApiCall } from "@utils/api";
|
||||
import { generateColorFromString } from "@utils/helpers";
|
||||
import dayjs from "dayjs";
|
||||
import { Ban, GalleryHorizontalEnd, History, Mail, User2 } from "lucide-react";
|
||||
import {
|
||||
Ban,
|
||||
GalleryHorizontalEnd,
|
||||
History,
|
||||
KeyRoundIcon,
|
||||
Mail,
|
||||
MonitorSmartphoneIcon,
|
||||
User2,
|
||||
} from "lucide-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
@@ -33,6 +42,7 @@ import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
import { useGroupIdsToGroups } from "@/modules/groups/useGroupIdsToGroups";
|
||||
import UserBlockCell from "@/modules/users/table-cells/UserBlockCell";
|
||||
import UserStatusCell from "@/modules/users/table-cells/UserStatusCell";
|
||||
import { UserPeersSection } from "@/modules/users/UserPeersSection";
|
||||
import { UserRoleSelector } from "@/modules/users/UserRoleSelector";
|
||||
|
||||
export default function UserPage() {
|
||||
@@ -80,6 +90,7 @@ type Props = {
|
||||
function UserOverview({ user, initialGroups }: Readonly<Props>) {
|
||||
const router = useRouter();
|
||||
const userRequest = useApiCall<User>("/users");
|
||||
const isServiceUser = !!user?.is_service_user;
|
||||
const { mutate } = useSWRConfig();
|
||||
const { loggedInUser, isOwnerOrAdmin, isUser } = useLoggedInUser();
|
||||
const isLoggedInUser = loggedInUser ? loggedInUser?.id === user.id : false;
|
||||
@@ -91,7 +102,6 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
|
||||
});
|
||||
|
||||
const [role, setRole] = useState(user.role || Role.User);
|
||||
|
||||
const { hasChanges, updateRef: updateChangesRef } = useHasChanges([
|
||||
role,
|
||||
selectedGroups,
|
||||
@@ -114,13 +124,24 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
|
||||
`/${user.id}`,
|
||||
)
|
||||
.then(() => {
|
||||
mutate(`/users?service_user=${user.is_service_user}`);
|
||||
mutate(`/users?service_user=${isServiceUser}`);
|
||||
updateChangesRef([role, selectedGroups]);
|
||||
}),
|
||||
loadingMessage: "Saving changes...",
|
||||
});
|
||||
};
|
||||
|
||||
const isProfilePage = !!user?.is_current && !isServiceUser;
|
||||
const canViewTokens = permission?.pats?.read;
|
||||
const canViewPeers = permission?.peers?.read;
|
||||
|
||||
const showAccessTokens = (user?.is_current || isServiceUser) && canViewTokens;
|
||||
const showPeers = !isServiceUser && canViewPeers;
|
||||
const showTabs = isProfilePage && showPeers && showAccessTokens;
|
||||
const showSeparator = !showTabs;
|
||||
|
||||
const [tab, setTab] = useState(isServiceUser ? "access-tokens" : "peers");
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6 mb-4"}>
|
||||
@@ -132,7 +153,7 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
|
||||
icon={<TeamIcon size={13} />}
|
||||
/>
|
||||
|
||||
{user.is_service_user ? (
|
||||
{isServiceUser ? (
|
||||
<Breadcrumbs.Item
|
||||
href={"/team/service-users"}
|
||||
label={"Service Users"}
|
||||
@@ -158,7 +179,7 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
|
||||
"w-10 h-10 rounded-full relative flex items-center justify-center text-white uppercase text-md font-medium bg-nb-gray-900"
|
||||
}
|
||||
style={
|
||||
user.is_service_user
|
||||
isServiceUser
|
||||
? {
|
||||
color: "white",
|
||||
}
|
||||
@@ -171,13 +192,13 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
|
||||
}
|
||||
}
|
||||
>
|
||||
{user.is_service_user ? (
|
||||
{isServiceUser ? (
|
||||
<IconSettings2 size={16} />
|
||||
) : (
|
||||
user?.name?.charAt(0) || user?.id?.charAt(0)
|
||||
)}
|
||||
</div>
|
||||
<h1 className={"flex items-center gap-3"}>
|
||||
<h1 className={"flex items-center gap-3"} title={user?.id}>
|
||||
{user.name || user.id}
|
||||
</h1>
|
||||
</div>
|
||||
@@ -188,7 +209,7 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
|
||||
variant={"default"}
|
||||
className={"w-full"}
|
||||
onClick={() => {
|
||||
user.is_service_user
|
||||
isServiceUser
|
||||
? router.push("/team/service-users")
|
||||
: router.push("/team/users");
|
||||
}}
|
||||
@@ -212,7 +233,7 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
|
||||
<div className={"flex gap-10 w-full mt-8 max-w-6xl items-start"}>
|
||||
<UserInformationCard user={user} />
|
||||
<div className={"flex flex-col gap-8 w-1/2 "}>
|
||||
{!user.is_service_user && isOwnerOrAdmin && (
|
||||
{!isServiceUser && isOwnerOrAdmin && (
|
||||
<div>
|
||||
<Label>Auto-assigned groups</Label>
|
||||
<HelpText>
|
||||
@@ -238,7 +259,7 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
|
||||
<UserRoleSelector
|
||||
value={role}
|
||||
onChange={setRole}
|
||||
hideOwner={user.is_service_user}
|
||||
hideOwner={isServiceUser}
|
||||
currentUser={user}
|
||||
disabled={isLoggedInUser || !permission.users.update}
|
||||
/>
|
||||
@@ -248,38 +269,65 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(user.is_current || user.is_service_user) && permission.pats.read && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className={"px-8 py-6"}>
|
||||
<div className={"max-w-6xl"}>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<div>
|
||||
<h2>Access Tokens</h2>
|
||||
<Paragraph>
|
||||
Access tokens give access to NetBird API.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"inline-flex gap-4 justify-end"}>
|
||||
{showSeparator && <Separator />}
|
||||
|
||||
<Tabs
|
||||
defaultValue={tab}
|
||||
onValueChange={setTab}
|
||||
value={tab}
|
||||
className={"pb-0 mb-0"}
|
||||
>
|
||||
<TabsList justify={"start"} className={"px-8"} hidden={!showTabs}>
|
||||
{showPeers && (
|
||||
<TabsTrigger value={"peers"}>
|
||||
<MonitorSmartphoneIcon size={16} />
|
||||
Peers
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{showAccessTokens && (
|
||||
<TabsTrigger value={"access-tokens"}>
|
||||
<KeyRoundIcon size={16} />
|
||||
Access Tokens
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
{showPeers && (
|
||||
<TabsContent value={"peers"} className={"pb-8"}>
|
||||
<UserPeersSection user={user} />
|
||||
</TabsContent>
|
||||
)}
|
||||
{showAccessTokens && (
|
||||
<TabsContent value={"access-tokens"} className={"pb-8"}>
|
||||
<div className={"px-8"}>
|
||||
<div className={"max-w-6xl"}>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<div>
|
||||
<CreateAccessTokenModal user={user}>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
data-cy={"access-token-open-modal"}
|
||||
disabled={!permission.pats.create}
|
||||
>
|
||||
<IconCirclePlus size={16} />
|
||||
Create Access Token
|
||||
</Button>
|
||||
</CreateAccessTokenModal>
|
||||
<h2>Access Tokens</h2>
|
||||
<Paragraph>
|
||||
Access tokens give access to NetBird API.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"inline-flex gap-4 justify-end"}>
|
||||
<div>
|
||||
<CreateAccessTokenModal user={user}>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
data-cy={"access-token-open-modal"}
|
||||
disabled={!permission.pats.create}
|
||||
>
|
||||
<IconCirclePlus size={16} />
|
||||
Create Access Token
|
||||
</Button>
|
||||
</CreateAccessTokenModal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AccessTokensTable user={user} />
|
||||
</div>
|
||||
<AccessTokensTable user={user} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { ExternalLinkIcon, User2 } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import TeamIcon from "@/assets/icons/TeamIcon";
|
||||
import { useGroups } from "@/contexts/GroupsProvider";
|
||||
@@ -18,57 +19,53 @@ import PageContainer from "@/layouts/PageContainer";
|
||||
const UsersTable = lazy(() => import("@/modules/users/UsersTable"));
|
||||
|
||||
export default function TeamUsers() {
|
||||
const { isLoading: isGroupsLoading } = useGroups();
|
||||
const { permission } = usePermissions();
|
||||
const { data: users, isLoading } = useFetchApi<User[]>(
|
||||
"/users?service_user=false",
|
||||
);
|
||||
const t = useTranslations("users");
|
||||
const { isLoading: isGroupsLoading } = useGroups();
|
||||
const { permission } = usePermissions();
|
||||
const { data: users, isLoading } = useFetchApi<User[]>(
|
||||
"/users?service_user=false",
|
||||
);
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/team"}
|
||||
label={"Team"}
|
||||
icon={<TeamIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/team/users"}
|
||||
label={"Users"}
|
||||
active
|
||||
icon={<User2 size={16} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>Users</h1>
|
||||
<Paragraph>
|
||||
Manage users and their permissions. Same-domain email users are added
|
||||
automatically on first sign-in.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/add-users-to-your-network"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Users
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<RestrictedAccess page={"Users"} hasAccess={permission.users.read}>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<UsersTable
|
||||
users={users}
|
||||
isLoading={isLoading || isGroupsLoading}
|
||||
headingTarget={portalTarget}
|
||||
/>
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</PageContainer>
|
||||
);
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/team"}
|
||||
label={t("team")}
|
||||
icon={<TeamIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/team/users"}
|
||||
label={t("title")}
|
||||
active
|
||||
icon={<User2 size={16} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>{t("title")}</h1>
|
||||
<Paragraph>
|
||||
{t("usersPageDescription")}{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/add-users-to-your-network"}
|
||||
target={"_blank"}
|
||||
>
|
||||
{t("learnMore")}
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<RestrictedAccess page={t("title")} hasAccess={permission.users.read}>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<UsersTable
|
||||
users={users}
|
||||
isLoading={isLoading || isGroupsLoading}
|
||||
headingTarget={portalTarget}
|
||||
/>
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function RDPPage() {
|
||||
} = useFetchApi<Peer>(`/peers/${peerId}`, true, false, !!peerId);
|
||||
|
||||
return (
|
||||
<div className={"w-screen h-screen overflow-hidden"}>
|
||||
<div className={"w-screen h-screen overflow-hidden fixed inset-0"}>
|
||||
{peerId && peer && !isLoading ? (
|
||||
<RDPSession key={peer.id} peer={peer} />
|
||||
) : (
|
||||
@@ -55,7 +55,7 @@ function RDPSession({ peer }: Props) {
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${peer.name} - ${peer.ip} - RDP`;
|
||||
}, []);
|
||||
}, [peer.ip, peer.name, connected, rdp]);
|
||||
|
||||
const sendErrorNotification = (title: string, message: string) => {
|
||||
notify({
|
||||
@@ -84,7 +84,9 @@ function RDPSession({ peer }: Props) {
|
||||
try {
|
||||
setCredentials(rdpCredentials);
|
||||
setIsNetBirdConnecting(true);
|
||||
await client.connectTemporary(peer.id, [`tcp/${rdpCredentials.port}`]);
|
||||
await client.connectTemporary(peer.id, [
|
||||
`tcp/${rdpCredentials.port}`,
|
||||
]);
|
||||
setIsNetBirdConnecting(false);
|
||||
} catch (error) {
|
||||
sendErrorNotification(
|
||||
@@ -104,6 +106,7 @@ function RDPSession({ peer }: Props) {
|
||||
port: credentials.port,
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
domain: credentials.domain,
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
|
||||
@@ -12,9 +12,13 @@ import {
|
||||
NetBirdStatus,
|
||||
useNetBirdClient,
|
||||
} from "@/modules/remote-access/useNetBirdClient";
|
||||
import {
|
||||
isNativeSSHSupported,
|
||||
isNetbirdSSHProtocolSupported,
|
||||
} from "@utils/version";
|
||||
|
||||
export default function SSHPage() {
|
||||
const { peerId, username, port } = useSSHQueryParams();
|
||||
const { peerId, username, port, ipVersion } = useSSHQueryParams();
|
||||
|
||||
const {
|
||||
data: peer,
|
||||
@@ -44,6 +48,7 @@ export default function SSHPage() {
|
||||
peer={peer}
|
||||
username={username}
|
||||
port={port}
|
||||
ipVersion={ipVersion}
|
||||
/>
|
||||
) : (
|
||||
<LoadingMessage message={"Starting ssh session..."} />
|
||||
@@ -56,9 +61,10 @@ type Props = {
|
||||
username: string;
|
||||
port: string;
|
||||
peer: Peer;
|
||||
ipVersion: string | null;
|
||||
};
|
||||
|
||||
function SSHTerminal({ username, port, peer }: Props) {
|
||||
function SSHTerminal({ username, port, peer, ipVersion }: Props) {
|
||||
const client = useNetBirdClient();
|
||||
const connected = useRef(false);
|
||||
const sshConnectedOnce = useRef(false);
|
||||
@@ -77,21 +83,29 @@ function SSHTerminal({ username, port, peer }: Props) {
|
||||
const isClientDisconnected = client.status === NetBirdStatus.DISCONNECTED;
|
||||
const isClientConnecting = client.status === NetBirdStatus.CONNECTING;
|
||||
|
||||
// Use the FQDN when an IP version is specified so the dialer resolves to the correct address family.
|
||||
const sshHost = ipVersion ? peer.dns_label || peer.ip : peer.ip;
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${username}@${peer.ip} - ${peer.hostname}`;
|
||||
}, [username, peer, client]);
|
||||
document.title = `${username}@${sshHost} - ${peer.hostname}`;
|
||||
}, [username, peer, client, sshHost]);
|
||||
|
||||
const handleReconnect = async () => {
|
||||
if (!peer?.id) return;
|
||||
if (isSSHConnected || isSSHConnecting) return;
|
||||
connected.current = false;
|
||||
try {
|
||||
const rules = [`tcp/${port}`];
|
||||
const aclPort = isNativeSSHSupported(peer.version) ? "22022" : port;
|
||||
const protocol = isNetbirdSSHProtocolSupported(peer.version)
|
||||
? "netbird-ssh"
|
||||
: "tcp";
|
||||
const rules = [`${protocol}/${aclPort}`];
|
||||
await client?.connectTemporary(peer.id, rules);
|
||||
await ssh({
|
||||
hostname: peer.ip,
|
||||
hostname: sshHost,
|
||||
port: Number(port),
|
||||
username,
|
||||
ipVersion: ipVersion || undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Reconnection failed:", error);
|
||||
@@ -106,19 +120,25 @@ function SSHTerminal({ username, port, peer }: Props) {
|
||||
if (!peer.id) return;
|
||||
if (connected.current) return;
|
||||
connected.current = true;
|
||||
|
||||
try {
|
||||
const rules = [`tcp/${port}`];
|
||||
const aclPort = isNativeSSHSupported(peer.version) ? "22022" : port;
|
||||
const protocol = isNetbirdSSHProtocolSupported(peer.version)
|
||||
? "netbird-ssh"
|
||||
: "tcp";
|
||||
const rules = [`${protocol}/${aclPort}`];
|
||||
await client?.connectTemporary(peer.id, rules);
|
||||
const res = await ssh({
|
||||
hostname: peer.ip,
|
||||
hostname: sshHost,
|
||||
port: Number(port),
|
||||
username,
|
||||
ipVersion: ipVersion || undefined,
|
||||
});
|
||||
if (res === SSHStatus.CONNECTED) {
|
||||
sshConnectedOnce.current = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Connection failed:", error);
|
||||
console.error("Connection error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,14 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--toasts-before: 0;
|
||||
--lift: 1;
|
||||
}
|
||||
|
||||
html{
|
||||
@apply bg-nb-gray;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-2xl font-medium text-gray-700 dark:text-nb-gray-100 my-1;
|
||||
@@ -167,4 +174,29 @@ p {
|
||||
|
||||
.xterm-viewport {
|
||||
@apply m-0 p-0 box-border;
|
||||
}
|
||||
}
|
||||
|
||||
/* Disable sonner's opacity fade-in for custom toasts, but respect visibility */
|
||||
[data-sonner-toast][data-visible="true"] {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
|
||||
/* Adjust sonner stacking: less shrink and less lift per toast */
|
||||
[data-sonner-toast][data-expanded="false"][data-front="false"] {
|
||||
--scale: calc(var(--toasts-before) * 0.03 - 1) !important;
|
||||
--lift-amount: calc(var(--lift) * 10px) !important;
|
||||
}
|
||||
|
||||
/* Override stacked toast removal to move up instead of down */
|
||||
[data-sonner-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='false'] {
|
||||
--y: translateY(calc(var(--lift) * -20%)) !important;
|
||||
|
||||
opacity: 0 !important;
|
||||
transition: transform 400ms ease, opacity 300ms ease !important;
|
||||
}
|
||||
|
||||
/* Control Center */
|
||||
.react-flow__node-groupNode .selected{
|
||||
@apply border-netbird;
|
||||
}
|
||||
|
||||
8
src/app/invite/layout.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Accept Invite - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
321
src/app/invite/page.tsx
Normal file
@@ -0,0 +1,321 @@
|
||||
"use client";
|
||||
|
||||
import Button from "@components/Button";
|
||||
import { Input } from "@components/Input";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import { acceptInvite, fetchInviteInfo } from "@utils/unauthenticatedApi";
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
KeyRound,
|
||||
Mail,
|
||||
User2,
|
||||
} from "lucide-react";
|
||||
import dayjs from "dayjs";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useMemo, useState } from "react";
|
||||
import NetBirdIcon from "@/assets/icons/NetBirdIcon";
|
||||
import { UserInviteInfo } from "@/interfaces/User";
|
||||
|
||||
export default function InviteAcceptPage() {
|
||||
return (
|
||||
<Suspense fallback={<FullScreenLoading />}>
|
||||
<InviteAcceptContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function InviteAcceptContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const token = searchParams?.get("token");
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [inviteInfo, setInviteInfo] = useState<UserInviteInfo | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isRateLimited, setIsRateLimited] = useState(false);
|
||||
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
setError("No invite token provided");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
fetchInviteInfo(token)
|
||||
.then((info) => {
|
||||
setInviteInfo(info);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.code === 429) {
|
||||
setError("Too many attempts. Please wait a moment and try again.");
|
||||
setIsRateLimited(true);
|
||||
} else {
|
||||
setError(err.message || "Invalid or expired invite link");
|
||||
setIsRateLimited(false);
|
||||
}
|
||||
setLoading(false);
|
||||
});
|
||||
}, [token]);
|
||||
|
||||
const passwordsMatch = password === confirmPassword;
|
||||
const hasMinLength = password.length >= 8;
|
||||
const hasUppercase = /[A-Z]/.test(password);
|
||||
const hasLowercase = /[a-z]/.test(password);
|
||||
const hasNumber = /[0-9]/.test(password);
|
||||
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
|
||||
const passwordValid = hasMinLength && hasUppercase && hasLowercase && hasNumber && hasSpecialChar;
|
||||
const canSubmit = passwordValid && passwordsMatch && !submitting;
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!canSubmit || !token) return;
|
||||
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await acceptInvite(token, password);
|
||||
setSuccess(true);
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Failed to accept invite");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isExpired = useMemo(() => {
|
||||
if (!inviteInfo) return false;
|
||||
return new Date(inviteInfo.expires_at) < new Date();
|
||||
}, [inviteInfo]);
|
||||
|
||||
if (loading) {
|
||||
return <FullScreenLoading />;
|
||||
}
|
||||
|
||||
if (error && !inviteInfo) {
|
||||
if (isRateLimited) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-nb-gray-950 p-4">
|
||||
<div className="max-w-md w-full text-center">
|
||||
<div className="mb-6 flex justify-center">
|
||||
<div className="w-16 h-16 bg-yellow-500/10 rounded-full flex items-center justify-center">
|
||||
<Clock className="w-8 h-8 text-yellow-500" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold text-white mb-2">
|
||||
Too Many Requests
|
||||
</h1>
|
||||
<Paragraph className="text-nb-gray-400 text-base">
|
||||
You've made too many requests. Please wait a moment and try
|
||||
again.
|
||||
</Paragraph>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="mt-6"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-nb-gray-950 p-4">
|
||||
<div className="max-w-md w-full text-center">
|
||||
<div className="mb-6 flex justify-center">
|
||||
<div className="w-16 h-16 bg-red-500/10 rounded-full flex items-center justify-center">
|
||||
<AlertCircle className="w-8 h-8 text-red-500" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold text-white mb-2">
|
||||
Invalid Invite
|
||||
</h1>
|
||||
<Paragraph className="text-nb-gray-400 text-base">
|
||||
This invite link is invalid or has expired. Please contact your
|
||||
administrator to receive a new invitation.
|
||||
</Paragraph>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="mt-6"
|
||||
onClick={() => router.push("/")}
|
||||
>
|
||||
Go to Login
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-nb-gray-950 p-4">
|
||||
<div className="max-w-md w-full text-center">
|
||||
<div className="mb-6 flex justify-center">
|
||||
<div className="w-16 h-16 bg-green-500/10 rounded-full flex items-center justify-center">
|
||||
<CheckCircle2 className="w-8 h-8 text-green-500" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold text-white mb-2">
|
||||
Account Created!
|
||||
</h1>
|
||||
<Paragraph className="text-nb-gray-400">
|
||||
Your account has been created successfully. You can now log in with
|
||||
your email and password.
|
||||
</Paragraph>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="mt-6"
|
||||
onClick={() => router.push("/")}
|
||||
>
|
||||
Go to Login
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isExpired || !inviteInfo?.valid) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-nb-gray-950 p-4">
|
||||
<div className="max-w-md w-full text-center">
|
||||
<div className="mb-6 flex justify-center">
|
||||
<div className="w-16 h-16 bg-yellow-500/10 rounded-full flex items-center justify-center">
|
||||
<AlertCircle className="w-8 h-8 text-yellow-500" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold text-white mb-2">
|
||||
Invite Expired
|
||||
</h1>
|
||||
<Paragraph className="text-nb-gray-400">
|
||||
This invite link has expired. Please contact your administrator to
|
||||
receive a new invitation.
|
||||
</Paragraph>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="mt-6"
|
||||
onClick={() => router.push("/")}
|
||||
>
|
||||
Go to Login
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-nb-gray-950 p-4">
|
||||
<div className="max-w-md w-full">
|
||||
<div className="mb-8 flex justify-center">
|
||||
<NetBirdIcon size={48} />
|
||||
</div>
|
||||
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-semibold text-white mb-2">
|
||||
Welcome to NetBird
|
||||
</h1>
|
||||
<p className="dark:text-nb-gray-400 text-nb-gray-500 text-base">
|
||||
You've been invited by <span className="dark:text-white text-nb-gray-900 font-medium">{inviteInfo.invited_by}</span> to join the network. Set your password to complete your account setup.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-nb-gray-930 border border-nb-gray-900 rounded-lg p-6 mb-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 bg-nb-gray-900 rounded-full flex items-center justify-center">
|
||||
<User2 className="w-5 h-5 text-nb-gray-400" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-white font-medium">{inviteInfo.name}</div>
|
||||
<div className="text-nb-gray-400 text-sm flex items-center gap-1">
|
||||
<Mail className="w-3 h-3" />
|
||||
{inviteInfo.email}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
customPrefix={
|
||||
<KeyRound size={16} className="text-nb-gray-400" />
|
||||
}
|
||||
/>
|
||||
{password && (
|
||||
<div className="mt-2 space-y-1">
|
||||
<PasswordRule met={hasMinLength} text="At least 8 characters" />
|
||||
<PasswordRule met={hasUppercase} text="One uppercase letter" />
|
||||
<PasswordRule met={hasLowercase} text="One lowercase letter" />
|
||||
<PasswordRule met={hasNumber} text="One number" />
|
||||
<PasswordRule met={hasSpecialChar} text="One special character (!@#$%^&*)" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Confirm Password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
customPrefix={
|
||||
<KeyRound size={16} className="text-nb-gray-400" />
|
||||
}
|
||||
/>
|
||||
{confirmPassword && !passwordsMatch && (
|
||||
<p className="text-xs text-red-500 mt-1">
|
||||
Passwords do not match
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/20 rounded-md p-3">
|
||||
<p className="text-sm text-red-500">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
disabled={!canSubmit}
|
||||
>
|
||||
{submitting ? "Creating Account..." : "Create Account"}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-xs text-nb-gray-500">
|
||||
Invite expires on {dayjs(inviteInfo.expires_at).format("D MMMM, YYYY [at] h:mm A")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PasswordRule({ met, text }: { met: boolean; text: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
{met ? (
|
||||
<CheckCircle2 className="w-3 h-3 text-green-500" />
|
||||
) : (
|
||||
<AlertCircle className="w-3 h-3 text-nb-gray-500" />
|
||||
)}
|
||||
<span className={met ? "text-green-500" : "text-nb-gray-500"}>{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -36,6 +36,6 @@ export default function NotFound() {
|
||||
|
||||
const Redirect = ({ url, queryParams }: Props) => {
|
||||
const params = queryParams && `?${queryParams}`;
|
||||
useRedirect(url == "/" ? `/peers${params}` : `${url}${params}`);
|
||||
useRedirect(url == "/" ? `/peers${params}` : `${url}${params}`, true);
|
||||
return <FullScreenLoading />;
|
||||
};
|
||||
|
||||
@@ -37,6 +37,6 @@ export default function Home() {
|
||||
|
||||
const Redirect = ({ url, queryParams }: Props) => {
|
||||
const params = queryParams && `?${queryParams}`;
|
||||
useRedirect(url == "/" ? `/peers${params}` : `${url}${params}`);
|
||||
useRedirect(url == "/" ? `/peers${params}` : `${url}${params}`, true);
|
||||
return <FullScreenLoading />;
|
||||
};
|
||||
|
||||
8
src/app/setup/layout.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { globalMetaTitle } from "@utils/meta";
|
||||
import type { Metadata } from "next";
|
||||
import BlankLayout from "@/layouts/BlankLayout";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Instance Setup - ${globalMetaTitle}`,
|
||||
};
|
||||
export default BlankLayout;
|
||||
22
src/app/setup/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import InstanceSetupWizard from "@/modules/instance-setup/InstanceSetupWizard";
|
||||
import { useInstanceSetup } from "@/contexts/InstanceSetupProvider";
|
||||
import { useRouter } from "next/navigation";
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function SetupPage() {
|
||||
const { setupRequired, loading } = useInstanceSetup();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !setupRequired) router.replace("/peers");
|
||||
}, [loading, setupRequired]);
|
||||
|
||||
return loading || !setupRequired ? (
|
||||
<FullScreenLoading />
|
||||
) : (
|
||||
<InstanceSetupWizard />
|
||||
);
|
||||
}
|
||||
28
src/assets/icons/AuthentikIcon.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
|
||||
|
||||
export default function AuthentikIcon(props: Readonly<IconProps>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="-0.03 59.9 512.03 392.1"
|
||||
{...iconProperties(props)}
|
||||
>
|
||||
<path
|
||||
d="M279.9 141h17.9v51.2h-17.9zm46.6-2.2h17.9v40h-17.9zM65.3 197.3c-24 0-46 13.2-57.4 34.3h30.4c13.5-11.6 33-15 47.1 0h32.2c-12.6-17.1-31.4-34.3-52.3-34.3"
|
||||
fill="#fd4b2d"
|
||||
/>
|
||||
<path
|
||||
d="M108.7 262.4C66.8 350-6.6 275.3 38.3 231.5H7.9C-15.9 273 17 329 65.3 327.8c37.4 0 68.2-55.5 68.2-65.3 0-4.3-6-17.6-16-31H85.4c10.7 9.7 20 23.7 23.3 30.9m1.1-2.6"
|
||||
fill="#fd4b2d"
|
||||
/>
|
||||
<path
|
||||
d="M512 140.3v231.3c0 44.3-36.1 80.4-80.4 80.4h-34.1v-78.8h-163V452h-34.1c-44.4 0-80.4-36.1-80.4-80.4v-72.8h258.4v-139H253.6V238H119.9v-97.6c0-3.1.2-6.2.5-9.2.4-3.7 1.1-7.3 2-10.8.3-1.1.6-2.3 1-3.4.1-.3.2-.6.3-.8.2-.6.4-1.1.5-1.7.2-.5.4-1.1.6-1.7s.5-1.2.7-1.8.5-1.2.8-1.8c2-4.7 4.4-9.3 7.3-13.6l.1-.1c.7-1.1 1.5-2.1 2.3-3.2.7-.9 1.3-1.7 2-2.6.8-.9 1.6-1.9 2.4-2.8s1.6-1.8 2.4-2.6l.1-.1c.4-.5.9-.9 1.4-1.4 3-2.9 6.2-5.6 9.6-8 .9-.7 1.9-1.3 2.8-1.9 1.1-.7 2.2-1.4 3.3-2 2.1-1.2 4.2-2.4 6.5-3.4.7-.3 1.4-.7 2.1-1 3.1-1.3 6.2-2.5 9.4-3.4 1.2-.4 2.5-.7 3.7-1 .6-.2 1.2-.3 1.8-.4 3.6-.8 7.2-1.3 10.9-1.6l1.6-.1h.8c1.2-.1 2.4-.1 3.7-.1h231.3c1.2 0 2.5 0 3.7.1h.8l1.6.1c3.7.3 7.3.8 10.9 1.6.6.1 1.2.3 1.8.4 1.3.3 2.5.6 3.7 1 3.2.9 6.3 2.1 9.4 3.4.7.3 1.4.6 2.1 1 2.2 1 4.4 2.2 6.5 3.4 1.1.7 2.2 1.3 3.3 2 1 .6 1.9 1.3 2.8 1.9 3.9 2.8 7.6 6 11 9.4.8.8 1.7 1.7 2.4 2.6.8.9 1.6 1.9 2.4 2.8.7.8 1.3 1.7 2 2.6.8 1.1 1.5 2.1 2.3 3.2l.1.1c2.9 4.3 5.3 8.8 7.3 13.6.2.6.5 1.2.8 1.8.2.6.5 1.2.7 1.8.2.5.4 1.1.6 1.7s.4 1.1.5 1.7c.1.3.2.6.3.8.3 1.1.7 2.3 1 3.4.9 3.6 1.6 7.2 2 10.8 0 3.1.2 6.1.2 9.2"
|
||||
fill="#fd4b2d"
|
||||
/>
|
||||
<path
|
||||
d="M498.3 95.5H133.5c14.9-22.2 40-35.6 66.7-35.6h231.3c26.9 0 51.9 13.4 66.8 35.6m13.2 35.6H120.4c1.4-12.8 6-25 13.1-35.6h364.8c7.2 10.6 11.7 22.9 13.2 35.6m.5 9.2v26.4H378.3v-6.9H253.6v6.9H119.9v-26.4c0-3.1.2-6.2.5-9.2h391.1c.3 3.1.5 6.1.5 9.2M119.9 166.7h133.7v35.6H119.9zm258.4 0H512v35.6H378.3zm-258.4 35.6h133.7v35.6H119.9zm258.4 0H512v35.6H378.3z"
|
||||
fill="#fd4b2d"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
22
src/assets/icons/ControlCenterIcon.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
|
||||
|
||||
export default function ControlCenterIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
{...iconProperties(props)}
|
||||
>
|
||||
<path d="M5 3a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5Zm0 12a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2H5Zm12 0a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2h-2Zm0-12a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2h-2Z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 6.5a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1ZM10 18a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2h-2a1 1 0 0 1-1-1Zm-4-4a1 1 0 0 1-1-1v-2a1 1 0 1 1 2 0v2a1 1 0 0 1-1 1Zm12 0a1 1 0 0 1-1-1v-2a1 1 0 1 1 2 0v2a1 1 0 0 1-1 1Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
19
src/assets/icons/DNSZoneIcon.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
|
||||
|
||||
export default function DNSZoneIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...iconProperties(props)}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5 5a2 2 0 0 0-2 2v3a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V7a2 2 0 0 0-2-2H5Zm9 2a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H14Zm3 0a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H17ZM3 17v-3a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Zm11-2a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H14Zm3 0a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H17Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
31
src/assets/icons/IdentityProviderIcons.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { SSOIdentityProviderType } from "@/interfaces/IdentityProvider";
|
||||
import React from "react";
|
||||
import GoogleIcon from "@/assets/icons/GoogleIcon";
|
||||
import MicrosoftIcon from "@/assets/icons/MicrosoftIcon";
|
||||
import EntraIcon from "@/assets/icons/EntraIcon";
|
||||
import OktaIcon from "@/assets/icons/OktaIcon";
|
||||
import PocketIdIcon from "@/assets/icons/PocketIdIcon";
|
||||
import ZitadelIcon from "@/assets/icons/ZitadelIcon";
|
||||
import AuthentikIcon from "@/assets/icons/AuthentikIcon";
|
||||
import KeycloakIcon from "@/assets/icons/KeycloakIcon";
|
||||
import { KeyRound } from "lucide-react";
|
||||
|
||||
export const idpIcon = (
|
||||
type: SSOIdentityProviderType,
|
||||
size: number = 16,
|
||||
): React.ReactNode => {
|
||||
const icons: Record<SSOIdentityProviderType, React.ReactNode> = {
|
||||
google: <GoogleIcon size={size} />,
|
||||
microsoft: <MicrosoftIcon size={size} />,
|
||||
entra: <EntraIcon size={size} />,
|
||||
okta: <OktaIcon size={size} className="text-nb-gray-300" />,
|
||||
pocketid: <PocketIdIcon size={size} />,
|
||||
zitadel: <ZitadelIcon size={size} />,
|
||||
authentik: <AuthentikIcon size={size} />,
|
||||
keycloak: <KeycloakIcon size={size} />,
|
||||
adfs: <MicrosoftIcon size={size} />,
|
||||
oidc: <KeyRound size={size} className="text-nb-gray-400" />,
|
||||
};
|
||||
|
||||
return icons[type];
|
||||
};
|
||||
19
src/assets/icons/JumpcloudIcon.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
|
||||
|
||||
export default function JumpcloudIcon(props: Readonly<IconProps>) {
|
||||
return (
|
||||
<svg
|
||||
width="167"
|
||||
height="82"
|
||||
viewBox="0 0 167 82"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...iconProperties(props)}
|
||||
>
|
||||
<path
|
||||
d="M166.911 58.3592C166.911 64.3815 164.519 70.1571 160.26 74.4155C156.002 78.6739 150.226 81.0662 144.204 81.0662H137.961C137.31 73.4972 129.5 67.0612 118.46 64.0722C121.244 61.3253 123.148 57.8124 123.931 53.9803C124.713 50.1482 124.338 46.17 122.854 42.5515C121.369 38.933 118.842 35.8378 115.594 33.6594C112.345 31.481 108.522 30.3178 104.611 30.3178C100.7 30.3178 96.8772 31.481 93.6289 33.6594C90.3805 35.8378 87.8534 38.933 86.3689 42.5515C84.8843 46.17 84.5094 50.1482 85.2918 53.9803C86.0743 57.8124 87.9786 61.3253 90.7628 64.0722C85.5111 65.3278 80.6301 67.8055 76.5167 71.3037C73.9207 69.8152 71.1411 68.6726 68.2487 67.9049C70.6422 65.5587 72.2829 62.5529 72.9614 59.2707C73.6399 55.9884 73.3255 52.5784 72.0584 49.4755C70.7913 46.3726 68.6288 43.7174 65.8467 41.8484C63.0646 39.9793 59.7888 38.9812 56.4372 38.9812C53.0855 38.9812 49.8098 39.9793 47.0277 41.8484C44.2455 43.7174 42.0831 46.3726 40.816 49.4755C39.5488 52.5784 39.2345 55.9884 39.913 59.2707C40.5915 62.5529 42.2321 65.5587 44.6257 67.9049C35.9237 70.3154 29.5841 75.1364 28.2342 80.9698H21.991C16.0936 80.7777 10.502 78.2999 6.39821 74.0603C2.2944 69.8206 0 64.1513 0 58.2508C0 52.3503 2.2944 46.681 6.39821 42.4413C10.502 38.2016 16.0936 35.7238 21.991 35.5317C24.8814 35.5419 27.7438 36.0981 30.4278 37.1709C32.2478 33.2162 35.1686 29.8695 38.8407 27.5312C42.5128 25.1928 46.7807 23.9618 51.1341 23.9854C51.6885 23.9854 52.2429 23.9854 52.7732 23.9854C53.9093 18.1059 56.8018 12.7093 61.0689 8.50798C65.336 4.30669 70.7769 1.49837 76.6733 0.453829C82.5698 -0.590709 88.6443 0.177651 94.095 2.65746C99.546 5.13728 104.116 9.21191 107.203 14.3434C110.733 13.2708 114.463 13.023 118.104 13.6193C121.746 14.2155 125.202 15.6397 128.206 17.7822C131.21 19.9247 133.682 22.7283 135.432 25.977C137.182 29.2257 138.162 32.8326 138.298 36.52C141.665 35.6031 145.198 35.4762 148.622 36.1492C152.046 36.8222 155.269 38.277 158.038 40.4001C160.808 42.5233 163.049 45.2574 164.588 48.3892C166.127 51.5211 166.922 54.9661 166.911 58.4557V58.3592Z"
|
||||
fill="#4CC2BF"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
88
src/assets/icons/KeycloakIcon.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
|
||||
|
||||
export default function KeycloakIcon(props: Readonly<IconProps>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
{...iconProperties(props)}
|
||||
>
|
||||
<g transform="translate(.714 .07)">
|
||||
<path
|
||||
d="M432.9 149.2c-1.4 0-2.7-.7-3.4-2L370.1 44.1c-.7-1.2-2-2-3.5-2H124.2c-1.4 0-2.7.7-3.4 2L58.9 150.9l23.9 34.9c-.7 1.2-6.2 24-5.5 25.2L58.9 360.9l61.9 106.9c.7 1.2 2 2 3.4 2h242.4c1.4 0 2.7-.7 3.5-2l59.4-103.2c.7-1.2 2-2 3.4-2h73.8c2.4 0 4.4-2 4.4-4.4V153.6c0-2.4-2-4.4-4.4-4.4z"
|
||||
fill="#4d4d4d"
|
||||
/>
|
||||
<path d="M72.7 245.3 6.4 269.4l-6.6-11.3c-.7-1.2-.7-2.7 0-3.9l30-52z" fill="#e1e1e1" />
|
||||
<path d="M511.3 258.3V309l-43.7-44.5z" fill="#c8c8c8" />
|
||||
<path
|
||||
d="m467.5 264.5 43.7 44.5v49.6c0 2.4-2 4.4-4.4 4.4H456z"
|
||||
fill="#c2c2c2"
|
||||
/>
|
||||
<path d="M467.5 264.5 456 362.9h-61.2l-18.5-44.7z" fill="#c7c7c7" />
|
||||
<path d="M511.3 211.2v47l-43.7 6.2z" fill="#cecece" />
|
||||
<path
|
||||
d="M511.3 153.6v57.6l-43.7 53.2-33.1-115.3h72.2c2.4-.1 4.5 1.8 4.6 4.3z"
|
||||
fill="#d3d3d3"
|
||||
/>
|
||||
<path d="M394.8 362.9h-32.3l-8.4-12 22.1-32.7z" fill="#c6c6c6" />
|
||||
<path d="m467.5 264.5-121.1-51.2 63.7-64.1h24.4z" fill="#d5d5d5" />
|
||||
<path d="m346.5 213.3 29.8 105 91.2-53.8z" fill="#d0d0d0" />
|
||||
<path d="m353.8 362.9.4-12 8.4 12z" fill="#bfbfbf" />
|
||||
<path d="m410.1 149.2-63.7 64.1-11.4-57.4 24.6-6.8h50.5z" fill="#d9d9d9" />
|
||||
<path d="m346.5 213.3-147 33.9 154.7 103.7z" fill="#d4d4d4" />
|
||||
<path d="m346.5 213.3 7.7 137.6 22.1-32.7z" fill="#d0d0d0" />
|
||||
<path d="m335 155.9-135.5 91.2 147-33.9z" fill="#d9d9d9" />
|
||||
<path d="m199.5 247.2-63.7 115.7H99.6L72.7 245.3z" fill="#d8d8d8" />
|
||||
<path
|
||||
d="m134.3 149.2-61.5 96.1L57.3 155l2.2-3.8c.7-1.2 2-1.9 3.4-1.9z"
|
||||
fill="#e2e2e2"
|
||||
/>
|
||||
<path
|
||||
d="M99.6 362.9H62.7c-1.4 0-2.8-.8-3.5-2L6.4 269.4l66.4-24.1z"
|
||||
fill="#d8d8d8"
|
||||
/>
|
||||
<path d="M29.9 202.1 57.1 155l15.7 90.3z" fill="#e4e4e4" />
|
||||
<path d="m335 155.9-40.8-6.8H159.4l40.1 98z" fill="#dedede" />
|
||||
<path d="m199.5 247.2-40.1-98h-25.1l-61.5 96.1z" fill="#dedede" />
|
||||
<path d="M324.7 362.9h29.1l.4-12z" fill="#c5c5c5" />
|
||||
<path d="M266.7 362.9h58l29.5-12-154.7-103.7 27.9 115.7z" fill="#d0d0d0" />
|
||||
<path d="m227.4 362.9-27.9-115.7-63.7 115.7z" fill="#d1d1d1" />
|
||||
<path d="m335.4 149.2-.4 6.8 24.6-6.8z" fill="#ddd" />
|
||||
<path d="m335 155.9-3.8-6.8h-37z" fill="#e3e3e3" />
|
||||
<path d="m335 155.9.4-6.8h-4.2z" fill="#e2e2e2" />
|
||||
<path
|
||||
d="m223.9 151-59.7 103.4c-.3.5-.4 1.1-.4 1.7h-41.7l82-142q.75.45 1.2 1.2l18.6 32.3c.5 1.1.5 2.4 0 3.4"
|
||||
fill="#00b8e3"
|
||||
/>
|
||||
<path
|
||||
d="M223.8 364.9 205.3 397q-.45.75-1.2 1.2l-82-142.2h41.7c0 .6.1 1.1.4 1.6l59.6 103.2c.8 1.2.9 2.9 0 4.1"
|
||||
fill="#33c6e9"
|
||||
/>
|
||||
<path
|
||||
d="m204 114.2-82 141.9-20.6 35.6-19.6-34c-.3-.5-.4-1-.4-1.6s.1-1.2.4-1.7l19.9-34.4 60.4-104.5c.6-1.1 1.8-1.8 3-1.8h37.2c.6 0 1.2.2 1.7.5"
|
||||
fill="#008aaa"
|
||||
/>
|
||||
<path
|
||||
d="M204 398.2c-.5.3-1.1.5-1.8.5h-37.1c-1.3 0-2.4-.7-3-1.8l-55.2-95.6-5.5-9.5 20.6-35.6z"
|
||||
fill="#00b8e3"
|
||||
/>
|
||||
<path
|
||||
d="m368.9 256.1-82 142q-.75-.45-1.2-1.2L267 364.7c-.5-1-.5-2.3 0-3.3L326.7 258c.3-.5.5-1.2.5-1.8z"
|
||||
fill="#008aaa"
|
||||
/>
|
||||
<path
|
||||
d="M409.4 256.1c0 .6-.2 1.3-.5 1.8l-80.3 139.3c-.6 1-1.8 1.7-3 1.6h-37c-.6 0-1.2-.2-1.8-.5L368.9 256l20.6-35.6 19.5 33.8c.3.7.4 1.3.4 1.9"
|
||||
fill="#00b8e3"
|
||||
/>
|
||||
<path
|
||||
d="M368.9 256.1h-41.7c0-.6-.2-1.2-.5-1.8L267 151.2c-.6-1.1-.6-2.5 0-3.6l18.6-32.2q.45-.75 1.2-1.2z"
|
||||
fill="#00b8e3"
|
||||
/>
|
||||
<path
|
||||
d="m389.4 220.5-20.6 35.6-82-142c.6-.3 1.2-.5 1.8-.5h37.1c1.2 0 2.3.6 3 1.6z"
|
||||
fill="#33c6e9"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
16
src/assets/icons/MicrosoftIcon.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
|
||||
|
||||
export default function MicrosoftIcon(props: Readonly<IconProps>) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 221 221"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...iconProperties(props)}
|
||||
>
|
||||
<path fill="#F1511B" d="M104.868 104.868H0V0h104.868z" />
|
||||
<path fill="#80CC28" d="M220.654 104.868H115.788V0h104.866z" />
|
||||
<path fill="#00ADEF" d="M104.865 220.695H0V115.828h104.865z" />
|
||||
<path fill="#FBBC09" d="M220.654 220.695H115.788V115.828h104.866z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
27
src/assets/icons/OIDCIcon.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
|
||||
|
||||
export default function OIDCIcon(props: Readonly<IconProps>) {
|
||||
return (
|
||||
<svg
|
||||
width="173"
|
||||
height="174"
|
||||
viewBox="0 0 173 174"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...iconProperties(props)}
|
||||
>
|
||||
<path
|
||||
d="M76.3945 173.48L103.325 154.065L102.072 0L76.3945 20.041V173.48Z"
|
||||
fill="#FF8E00"
|
||||
/>
|
||||
<path
|
||||
d="M76.7077 173.48C-24.0221 157.466 -26.8926 69.7689 76.0814 50.7288L76.3945 68.8909C3.35034 81.0694 12.6045 146.598 76.3945 156.257L76.7077 173.48Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M103.011 68.2646C115.468 68.3493 126.32 74.0515 137.144 79.8508L121.174 91.7502H172.216L172.529 56.9916L156.558 68.8909C140.397 60.7278 125.542 50.9315 103.011 50.7288V68.2646Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
26
src/assets/icons/PeerOSIcon.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||
import { cn } from "@utils/helpers";
|
||||
import * as React from "react";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
import { OSLogo } from "@/modules/peers/PeerOSCell";
|
||||
|
||||
type Props = {
|
||||
os: string;
|
||||
};
|
||||
|
||||
export const PeerOSIcon = ({ os }: Props) => {
|
||||
const osType = getOperatingSystem(os);
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center grayscale brightness-[100%] contrast-[40%]",
|
||||
"w-4 h-4 shrink-0",
|
||||
osType === OperatingSystem.WINDOWS && "p-[2.5px]",
|
||||
osType === OperatingSystem.APPLE && "p-[2.7px]",
|
||||
osType === OperatingSystem.FREEBSD && "p-[1.5px]",
|
||||
)}
|
||||
>
|
||||
<OSLogo os={os} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
19
src/assets/icons/PeerOrResourceIcon.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from "react";
|
||||
import { NetworkResource } from "@/interfaces/Network";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import { PeerOSIcon } from "./PeerOSIcon";
|
||||
import { ResourceIcon } from "./ResourceIcon";
|
||||
|
||||
type Props = {
|
||||
peer?: Peer;
|
||||
resource?: NetworkResource;
|
||||
};
|
||||
|
||||
export const PeerOrResourceIcon = ({ peer, resource }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{peer && <PeerOSIcon os={peer.os} />}
|
||||
{resource?.type && <ResourceIcon type={resource.type} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
17
src/assets/icons/PocketIdIcon.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
|
||||
|
||||
export default function PocketIdIcon(props: Readonly<IconProps>) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...iconProperties(props)}
|
||||
>
|
||||
<circle cx="256" cy="256" r="256" fill="#fff" />
|
||||
<path
|
||||
d="M268.6 102.4c64.4 0 116.8 52.4 116.8 116.7 0 25.3-8 49.4-23 69.6-14.8 19.9-35 34.3-58.4 41.7l-6.5 2-15.5-76.2 4.3-2c14-6.7 23-21.1 23-36.6 0-22.4-18.2-40.6-40.6-40.6S228 195.2 228 217.6c0 15.5 9 29.8 23 36.6l4.2 2-25 153.4h-69.5V102.4z"
|
||||
fill="#191919"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
20
src/assets/icons/ResourceIcon.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { GlobeIcon, NetworkIcon, WorkflowIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
type: "domain" | "host" | "subnet";
|
||||
size?: number;
|
||||
};
|
||||
|
||||
export const ResourceIcon = ({ type, size = 15 }: Props) => {
|
||||
switch (type) {
|
||||
case "domain":
|
||||
return <GlobeIcon size={size} />;
|
||||
case "subnet":
|
||||
return <NetworkIcon size={size} />;
|
||||
case "host":
|
||||
return <WorkflowIcon size={size} />;
|
||||
default:
|
||||
return <WorkflowIcon size={size} />;
|
||||
}
|
||||
};
|
||||
19
src/assets/icons/ReverseProxyIcon.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
|
||||
|
||||
export default function ReverseProxyIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...iconProperties(props)}
|
||||
fill={"currentColor"}
|
||||
>
|
||||
<path
|
||||
fill={"currentColor"}
|
||||
d="M11.4488 2.1499C11.7903 1.95003 12.2097 1.95003 12.5513 2.1499L16.5018 4.46123L12 7.03523L7.49823 4.46123L11.4488 2.1499ZM6.44447 6.46472L6.44444 10.2784L2.93531 12.3315L7.53662 14.8399L10.8889 12.8787V9.00593L6.44447 6.46472ZM2 14.3992V18.7395C2 19.1477 2.21366 19.5247 2.55984 19.7272L6.44446 22V16.8223L2 14.3992ZM8.66668 22L12 20.0497L15.3333 22V16.7994L12 14.8492L8.66668 16.7993V22ZM17.5556 22L21.4401 19.7272C21.7863 19.5247 22 19.1477 22 18.7395V14.3992L17.5556 16.8223V22ZM21.0647 12.3315L17.5556 10.2784V6.46474L13.1111 9.00593V12.8787L16.4634 14.8399L21.0647 12.3315Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
30
src/assets/icons/SlackIcon.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
|
||||
|
||||
export default function SlackIcon(props: Readonly<IconProps>) {
|
||||
return (
|
||||
<svg
|
||||
width="127"
|
||||
height="127"
|
||||
viewBox="0 0 127 127"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...iconProperties(props)}
|
||||
>
|
||||
<path
|
||||
d="M27.2 80c0 7.3-5.9 13.2-13.2 13.2C6.7 93.2.8 87.3.8 80c0-7.3 5.9-13.2 13.2-13.2h13.2V80zm6.6 0c0-7.3 5.9-13.2 13.2-13.2 7.3 0 13.2 5.9 13.2 13.2v33c0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V80z"
|
||||
fill="#E01E5A"
|
||||
/>
|
||||
<path
|
||||
d="M47 27c-7.3 0-13.2-5.9-13.2-13.2C33.8 6.5 39.7.6 47 .6c7.3 0 13.2 5.9 13.2 13.2V27H47zm0 6.7c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H13.9C6.6 60.1.7 54.2.7 46.9c0-7.3 5.9-13.2 13.2-13.2H47z"
|
||||
fill="#36C5F0"
|
||||
/>
|
||||
<path
|
||||
d="M99.9 46.9c0-7.3 5.9-13.2 13.2-13.2 7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H99.9V46.9zm-6.6 0c0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V13.8C66.9 6.5 72.8.6 80.1.6c7.3 0 13.2 5.9 13.2 13.2v33.1z"
|
||||
fill="#2EB67D"
|
||||
/>
|
||||
<path
|
||||
d="M80.1 99.8c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V99.8h13.2zm0-6.6c-7.3 0-13.2-5.9-13.2-13.2 0-7.3 5.9-13.2 13.2-13.2h33.1c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H80.1z"
|
||||
fill="#ECB22E"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
32
src/assets/icons/ZitadelIcon.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
|
||||
|
||||
export default function ZitadelIcon(props: Readonly<IconProps>) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 80 79"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...iconProperties(props)}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="zitadel-grad"
|
||||
x1="3.86"
|
||||
x2="76.88"
|
||||
y1="47.89"
|
||||
y2="47.89"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#FF8F00" />
|
||||
<stop offset="1" stopColor="#FE00FF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
fill="url(#zitadel-grad)"
|
||||
fillRule="evenodd"
|
||||
d="M17.12 39.17l1.42 5.32-6.68 6.68 9.12 2.44 1.43 5.32-19.77-5.3L17.12 39.17zM58.82 22.41l-5.32-1.43-2.44-9.12-6.68 6.68-5.32-1.43 14.47-14.47 5.3 19.77zM52.65 67.11l3.89-3.89 9.12 2.44-2.44-9.12 3.9-3.9 5.29 19.77-19.76-5.3zM36.43 69.54l-1.18-12.07 8.23 2.21-7.05 9.86zM23 23.84l5.02 11.04 6.02-6.02L23 23.84zM69.32 36.2l-12.07-1.18 2.2 8.23 9.87-7.05z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
BIN
src/assets/integrations/crowdsec.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
@@ -1,12 +0,0 @@
|
||||
<svg width="573" height="148" viewBox="0 0 573 148" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_4_4)">
|
||||
<path d="M0.739014 125.602V33.09H28.239C34.491 33.09 39.919 34.274 44.524 36.638C49.128 39.004 52.698 42.341 55.233 46.65C57.767 50.958 59.034 56.071 59.034 61.984V96.58C59.034 102.41 57.767 107.5 55.233 111.85C52.698 116.203 49.128 119.581 44.523 121.99C39.918 124.397 34.491 125.601 28.239 125.601L0.739014 125.602ZM16.58 111.028H28.24C32.801 111.028 36.433 109.741 39.138 107.163C41.841 104.587 43.193 101.06 43.193 96.581V61.984C43.193 57.592 41.841 54.107 39.138 51.529C36.433 48.952 32.801 47.663 28.239 47.663H16.58V111.028ZM76.396 125.602V33.09H95.786L121.512 107.86C121.216 104.673 120.942 101.483 120.688 98.292C120.386 94.5366 120.154 90.776 119.991 87.012C119.821 83.169 119.738 79.812 119.738 76.938V33.09H134.185V125.602H114.795L89.323 50.832C89.491 53.283 89.703 56.239 89.956 59.702C90.2137 63.2478 90.4251 66.7967 90.59 70.348C90.758 73.982 90.844 77.318 90.844 80.36V125.602H76.396ZM181.582 126.869C175.245 126.869 169.752 125.812 165.107 123.701C160.46 121.591 156.889 118.569 154.398 114.64C151.905 110.711 150.616 106.086 150.533 100.763H166.374C166.374 104.565 167.747 107.543 170.494 109.698C173.238 111.852 176.976 112.929 181.709 112.929C186.271 112.929 189.839 111.874 192.417 109.761C194.993 107.65 196.282 104.735 196.282 101.017C196.282 97.892 195.374 95.167 193.558 92.842C191.74 90.52 189.142 88.936 185.764 88.09L175.119 85.175C167.852 83.318 162.256 79.979 158.327 75.164C154.398 70.348 152.434 64.518 152.434 57.675C152.434 52.438 153.616 47.875 155.982 43.988C158.347 40.103 161.705 37.103 166.058 34.99C170.408 32.88 175.54 31.822 181.455 31.822C190.409 31.822 197.506 34.125 202.745 38.729C207.983 43.335 210.645 49.523 210.73 57.295H194.888C194.888 53.663 193.704 50.812 191.34 48.741C188.974 46.671 185.637 45.636 181.328 45.636C177.188 45.636 173.978 46.608 171.697 48.551C169.416 50.495 168.275 53.24 168.275 56.788C168.275 60 169.141 62.724 170.873 64.962C172.603 67.202 175.119 68.786 178.413 69.714L189.439 72.756C196.789 74.616 202.407 77.932 206.294 82.704C210.179 87.478 212.124 93.371 212.124 100.383C212.124 105.623 210.856 110.248 208.322 114.26C205.787 118.273 202.239 121.378 197.677 123.574C193.114 125.77 187.748 126.869 181.582 126.869ZM257.366 126.869C251.366 126.869 246.17 125.729 241.778 123.448C237.384 121.167 233.985 117.957 231.577 113.816C229.169 109.678 227.965 104.818 227.965 99.242V59.45C227.965 53.874 229.169 49.017 231.577 44.876C233.984 40.738 237.384 37.526 241.778 35.245C246.17 32.964 251.366 31.823 257.366 31.823C263.449 31.823 268.665 32.963 273.017 35.245C277.367 37.526 280.747 40.738 283.156 44.876C285.563 49.016 286.767 53.874 286.767 59.45V99.243C286.767 104.819 285.563 109.679 283.156 113.817C280.748 117.957 277.346 121.167 272.954 123.449C268.56 125.729 263.364 126.869 257.366 126.869ZM257.366 113.183C261.758 113.183 265.265 111.915 267.885 109.381C270.502 106.846 271.813 103.468 271.813 99.242V59.45C271.813 55.227 270.503 51.846 267.885 49.312C265.265 46.777 261.758 45.51 257.366 45.51C252.972 45.51 249.466 46.777 246.848 49.312C244.228 51.846 242.918 55.227 242.918 59.45V99.243C242.918 103.469 244.228 106.847 246.848 109.382C249.465 111.916 252.972 113.183 257.366 113.183ZM257.366 87.583C254.915 87.583 252.909 86.781 251.346 85.175C249.782 83.571 249.002 81.5 249.002 78.965C249.002 76.431 249.762 74.403 251.283 72.883C252.803 71.362 254.831 70.601 257.366 70.601C259.901 70.601 261.928 71.361 263.449 72.883C264.969 74.403 265.73 76.431 265.73 78.966C265.73 81.5 264.97 83.571 263.45 85.176C261.928 86.781 259.9 87.583 257.366 87.583Z" fill="black"/>
|
||||
<path d="M332.69 126.999C329.057 126.999 326.164 125.941 324.01 123.831C321.855 121.72 320.778 118.847 320.778 115.213C320.778 111.581 321.855 108.686 324.01 106.532C326.164 104.378 329.057 103.3 332.69 103.3C336.322 103.3 339.217 104.378 341.372 106.532C343.526 108.686 344.603 111.582 344.603 115.213C344.603 118.847 343.526 121.72 341.372 123.831C339.217 125.941 336.322 126.999 332.691 126.999H332.69ZM381.862 125.732V33.219H437.369V47.032H397.449V71.112H432.934V84.798H397.45V111.918H437.37V125.732H381.862ZM484.766 126.999C475.725 126.999 468.65 124.528 463.539 119.585C458.426 114.643 455.872 107.906 455.872 99.372V33.219H471.84V99.245C471.84 103.639 472.937 107.061 475.135 109.51C477.331 111.962 480.54 113.185 484.766 113.185C488.905 113.185 492.095 111.962 494.334 109.51C496.572 107.06 497.693 103.639 497.693 99.245V33.219H513.66V99.372C513.66 107.906 511.126 114.642 506.057 119.585C500.987 124.528 493.891 126.999 484.767 126.999H484.766Z" fill="#686868"/>
|
||||
<path d="M549 0H573V148H549V0Z" fill="#FFCC03"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4_4">
|
||||
<rect width="573" height="148" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.7 KiB |
@@ -1,12 +0,0 @@
|
||||
<svg width="573" height="148" viewBox="0 0 573 148" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_4_4)">
|
||||
<path d="M0.739014 125.602V33.09H28.239C34.491 33.09 39.919 34.274 44.524 36.638C49.128 39.004 52.698 42.341 55.233 46.65C57.767 50.958 59.034 56.071 59.034 61.984V96.58C59.034 102.41 57.767 107.5 55.233 111.85C52.698 116.203 49.128 119.581 44.523 121.99C39.918 124.397 34.491 125.601 28.239 125.601L0.739014 125.602ZM16.58 111.028H28.24C32.801 111.028 36.433 109.741 39.138 107.163C41.841 104.587 43.193 101.06 43.193 96.581V61.984C43.193 57.592 41.841 54.107 39.138 51.529C36.433 48.952 32.801 47.663 28.239 47.663H16.58V111.028ZM76.396 125.602V33.09H95.786L121.512 107.86C121.216 104.673 120.942 101.483 120.688 98.292C120.386 94.5366 120.154 90.776 119.991 87.012C119.821 83.169 119.738 79.812 119.738 76.938V33.09H134.185V125.602H114.795L89.323 50.832C89.491 53.283 89.703 56.239 89.956 59.702C90.2137 63.2478 90.4251 66.7967 90.59 70.348C90.758 73.982 90.844 77.318 90.844 80.36V125.602H76.396ZM181.582 126.869C175.245 126.869 169.752 125.812 165.107 123.701C160.46 121.591 156.889 118.569 154.398 114.64C151.905 110.711 150.616 106.086 150.533 100.763H166.374C166.374 104.565 167.747 107.543 170.494 109.698C173.238 111.852 176.976 112.929 181.709 112.929C186.271 112.929 189.839 111.874 192.417 109.761C194.993 107.65 196.282 104.735 196.282 101.017C196.282 97.892 195.374 95.167 193.558 92.842C191.74 90.52 189.142 88.936 185.764 88.09L175.119 85.175C167.852 83.318 162.256 79.979 158.327 75.164C154.398 70.348 152.434 64.518 152.434 57.675C152.434 52.438 153.616 47.875 155.982 43.988C158.347 40.103 161.705 37.103 166.058 34.99C170.408 32.88 175.54 31.822 181.455 31.822C190.409 31.822 197.506 34.125 202.745 38.729C207.983 43.335 210.645 49.523 210.73 57.295H194.888C194.888 53.663 193.704 50.812 191.34 48.741C188.974 46.671 185.637 45.636 181.328 45.636C177.188 45.636 173.978 46.608 171.697 48.551C169.416 50.495 168.275 53.24 168.275 56.788C168.275 60 169.141 62.724 170.873 64.962C172.603 67.202 175.119 68.786 178.413 69.714L189.439 72.756C196.789 74.616 202.407 77.932 206.294 82.704C210.179 87.478 212.124 93.371 212.124 100.383C212.124 105.623 210.856 110.248 208.322 114.26C205.787 118.273 202.239 121.378 197.677 123.574C193.114 125.77 187.748 126.869 181.582 126.869ZM257.366 126.869C251.366 126.869 246.17 125.729 241.778 123.448C237.384 121.167 233.985 117.957 231.577 113.816C229.169 109.678 227.965 104.818 227.965 99.242V59.45C227.965 53.874 229.169 49.017 231.577 44.876C233.984 40.738 237.384 37.526 241.778 35.245C246.17 32.964 251.366 31.823 257.366 31.823C263.449 31.823 268.665 32.963 273.017 35.245C277.367 37.526 280.747 40.738 283.156 44.876C285.563 49.016 286.767 53.874 286.767 59.45V99.243C286.767 104.819 285.563 109.679 283.156 113.817C280.748 117.957 277.346 121.167 272.954 123.449C268.56 125.729 263.364 126.869 257.366 126.869ZM257.366 113.183C261.758 113.183 265.265 111.915 267.885 109.381C270.502 106.846 271.813 103.468 271.813 99.242V59.45C271.813 55.227 270.503 51.846 267.885 49.312C265.265 46.777 261.758 45.51 257.366 45.51C252.972 45.51 249.466 46.777 246.848 49.312C244.228 51.846 242.918 55.227 242.918 59.45V99.243C242.918 103.469 244.228 106.847 246.848 109.382C249.465 111.916 252.972 113.183 257.366 113.183ZM257.366 87.583C254.915 87.583 252.909 86.781 251.346 85.175C249.782 83.571 249.002 81.5 249.002 78.965C249.002 76.431 249.762 74.403 251.283 72.883C252.803 71.362 254.831 70.601 257.366 70.601C259.901 70.601 261.928 71.361 263.449 72.883C264.969 74.403 265.73 76.431 265.73 78.966C265.73 81.5 264.97 83.571 263.45 85.176C261.928 86.781 259.9 87.583 257.366 87.583Z" fill="black"/>
|
||||
<path d="M332.69 126.999C329.057 126.999 326.164 125.941 324.01 123.831C321.855 121.72 320.778 118.847 320.778 115.213C320.778 111.581 321.855 108.686 324.01 106.532C326.164 104.378 329.057 103.3 332.69 103.3C336.322 103.3 339.217 104.378 341.372 106.532C343.526 108.686 344.603 111.582 344.603 115.213C344.603 118.847 343.526 121.72 341.372 123.831C339.217 125.941 336.322 126.999 332.691 126.999H332.69ZM381.862 125.732V33.219H437.369V47.032H397.449V71.112H432.934V84.798H397.45V111.918H437.37V125.732H381.862ZM484.766 126.999C475.725 126.999 468.65 124.528 463.539 119.585C458.426 114.643 455.872 107.906 455.872 99.372V33.219H471.84V99.245C471.84 103.639 472.937 107.061 475.135 109.51C477.331 111.962 480.54 113.185 484.766 113.185C488.905 113.185 492.095 111.962 494.334 109.51C496.572 107.06 497.693 103.639 497.693 99.245V33.219H513.66V99.372C513.66 107.906 511.126 114.642 506.057 119.585C500.987 124.528 493.891 126.999 484.767 126.999H484.766Z" fill="#359CEF"/>
|
||||
<path d="M549 0H573V148H549V0Z" fill="#FFCC03"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4_4">
|
||||
<rect width="573" height="148" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.7 KiB |
BIN
src/assets/onboarding/acl.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
src/assets/onboarding/activity.png
Normal file
|
After Width: | Height: | Size: 261 KiB |
BIN
src/assets/onboarding/posture.png
Normal file
|
After Width: | Height: | Size: 283 KiB |
BIN
src/assets/ssh/ssh-client.png
Normal file
|
After Width: | Height: | Size: 290 KiB |
@@ -6,10 +6,8 @@ import {
|
||||
OidcProvider,
|
||||
} from "@axa-fr/react-oidc";
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import { useLocalStorage } from "@hooks/useLocalStorage";
|
||||
import { useRedirect } from "@hooks/useRedirect";
|
||||
import loadConfig, { buildExtras } from "@utils/config";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { OIDCError } from "@/auth/OIDCError";
|
||||
import { SecureProvider } from "@/auth/SecureProvider";
|
||||
@@ -44,39 +42,11 @@ export default function OIDCProvider({ children }: Props) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const router = useRouter();
|
||||
const path = usePathname();
|
||||
const params = useSearchParams()?.toString();
|
||||
const [, setQueryParams] = useLocalStorage("netbird-query-params", params);
|
||||
|
||||
useEffect(() => {
|
||||
const validParams = [
|
||||
"tab",
|
||||
"search",
|
||||
"id",
|
||||
"invite",
|
||||
"utm_source",
|
||||
"utm_medium",
|
||||
"utm_content",
|
||||
"utm_campaign",
|
||||
"hs_id",
|
||||
"page",
|
||||
"page_size",
|
||||
"user",
|
||||
"port",
|
||||
];
|
||||
|
||||
try {
|
||||
const urlParams = new URLSearchParams(params);
|
||||
if (validParams.some((param) => urlParams.has(param))) {
|
||||
setQueryParams(params);
|
||||
}
|
||||
} catch (e) {}
|
||||
}, []);
|
||||
|
||||
const withCustomHistory = () => {
|
||||
return {
|
||||
replaceState: (url: any) => {
|
||||
router.replace(url);
|
||||
window.dispatchEvent(new Event("popstate"));
|
||||
window?.location?.replace(url);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -105,16 +75,19 @@ export default function OIDCProvider({ children }: Props) {
|
||||
|
||||
// We bypass authentication for pages that do not require auth.
|
||||
// E.g., when we just want to show installation steps for public.
|
||||
if (path === "/install") return children;
|
||||
// Or the instance setup wizard for first-time setup.
|
||||
// Or the invite acceptance page for new users.
|
||||
if (path === "/install" || path === "/setup" || path?.startsWith("/invite"))
|
||||
return children;
|
||||
|
||||
return mounted && providerConfig ? (
|
||||
<OidcProvider
|
||||
configuration={providerConfig}
|
||||
//withCustomHistory={withCustomHistory}
|
||||
withCustomHistory={withCustomHistory}
|
||||
authenticatingComponent={FullScreenLoading}
|
||||
authenticatingErrorComponent={OIDCError}
|
||||
loadingComponent={FullScreenLoading}
|
||||
callbackSuccessComponent={CallBackSuccess}
|
||||
callbackSuccessComponent={FullScreenLoading}
|
||||
onEvent={onEvent}
|
||||
onSessionLost={() => void 0}
|
||||
//sessionLostComponent={SessionLost}
|
||||
@@ -125,11 +98,3 @@ export default function OIDCProvider({ children }: Props) {
|
||||
<FullScreenLoading />
|
||||
);
|
||||
}
|
||||
|
||||
const CallBackSuccess = () => {
|
||||
const params = useSearchParams();
|
||||
const errorParam = params.get("error");
|
||||
const currentPath = usePathname();
|
||||
useRedirect(currentPath, true, !errorParam);
|
||||
return <FullScreenLoading />;
|
||||
};
|
||||
|
||||
@@ -3,6 +3,24 @@ import { usePathname } from "next/navigation";
|
||||
import * as React from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const QUERY_PARAMS_KEY = "netbird-query-params";
|
||||
const PRESERVE_QUERY_PARAMS_PATHS = ["/peer/ssh", "/peer/rdp"];
|
||||
const VALID_PARAMS = [
|
||||
"tab",
|
||||
"search",
|
||||
"id",
|
||||
"invite",
|
||||
"utm_source",
|
||||
"utm_medium",
|
||||
"utm_content",
|
||||
"utm_campaign",
|
||||
"hs_id",
|
||||
"page",
|
||||
"page_size",
|
||||
"user",
|
||||
"port",
|
||||
];
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
@@ -10,6 +28,22 @@ export const SecureProvider = ({ children }: Props) => {
|
||||
const { isAuthenticated, login } = useOidc();
|
||||
const currentPath = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated && !PRESERVE_QUERY_PARAMS_PATHS.includes(currentPath)) {
|
||||
localStorage.removeItem(QUERY_PARAMS_KEY);
|
||||
} else if (!isAuthenticated) {
|
||||
try {
|
||||
const params = window.location.search.substring(1);
|
||||
if (params) {
|
||||
const urlParams = new URLSearchParams(params);
|
||||
if (VALID_PARAMS.some((param) => urlParams.has(param))) {
|
||||
localStorage.setItem(QUERY_PARAMS_KEY, JSON.stringify(params));
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}, [isAuthenticated, currentPath]);
|
||||
|
||||
useEffect(() => {
|
||||
let timeout: NodeJS.Timeout | undefined = undefined;
|
||||
if (!isAuthenticated) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { motion } from "framer-motion";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
@@ -23,7 +24,7 @@ const AccordionTrigger = React.forwardRef<
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center gap-4 font-medium transition-all [&[data-state=open]>svg.chevron]:rotate-180 hover:opacity-80 my-2",
|
||||
"flex flex-1 items-center gap-4 font-medium [&[data-state=open]>svg.chevron]:rotate-180 hover:opacity-80 my-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -36,20 +37,41 @@ const AccordionTrigger = React.forwardRef<
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
HTMLDivElement,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className=" pt-0">{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
>(({ className, children }, ref) => {
|
||||
const wrapperRef = React.useRef<HTMLDivElement>(null);
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const el = wrapperRef.current?.closest("[data-state]");
|
||||
if (!el) return;
|
||||
|
||||
const update = () => setIsOpen(el.getAttribute("data-state") === "open");
|
||||
update();
|
||||
|
||||
const observer = new MutationObserver(update);
|
||||
observer.observe(el, { attributes: true, attributeFilter: ["data-state"] });
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef}>
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={false}
|
||||
animate={{
|
||||
height: isOpen ? "auto" : 0,
|
||||
opacity: isOpen ? 1 : 0,
|
||||
}}
|
||||
transition={{ duration: 0.15, ease: "easeOut" }}
|
||||
className={cn("overflow-hidden text-sm", className)}
|
||||
>
|
||||
<div className="pt-0">{children}</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import { Checkbox } from "@components/Checkbox";
|
||||
import { Input } from "@components/Input";
|
||||
import { Popover, PopoverContent } from "@components/Popover";
|
||||
import { useElementSize } from "@hooks/useElementSize";
|
||||
import { Anchor } from "@radix-ui/react-popover";
|
||||
import * as React from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { FaWindows } from "react-icons/fa6";
|
||||
|
||||
type Props = {};
|
||||
export const AutoCompleteInput = ({}: Props) => {
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [elementWidth, { width }] = useElementSize<HTMLDivElement>();
|
||||
|
||||
useEffect(() => {
|
||||
const input = inputRef.current;
|
||||
|
||||
const onFocus = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
if (input) {
|
||||
inputRef.current.addEventListener("focus", onFocus);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (input) {
|
||||
inputRef.current.removeEventListener("focus", onFocus);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={"z-10 relative"}>
|
||||
<Popover modal={false} open={open} onOpenChange={setOpen}>
|
||||
<Anchor ref={elementWidth}>
|
||||
<Input
|
||||
placeholder={"11"}
|
||||
ref={inputRef}
|
||||
maxWidthClass={"max-w-[200px]"}
|
||||
customPrefix={
|
||||
<div className={"flex items-center gap-2"}>
|
||||
<Checkbox></Checkbox>
|
||||
<div
|
||||
className={"flex gap-2 items-center text-sm text-nb-gray-200"}
|
||||
>
|
||||
<FaWindows className={"text-sky-600 text-lg"} />
|
||||
Windows
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Anchor>
|
||||
|
||||
<PopoverContent
|
||||
hideWhenDetached={false}
|
||||
className="w-full p-0 shadow-sm shadow-nb-gray-950"
|
||||
style={{
|
||||
width: width,
|
||||
}}
|
||||
forceMount={true}
|
||||
align="start"
|
||||
side={"bottom"}
|
||||
sideOffset={10}
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
onCloseAutoFocus={(event) => event.preventDefault()}
|
||||
onInteractOutside={(event) => {
|
||||
event.preventDefault();
|
||||
if (event.target !== inputRef.current) {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
onPointerDownOutside={(event) => {
|
||||
event.preventDefault();
|
||||
if (event.target !== inputRef.current) {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
onFocusOutside={(event) => {
|
||||
event.preventDefault();
|
||||
if (event.target !== inputRef.current) {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
></PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -23,6 +23,7 @@ const variants = cva("", {
|
||||
purple: ["bg-purple-950/50 border-purple-500 border text-purple-500"],
|
||||
yellow: ["bg-yellow-950 border-yellow-500 border text-yellow-400"],
|
||||
gray: ["bg-nb-gray-930/60 border-nb-gray-800/40 text-nb-gray-300 border"],
|
||||
lightGray: ["bg-nb-gray-910 text-nb-gray-200 border border-nb-gray-900"],
|
||||
grayer: [
|
||||
"bg-nb-gray-900/40 border-nb-gray-800/40 text-nb-gray-300 border",
|
||||
],
|
||||
@@ -32,6 +33,10 @@ const variants = cva("", {
|
||||
green: ["bg-green-950 border-green-500 border text-green-400"],
|
||||
netbird: ["bg-netbird-950 border-netbird-500 border text-netbird-500"],
|
||||
},
|
||||
size: {
|
||||
default: "text-[0.75rem] py-1.5 px-3",
|
||||
xs: "text-[0.6rem] py-[0.3rem] px-2",
|
||||
},
|
||||
hover: {
|
||||
none: [],
|
||||
blue: ["hover:bg-sky-200"],
|
||||
@@ -41,8 +46,9 @@ const variants = cva("", {
|
||||
"blue-darker": ["hover:bg-sky-800"],
|
||||
red: ["hover:bg-red-950/40"],
|
||||
gray: ["hover:bg-nb-gray-900"],
|
||||
lightGray: ["hover:bg-nb-gray-900"],
|
||||
grayer: ["hover:bg-nb-gray-900"],
|
||||
"gray-ghost": ["hover:bg-nb-gray-900"],
|
||||
"gray-ghost": ["hover:bg-nb-gray-800 cursor-pointer"],
|
||||
green: ["hover:bg-green-950/50"],
|
||||
netbird: ["hover:bg-netbird-950/50"],
|
||||
},
|
||||
@@ -53,6 +59,7 @@ export default function Badge({
|
||||
children,
|
||||
className,
|
||||
variant = "blue",
|
||||
size = "default",
|
||||
useHover = false,
|
||||
disabled = false,
|
||||
...props
|
||||
@@ -60,8 +67,8 @@ export default function Badge({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative z-10 cursor-inherit whitespace-nowrap rounded-md text-[12px] py-1.5 px-3 font-normal flex gap-1.5 items-center justify-center transition-all",
|
||||
variants({ variant, hover: useHover ? variant : "none" }),
|
||||
"relative z-10 cursor-inherit whitespace-nowrap rounded-md font-normal flex gap-1.5 items-center justify-center transition-all",
|
||||
variants({ variant, hover: useHover ? variant : "none", size }),
|
||||
disabled && "cursor-not-allowed opacity-50 select-none",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cn } from "@utils/helpers";
|
||||
import { ChevronRightIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
@@ -25,8 +25,6 @@ export const Item = ({
|
||||
active,
|
||||
disabled = false,
|
||||
}: ItemProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -45,7 +43,13 @@ export const Item = ({
|
||||
)}
|
||||
>
|
||||
{icon && icon}
|
||||
{href ? <span onClick={() => router.push(href)}>{label}</span> : label}
|
||||
{href ? (
|
||||
<Link href={href} data-cy={"breadcrumb-item"}>
|
||||
{label}
|
||||
</Link>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -34,7 +34,7 @@ export const buttonVariants = cva(
|
||||
secondary: [
|
||||
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
|
||||
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20 ",
|
||||
"dark:bg-nb-gray-920 dark:text-gray-400 dark:border-gray-700/40 dark:hover:text-white dark:hover:bg-zinc-800/50",
|
||||
"dark:bg-nb-gray-920 dark:text-gray-400 dark:border-gray-700/40 dark:hover:text-white dark:hover:bg-nb-gray-910",
|
||||
],
|
||||
secondaryLighter: [
|
||||
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
|
||||
@@ -54,7 +54,7 @@ export const buttonVariants = cva(
|
||||
dotted: [
|
||||
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900 border-dashed",
|
||||
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20 ",
|
||||
"dark:bg-nb-gray-900/30 dark:text-gray-400 dark:border-gray-500/40 dark:hover:text-white dark:hover:bg-zinc-800/50",
|
||||
"dark:bg-nb-gray-900/30 dark:text-gray-400 dark:border-gray-500/40 dark:hover:text-white dark:hover:bg-nb-gray-900/50",
|
||||
],
|
||||
tertiary: [
|
||||
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
|
||||
@@ -73,9 +73,13 @@ export const buttonVariants = cva(
|
||||
"enabled:dark:focus:ring-red-800/20 enabled:dark:focus:bg-red-950/40 enabled:hover:dark:bg-red-950/50 enabled:dark:hover:border-red-800/50 dark:bg-transparent dark:text-red-500",
|
||||
"",
|
||||
],
|
||||
"danger-text": [
|
||||
"dark:bg-transparent dark:text-red-500 dark:hover:text-red-600 dark:border-transparent !px-0 !shadow-none !py-0 focus:ring-red-500/30 dark:ring-offset-neutral-950/50 rounded-sm",
|
||||
],
|
||||
"default-outline": [
|
||||
"dark:ring-offset-nb-gray-950/50 dark:focus:ring-nb-gray-500/20",
|
||||
"dark:bg-transparent dark:text-nb-gray-400 dark:border-transparent dark:hover:text-white dark:hover:bg-nb-gray-900/30 dark:hover:border-nb-gray-800/50",
|
||||
"data-[state=open]:dark:text-white data-[state=open]:dark:bg-nb-gray-900/30 data-[state=open]:dark:border-nb-gray-800/50",
|
||||
],
|
||||
danger: [
|
||||
"", // TODO - add danger button styles for light mode
|
||||
|
||||
@@ -34,7 +34,7 @@ const ButtonGroupButton = forwardRef(
|
||||
border={2}
|
||||
rounded={false}
|
||||
className={cn(
|
||||
"first:border-l-0 last:border-r-0 border-t-0 border-b-0 h-[41px]",
|
||||
"first:border-l-0 last:border-r-0 border-t-0 border-b-0 h-[40px]",
|
||||
"!py-2.5 !px-4",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -19,6 +19,8 @@ export const calloutVariants = cva(
|
||||
default: "bg-nb-gray-900/60 border-nb-gray-800/80 text-nb-gray-300",
|
||||
warning: "bg-netbird-500/10 border-netbird-400/20 text-netbird-150",
|
||||
info: "bg-sky-400/10 border-sky-400/20 text-sky-100",
|
||||
success: "bg-green-400/15 border-green-400/20 text-green-100",
|
||||
error: "bg-red-500/10 border-red-400/20 text-red-100",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -50,11 +50,11 @@ function CardListItem({
|
||||
return (
|
||||
<li
|
||||
className={cn(
|
||||
"flex justify-between px-4 border-b border-nb-gray-900 py-4 last:border-b-0 items-center h-full",
|
||||
"flex justify-between px-4 border-b border-nb-gray-900 py-3.5 last:border-b-0 items-center h-full",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className={"flex gap-2.5 items-center text-sm"}>{label}</div>
|
||||
<div className={"flex gap-2.5 items-center text-[0.84rem]"}>{label}</div>
|
||||
<div className={"flex flex-col gap-2"}>
|
||||
<CardTextItem
|
||||
label={label}
|
||||
@@ -100,7 +100,7 @@ const CardTextItem = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"text-right text-nb-gray-400 text-sm flex items-center gap-2",
|
||||
"text-right text-nb-gray-400 text-[0.84rem] flex items-center gap-2",
|
||||
copy && "cursor-pointer hover:text-nb-gray-300 transition-all",
|
||||
)}
|
||||
onClick={() =>
|
||||
|
||||
129
src/components/CardTable.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import useCopyToClipboard from "@hooks/useCopyToClipboard";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { Copy } from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
type CardTableProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function CardTable({ children, className }: CardTableProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-nb-gray-940 rounded-md border border-nb-gray-900 w-full overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<table className={"w-full border-collapse text-sm"}>{children}</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CardTableHeader({ children, className }: CardTableProps) {
|
||||
return (
|
||||
<thead>
|
||||
<tr
|
||||
className={cn(
|
||||
"border-b border-nb-gray-900",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
|
||||
type CardTableHeaderCellProps = {
|
||||
children: React.ReactNode;
|
||||
width?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function CardTableHeaderCell({
|
||||
children,
|
||||
width,
|
||||
className,
|
||||
}: CardTableHeaderCellProps) {
|
||||
return (
|
||||
<th
|
||||
className={cn(
|
||||
"px-4 py-2.5 text-left text-sm font-normal",
|
||||
className,
|
||||
)}
|
||||
style={width ? { width } : undefined}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
function CardTableBody({ children, className }: CardTableProps) {
|
||||
return <tbody className={className}>{children}</tbody>;
|
||||
}
|
||||
|
||||
type CardTableRowProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function CardTableRow({ children, className }: CardTableRowProps) {
|
||||
return (
|
||||
<tr
|
||||
className={cn(
|
||||
"border-b border-nb-gray-900 last:border-b-0",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
type CardTableCellProps = {
|
||||
children: React.ReactNode;
|
||||
copy?: boolean;
|
||||
copyText?: string;
|
||||
width?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function CardTableCell({
|
||||
children,
|
||||
copy = false,
|
||||
copyText,
|
||||
width,
|
||||
className,
|
||||
}: CardTableCellProps) {
|
||||
const [, copyToClipBoard] = useCopyToClipboard(copyText ?? "");
|
||||
return (
|
||||
<td
|
||||
className={cn("px-4 py-3", className)}
|
||||
style={width ? { width } : undefined}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"text-nb-gray-400 text-sm flex items-center gap-2",
|
||||
copy && "cursor-pointer hover:text-nb-gray-300 transition-all",
|
||||
)}
|
||||
onClick={() =>
|
||||
copy &&
|
||||
copyToClipBoard(`${copyText} has been copied to clipboard.`)
|
||||
}
|
||||
>
|
||||
{children}
|
||||
{copy && <Copy size={13} className={"shrink-0"} />}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
CardTable.Header = CardTableHeader;
|
||||
CardTable.HeaderCell = CardTableHeaderCell;
|
||||
CardTable.Body = CardTableBody;
|
||||
CardTable.Row = CardTableRow;
|
||||
CardTable.Cell = CardTableCell;
|
||||
|
||||
export default CardTable;
|
||||
@@ -9,6 +9,11 @@ type Props = {
|
||||
iconAlignment?: "left" | "right";
|
||||
className?: string;
|
||||
alwaysShowIcon?: boolean;
|
||||
// Overrides the rendered innerText as the value written to the
|
||||
// clipboard. Use when the displayed text is an abbreviation of the
|
||||
// canonical value (e.g. the short DNS label) but the user should
|
||||
// still get the full string when they click.
|
||||
textToCopy?: string;
|
||||
};
|
||||
|
||||
export default function CopyToClipboardText({
|
||||
@@ -17,16 +22,13 @@ export default function CopyToClipboardText({
|
||||
iconAlignment = "right",
|
||||
className,
|
||||
alwaysShowIcon = false,
|
||||
textToCopy,
|
||||
}: Props) {
|
||||
const [wrapper, copyToClipboard, copied] = useCopyToClipboard();
|
||||
const [wrapper, copyToClipboard, copied] = useCopyToClipboard(textToCopy);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex gap-2 items-center group cursor-pointer transition-all hover:underline underline-offset-4 decoration-dashed decoration-nb-gray-600",
|
||||
!copied && "hover:opacity-90",
|
||||
className,
|
||||
)}
|
||||
className={cn("flex gap-2 items-center group cursor-pointer", className)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@@ -34,27 +36,34 @@ export default function CopyToClipboardText({
|
||||
}}
|
||||
ref={wrapper}
|
||||
>
|
||||
{children}
|
||||
<span className="relative truncate">
|
||||
{children}
|
||||
<span className="absolute bottom-0 left-0 right-0 border-b border-dashed border-transparent group-hover:border-nb-gray-500 pointer-events-none" />
|
||||
</span>
|
||||
|
||||
{copied ? (
|
||||
<span
|
||||
className={cn(
|
||||
"shrink-0",
|
||||
iconAlignment === "left" ? "order-first" : "order-last",
|
||||
)}
|
||||
>
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"text-nb-gray-100 group-hover:opacity-100 shrink-0",
|
||||
iconAlignment === "left" ? "order-first" : "order-last",
|
||||
!alwaysShowIcon && "opacity-0",
|
||||
"text-nb-gray-100 group-hover:opacity-100",
|
||||
!copied && "hidden",
|
||||
!alwaysShowIcon && !copied && "opacity-0",
|
||||
)}
|
||||
size={11}
|
||||
/>
|
||||
) : (
|
||||
<CopyIcon
|
||||
className={cn(
|
||||
"text-nb-gray-100 group-hover:opacity-100 shrink-0",
|
||||
iconAlignment === "left" ? "order-first" : "order-last",
|
||||
"text-nb-gray-100 group-hover:opacity-100",
|
||||
copied && "hidden",
|
||||
!alwaysShowIcon && "opacity-0",
|
||||
)}
|
||||
size={11}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ interface Props {
|
||||
value?: DateRange;
|
||||
onChange?: (range: DateRange | undefined) => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const defaultRanges = {
|
||||
@@ -61,6 +62,7 @@ export function DatePickerWithRange({
|
||||
className,
|
||||
value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}: Readonly<Props>) {
|
||||
const isActive = useMemo(() => {
|
||||
return {
|
||||
@@ -120,6 +122,7 @@ export function DatePickerWithRange({
|
||||
<Button
|
||||
id="date"
|
||||
variant={"secondary"}
|
||||
disabled={disabled}
|
||||
className={cn("max-w-[260px] justify-start text-left font-normal")}
|
||||
>
|
||||
<CalendarIcon size={16} className={"shrink-0"} />
|
||||
|
||||
95
src/components/DeviceCard.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import TruncatedText from "@components/ui/TruncatedText";
|
||||
import { cn } from "@utils/helpers";
|
||||
import * as React from "react";
|
||||
import { useMemo } from "react";
|
||||
import RoundedFlag from "@/assets/countries/RoundedFlag";
|
||||
import { PeerOSIcon } from "@/assets/icons/PeerOSIcon";
|
||||
import { ResourceIcon } from "@/assets/icons/ResourceIcon";
|
||||
import { NetworkResource } from "@/interfaces/Network";
|
||||
import type { Peer } from "@/interfaces/Peer";
|
||||
|
||||
type DeviceCardProps = {
|
||||
device?: Peer;
|
||||
resource?: NetworkResource;
|
||||
className?: string;
|
||||
address?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export const DeviceCard = ({
|
||||
device,
|
||||
resource,
|
||||
className,
|
||||
address,
|
||||
description,
|
||||
}: DeviceCardProps) => {
|
||||
if (!device && !resource) return null;
|
||||
|
||||
const descriptionText = useMemo(() => {
|
||||
return description !== undefined
|
||||
? description
|
||||
: address || device?.ip || resource?.address;
|
||||
}, [description, address, device]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex shrink-0 items-center gap-2.5 text-nb-gray-200 text-left py-1 pl-3 pr-4 rounded-md group/machine my-0 w-[230px]",
|
||||
!descriptionText && "py-2",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded-md h-9 w-9 shrink-0 bg-nb-gray-900 transition-all",
|
||||
"group-hover:bg-nb-gray-800 relative",
|
||||
)}
|
||||
>
|
||||
{device ? (
|
||||
<PeerOSIcon os={device.os} />
|
||||
) : resource?.type ? (
|
||||
<ResourceIcon type={resource.type} />
|
||||
) : null}
|
||||
|
||||
{device?.country_code && (
|
||||
<div className={"absolute -bottom-[4px] -right-[4px]"}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded-full border-[3px] shrink-0",
|
||||
"border-nb-gray-940",
|
||||
)}
|
||||
>
|
||||
<RoundedFlag country={device?.country_code} size={10} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
"flex flex-col gap-0 justify-center top-[0.15rem] leading-tight relative"
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={
|
||||
"font-normal text-[0.85rem] text-nb-gray-100 flex items-center gap-2"
|
||||
}
|
||||
>
|
||||
<TruncatedText
|
||||
text={device?.name || resource?.name || "Unknown"}
|
||||
maxWidth={"150px"}
|
||||
hideTooltip={true}
|
||||
/>
|
||||
</span>
|
||||
{descriptionText && (
|
||||
<span
|
||||
className={
|
||||
"text-sm font-normal text-nb-gray-400 relative whitespace-nowrap"
|
||||
}
|
||||
>
|
||||
<TruncatedText text={descriptionText} maxWidth={"160px"} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||