Compare commits

..
Author SHA1 Message Date
braginini 92528d8377 Clear up password from the state 2025-12-29 09:17:30 -05:00
braginini 876f803033 Create user for self-hosted 2025-12-29 08:56:52 -05:00
braginini 43286352d1 Add user creation with password copy 2025-12-25 19:35:19 -05:00
217 changed files with 3729 additions and 12331 deletions

No files matched your search

+1 -12
View File
@@ -19,7 +19,7 @@ jobs:
- name: setup-node
uses: actions/setup-node@v3
with:
node-version: '20'
node-version: '18'
cache: 'npm'
- name: Install dependencies
@@ -54,19 +54,8 @@ 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
+1 -1
View File
@@ -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, BrunnenstraĂźe 196, 10119 Berlin, Germany,
submitting this Agreement and NetBird GmbH, c/o Max-Beer-StraĂźe 2-4 MĂĽnzstraĂźe 12 10178 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
+8 -11
View File
@@ -10,7 +10,6 @@ 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)
@@ -18,10 +17,10 @@ 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
@@ -34,9 +33,8 @@ The dashboard makes it possible to:
- 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, JWT for the management API).
use Auth0 service that covers all our needs (user management, social login, JTW 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/)
@@ -45,9 +43,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) up until "Configure Allowed Web Origins"
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"
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.
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.
5. Run docker container without SSL (Let's Encrypt):
```shell
@@ -56,10 +54,9 @@ 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 MANAGEMENT API URL> \
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> \
netbirdio/dashboard:main
```
6. Run docker container with SSL (Let's Encrypt):
```shell
@@ -71,7 +68,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 MANAGEMENT API URL> \
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> \
netbirdio/dashboard:main
```
@@ -87,11 +84,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
View File
@@ -1,12 +0,0 @@
[
{
"tag": "New",
"text": "Custom DNS Zones for Private Network Resolution",
"link": "https://netbird.io/knowledge-hub/custom-dns-zones",
"linkText": "Read Release Article",
"variant": "important",
"isExternal": true,
"closeable": true,
"isCloudOnly": false
}
]
+1 -1
View File
@@ -15,4 +15,4 @@
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID",
"googleTagManagerID": "$NETBIRD_GOOGLE_TAG_MANAGER_ID",
"wasmPath": "$NETBIRD_WASM_PATH"
}
}
+2 -12
View File
@@ -14,24 +14,14 @@ server {
location / {
try_files $uri $uri.html $uri/ =404;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: ws: wss: https:;" always;
add_header Last-Modified "";
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
expires off;
}
error_page 404 /404.html;
location = /404.html {
internal;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: ws: wss: https:;" always;
add_header Last-Modified "";
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
expires off;
}
}
+1 -88
View File
@@ -62,93 +62,9 @@ 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}
export NETBIRD_CSP=${NETBIRD_CSP}
export NETBIRD_CSP_CONNECT_SRC=${NETBIRD_CSP_CONNECT_SRC}
export NETBIRD_DISABLE_CSP=${NETBIRD_DISABLE_CSP}
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
# Build CSP
if [[ "${NETBIRD_DISABLE_CSP}" == "true" ]]; then
echo "CSP disabled via NETBIRD_DISABLE_CSP"
sed -i '/add_header Content-Security-Policy/d' /etc/nginx/http.d/default.conf
else
FIRST_PARTY_CSP="https://pkgs.netbird.io"
FIRST_PARTY_CSP_CONNECT_SRC="$NETBIRD_CSP_CONNECT_SRC"
THIRD_PARTY_CSP=""
THIRD_PARTY_CSP_CONNECT_SRC="https://api.github.com/repos/netbirdio/netbird/releases/latest https://raw.githubusercontent.com/netbirdio/dashboard/"
THIRD_PARTY_CSP_SCRIPT_SRC=""
CSP_DOMAINS=""
CSP_DOMAINS_CONNECT_SRC=""
if [[ -n "${NETBIRD_CSP}" ]]; then
CSP_DOMAINS="$CSP_DOMAINS $NETBIRD_CSP"
fi
# Add AUTH_AUTHORITY to CSP
if [[ -n "${AUTH_AUTHORITY}" ]]; then
CSP_DOMAINS="$CSP_DOMAINS $AUTH_AUTHORITY"
fi
# Add AUTH_AUDIENCE to CSP
if [[ -n "${AUTH_AUDIENCE}" && "${AUTH_AUDIENCE}" != "none" && "${AUTH_AUDIENCE}" == *.* ]]; then
if [[ "${AUTH_AUDIENCE}" == *"http://"* || "${AUTH_AUDIENCE}" == *"https://"* ]]; then
CSP_DOMAINS="$CSP_DOMAINS $AUTH_AUDIENCE"
else
CSP_DOMAINS="$CSP_DOMAINS https://$AUTH_AUDIENCE"
fi
fi
# Add NETBIRD_MGMT_API_ENDPOINT to CSP
if [[ -n "${NETBIRD_MGMT_API_ENDPOINT}" ]]; then
MGMT_HOST=$(echo "$NETBIRD_MGMT_API_ENDPOINT" | sed -E 's|https?://||' | cut -d'/' -f1)
if [[ -n "$MGMT_HOST" ]]; then
if [[ "$NETBIRD_MGMT_API_ENDPOINT" == https://* ]]; then
CSP_DOMAINS="$CSP_DOMAINS $NETBIRD_MGMT_API_ENDPOINT"
CSP_DOMAINS_CONNECT_SRC="$CSP_DOMAINS_CONNECT_SRC wss://$MGMT_HOST"
elif [[ "$NETBIRD_MGMT_API_ENDPOINT" == http://* ]]; then
CSP_DOMAINS="$CSP_DOMAINS $NETBIRD_MGMT_API_ENDPOINT"
CSP_DOMAINS_CONNECT_SRC="$CSP_DOMAINS_CONNECT_SRC ws://$MGMT_HOST"
fi
fi
fi
# Add LETSENCRYPT_DOMAIN to CSP
if [[ -n "${LETSENCRYPT_DOMAIN}" && "${LETSENCRYPT_DOMAIN}" != "none" ]]; then
if [[ "$LETSENCRYPT_DOMAIN" == *"localhost"* ]]; then
CSP_DOMAINS="$CSP_DOMAINS http://$LETSENCRYPT_DOMAIN"
CSP_DOMAINS_CONNECT_SRC="$CSP_DOMAINS_CONNECT_SRC ws://$LETSENCRYPT_DOMAIN ws://*.$LETSENCRYPT_DOMAIN"
else
CSP_DOMAINS="$CSP_DOMAINS https://$LETSENCRYPT_DOMAIN"
CSP_DOMAINS_CONNECT_SRC="$CSP_DOMAINS_CONNECT_SRC wss://$LETSENCRYPT_DOMAIN wss://*.$LETSENCRYPT_DOMAIN"
fi
fi
CSP_CONNECT_SRC="$CSP_DOMAINS $CSP_DOMAINS_CONNECT_SRC $FIRST_PARTY_CSP $FIRST_PARTY_CSP_CONNECT_SRC $THIRD_PARTY_CSP $THIRD_PARTY_CSP_CONNECT_SRC"
CSP_FRAME_SRC="$CSP_DOMAINS $FIRST_PARTY_CSP $THIRD_PARTY_CSP"
CSP_SCRIPT_SRC="$CSP_DOMAINS $FIRST_PARTY_CSP $THIRD_PARTY_CSP $THIRD_PARTY_CSP_SCRIPT_SRC"
# Remove duplicates
CSP_CONNECT_SRC=$(echo $CSP_CONNECT_SRC | tr ' ' '\n' | grep -v '^$' | sort -u | tr '\n' ' ' | sed 's/ $//')
CSP_FRAME_SRC=$(echo $CSP_FRAME_SRC | tr ' ' '\n' | grep -v '^$' | sort -u | tr '\n' ' ' | sed 's/ $//')
CSP_SCRIPT_SRC=$(echo $CSP_SCRIPT_SRC | tr ' ' '\n' | grep -v '^$' | sort -u | tr '\n' ' ' | sed 's/ $//')
# Update CSP in nginx config
CSP_POLICY="default-src 'none'; connect-src 'self' $CSP_CONNECT_SRC; frame-src 'self' $CSP_FRAME_SRC; script-src 'self' 'wasm-unsafe-eval' $CSP_SCRIPT_SRC; font-src 'self'; img-src * data:; manifest-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests;"
CSP_HEADER="add_header Content-Security-Policy \"$CSP_POLICY\" always;"
# Escape sed special characters in replacement string (& | \)
CSP_HEADER=$(printf '%s' "$CSP_HEADER" | sed -e 's/[\\&|]/\\&/g')
echo "CSP header: $CSP_HEADER"
# Replace CSP header in nginx config
sed -i "s|add_header Content-Security-Policy \"[^\"]*\" always;|$CSP_HEADER|g" /etc/nginx/http.d/default.conf || {
echo "Failed to replace CSP header"
}
fi
# 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 \$\$NETBIRD_WASM_PATH"
@@ -158,7 +74,4 @@ for f in $(grep -R -l AUTH_SUPPORTED_SCOPES /usr/share/nginx/html); do
cp "$f" "$f".copy
envsubst "$ENV_STR" < "$f".copy > "$f"
rm "$f".copy
done
# Reload nginx to pick up the updated CSP and config
nginx -s reload
done
-2
View File
@@ -7,8 +7,6 @@ const nextConfig = {
reactStrictMode: false,
env: {
APP_ENV: process.env.APP_ENV || "production",
NEXT_PUBLIC_DASHBOARD_VERSION:
process.env.NEXT_PUBLIC_DASHBOARD_VERSION || "development",
},
};
+2294 -2104
View File
File diff suppressed because it is too large. Load diff
+32 -37
View File
@@ -2,49 +2,45 @@
"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",
"dev": "next dev -p 3000",
"turbo": "next dev -p 3000 --turbo",
"build": "next build",
"postbuild": "node postbuild.js",
"start": "next start",
"lint": "next lint",
"cypress:open": "cypress open"
},
"dependencies": {
"@axa-fr/react-oidc": "^7.26.3",
"@axa-fr/react-oidc": "^7.22.18",
"@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",
"@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",
"@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": "^19",
"@types/react-dom": "^19",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-window": "^1.8.8",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
@@ -53,9 +49,8 @@
"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": "^1.1.1",
"cmdk": "^0.2.0",
"crypto-js": "^4.2.0",
"d3": "^7.9.0",
"date-fns": "^2.30.0",
@@ -63,23 +58,23 @@
"elkjs": "^0.10.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"framer-motion": "^12.29.2",
"ip-address": "^10.1.0",
"flowbite": "^1.8.1",
"flowbite-react": "^0.6.4",
"framer-motion": "^10.16.4",
"ip-cidr": "^3.1.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.23",
"lodash": "^4.17.21",
"lucide-react": "^0.539.0",
"next": "^16.1.6",
"next": "^14.2.35",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
"react": "^19.2.4",
"react-day-picker": "^9.13.0",
"react-dom": "^19.2.4",
"react": "^18.3.1",
"react-day-picker": "^8.9.1",
"react-dom": "^18.3.1",
"react-ga4": "^2.1.0",
"react-hot-toast": "^2.4.1",
"react-hotjar": "^6.3.1",
"react-hotjar": "^6.2.0",
"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",
@@ -95,7 +90,7 @@
"@types/chroma-js": "^3.1.1",
"@types/js-cookie": "^3.0.6",
"eslint": "^9.39.1",
"eslint-config-next": "^16.1.6",
"eslint-config-next": "^16.0.5",
"postcss": "^8",
"prettier": "3.0.3",
"tailwindcss": "^3.4.17"
-96
View File
@@ -1,96 +0,0 @@
const { resolve, join } = require("path");
const { createHash } = require("crypto");
const {
readFileSync,
writeFileSync,
mkdirSync,
readdirSync,
statSync,
} = require("fs");
process.env.NODE_ENV = "production";
const PLACEHOLDER = "NB_INLINE_SCRIPT_PLACEHOLDER";
console.log("Starting post-build script to extract inline scripts...");
// Function to find HTML files recursively
function findHtmlFiles(dir) {
const files = [];
const entries = readdirSync(dir);
for (const entry of entries) {
const fullPath = join(dir, entry);
const stat = statSync(fullPath);
if (stat.isDirectory()) {
files.push(...findHtmlFiles(fullPath));
} else if (entry.endsWith(".html")) {
files.push(fullPath);
}
}
return files;
}
// For Next.js export output, the files are in the 'out' directory
const baseDir = resolve("out");
const htmlFiles = findHtmlFiles(baseDir);
console.log(`Found ${htmlFiles.length} .html files to process`);
// Ensure assets directory exists
const assetsDir = `${baseDir}/assets`;
mkdirSync(assetsDir, { recursive: true });
htmlFiles.forEach((file) => {
// Read file contents
const contents = readFileSync(file, "utf8");
const scripts = [];
// Extract inline scripts
const newFile = contents.replace(
/<script(?![^>]*src)([^>]*)>(.+?)<\/script>/gs,
(match, attributes, scriptContent) => {
// Skip if script has src attribute (external script)
if (attributes.includes("src=")) {
return match;
}
const addPlaceholderString = scripts.length === 0;
const cleanedScript = scriptContent.trim();
if (cleanedScript) {
scripts.push(
`${cleanedScript}${cleanedScript.endsWith(";") ? "" : ";"}`,
);
}
return addPlaceholderString ? PLACEHOLDER : "";
},
);
// Early exit if no inline scripts found
if (!scripts.length) {
console.log(`No inline scripts found`);
return;
}
// Combine scripts and create hash
const chunk = scripts.join("\n");
const hash = createHash("md5").update(chunk).digest("hex").slice(0, 8);
const chunkFileName = `chunk.${hash}.js`;
const chunkPath = `${assetsDir}/${chunkFileName}`;
// Write the chunk file
writeFileSync(chunkPath, chunk, "utf8");
// Replace placeholder string with script tag
const updatedFile = newFile.replace(
PLACEHOLDER,
`<script src="/assets/${chunkFileName}" crossorigin=""></script>`,
);
// Write updated HTML file
writeFileSync(file, updatedFile, "utf8");
});
console.log("Post-build script completed successfully!");
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -7,7 +7,7 @@ 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 { ExternalLinkIcon, ServerIcon } from "lucide-react";
import React, { lazy, Suspense } from "react";
import DNSIcon from "@/assets/icons/DNSIcon";
import { usePermissions } from "@/contexts/PermissionsProvider";
@@ -15,7 +15,7 @@ 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() {
@@ -40,7 +40,7 @@ export default function NameServers() {
href={"/dns/nameservers"}
label={"Nameservers"}
active
icon={<DNSIcon size={13} />}
icon={<ServerIcon size={13} />}
/>
</Breadcrumbs>
<h1 ref={headingRef}>Nameservers</h1>
-8
View File
@@ -1,8 +0,0 @@
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;
-70
View File
@@ -1,70 +0,0 @@
"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 React, { 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 { 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={"DNS"} icon={<DNSIcon size={13} />} />
<Breadcrumbs.Item
href={"/dns/zones"}
label={"Zones"}
active
icon={<DNSZoneIcon size={16} />}
/>
</Breadcrumbs>
<h1 ref={headingRef}>Zones</h1>
<Paragraph>
Manage DNS zones to control domain name resolution for your network.
</Paragraph>
<Paragraph>
Learn more about
<InlineLink href={DNS_ZONE_DOCS_LINK} target={"_blank"}>
DNS Zones
<ExternalLinkIcon size={12} />
</InlineLink>
in our documentation.
</Paragraph>
</div>
<RestrictedAccess page={"DNS Zones"} hasAccess={permission?.dns?.read}>
<Suspense fallback={<SkeletonTable />}>
<DNSZonesProvider>
<DNSZonesTable
isLoading={isLoading}
headingTarget={portalTarget}
data={zones}
/>
</DNSZonesProvider>
</Suspense>
</RestrictedAccess>
</PageContainer>
);
}
+8 -51
View File
@@ -15,7 +15,6 @@ import { useSearchParams } from "next/navigation";
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";
@@ -25,7 +24,6 @@ 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";
@@ -136,9 +134,7 @@ const validAllGroupTabs = [
"resources",
"network-routes",
"nameservers",
"zones",
];
const validOtherGroupTabs = ["users", "peers", "setup-keys"];
const GroupOverviewTabs = ({ group }: { group: Group }) => {
@@ -158,7 +154,7 @@ const GroupOverviewTabs = ({ group }: { group: Group }) => {
};
const [tab, setTab] = useState(getInitialTab());
const { groupDetails, isLoading } = useGroupDetails(group?.id || "");
const groupDetails = useGroupDetails(group?.id || "");
const peersCount = groupDetails?.peers_count || 0;
const usersCount = groupDetails?.users?.length || 0;
@@ -166,7 +162,6 @@ const GroupOverviewTabs = ({ group }: { group: Group }) => {
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 (
@@ -254,19 +249,6 @@ const GroupOverviewTabs = ({ group }: { group: Group }) => {
{singularize("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("Zones", zonesCount)}
</TabsTrigger>
{group.name !== "All" && (
<TabsTrigger
value={"setup-keys"}
@@ -284,56 +266,31 @@ const GroupOverviewTabs = ({ group }: { group: Group }) => {
</TabsList>
<TabsContent value={"users"} className={"pb-8"}>
<GroupUsersSection users={groupDetails?.users} isLoading={isLoading} />
<GroupUsersSection users={groupDetails?.users} />
</TabsContent>
<TabsContent value={"peers"} className={"pb-8"}>
<GroupPeersSection
peers={groupDetails?.peersOfGroup}
isLoading={isLoading}
/>
<GroupPeersSection peers={groupDetails?.peersOfGroup} />
</TabsContent>
<TabsContent value={"policies"} className={"pb-8"}>
<GroupPoliciesSection
policies={groupDetails?.policies}
isLoading={isLoading}
/>
<GroupPoliciesSection policies={groupDetails?.policies} />
</TabsContent>
<TabsContent value={"resources"} className={"pb-8"}>
<GroupResourcesSection
resources={groupDetails?.networkResources}
isLoading={isLoading}
/>
<GroupResourcesSection resources={groupDetails?.networkResources} />
</TabsContent>
<TabsContent value={"network-routes"} className={"pb-8"}>
<GroupNetworkRoutesSection
routes={groupDetails?.routes}
isLoading={isLoading}
/>
<GroupNetworkRoutesSection routes={groupDetails?.routes} />
</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}
/>
<GroupNameserversSection nameserverGroups={groupDetails?.nameservers} />
</TabsContent>
<TabsContent value={"setup-keys"} className={"pb-8"}>
<GroupSetupKeysSection
setupKeys={groupDetails?.setupKeys}
isLoading={isLoading}
/>
<GroupSetupKeysSection setupKeys={groupDetails?.setupKeys} />
</TabsContent>
</Tabs>
);
+1 -1
View File
@@ -1,6 +1,7 @@
"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";
@@ -16,7 +17,6 @@ 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"),
+66 -39
View File
@@ -26,10 +26,10 @@ import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import TextWithTooltip from "@components/ui/TextWithTooltip";
import useRedirect from "@hooks/useRedirect";
import useFetchApi from "@utils/api";
import { cn } from "@utils/helpers";
import dayjs from "dayjs";
import { isEmpty, trim } from "lodash";
import {
ArrowRightIcon,
Barcode,
CalendarDays,
Cpu,
@@ -40,7 +40,6 @@ import {
MonitorSmartphoneIcon,
NetworkIcon,
PencilIcon,
RadioTowerIcon,
TimerResetIcon,
} from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
@@ -61,13 +60,11 @@ import type { Peer } from "@/interfaces/Peer";
import PageContainer from "@/layouts/PageContainer";
import useGroupHelper from "@/modules/groups/useGroupHelper";
import { AccessiblePeersSection } from "@/modules/peer/AccessiblePeersSection";
import { PeerExpirationToggle } from "@/modules/peer/PeerExpirationToggle";
import { PeerNetworkRoutesSection } from "@/modules/peer/PeerNetworkRoutesSection";
import { PeerRemoteJobsSection } from "@/modules/peer/PeerRemoteJobsSection";
import { PeerSSHToggle } from "@/modules/peer/PeerSSHToggle";
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
import Link from "next/link";
import { PeerExpirationSettings } from "@/modules/peer/PeerExpirationSettings";
export default function PeerPage() {
const queryParameter = useSearchParams();
@@ -81,6 +78,12 @@ export default function PeerPage() {
useRedirect("/peers", false, !peerId || isRestricted);
const peerKey = useMemo(() => {
let id = peer?.id ?? "";
let expiration = peer?.login_expiration_enabled ? "1" : "0";
return `${id}-${expiration}`;
}, [peer]);
if (isRestricted) {
return (
<PageContainer>
@@ -101,7 +104,7 @@ export default function PeerPage() {
return peer && !isLoading ? (
<PeerProvider peer={peer} key={peerId} isPeerDetailPage={true}>
<PeerOverview key={peer?.id} />
<PeerOverview key={peerKey} />
</PeerProvider>
) : (
<FullScreenLoading />
@@ -137,9 +140,15 @@ const PeerGeneralInformation = () => {
const { peer, user, peerGroups, update } = usePeer();
const [name, setName] = useState(peer.name);
const [showEditNameModal, setShowEditNameModal] = useState(false);
const [loginExpiration, setLoginExpiration] = useState(
peer.login_expiration_enabled,
);
const [inactivityExpiration, setInactivityExpiration] = useState(
peer.inactivity_expiration_enabled,
);
const [selectedGroups, setSelectedGroups, { getAllGroupCalls }] =
useGroupHelper({
initial: peerGroups?.filter((g) => g?.name !== "All"),
initial: peerGroups,
peer,
});
@@ -148,6 +157,8 @@ const PeerGeneralInformation = () => {
*/
const { hasChanges, updateRef: updateHasChangedRef } = useHasChanges([
selectedGroups,
loginExpiration,
inactivityExpiration,
]);
const updatePeer = async (newName?: string) => {
@@ -157,6 +168,8 @@ const PeerGeneralInformation = () => {
if (permission.peers.update) {
const updateRequest = update({
name: newName ?? name,
loginExpiration,
inactivityExpiration,
});
batchCall = groupCalls ? [...groupCalls, updateRequest] : [updateRequest];
} else {
@@ -169,7 +182,11 @@ const PeerGeneralInformation = () => {
promise: Promise.all(batchCall).then(() => {
mutate("/peers/" + peer.id);
mutate("/groups");
updateHasChangedRef([selectedGroups]);
updateHasChangedRef([
selectedGroups,
loginExpiration,
inactivityExpiration,
]);
}),
loadingMessage: "Saving the peer...",
});
@@ -220,21 +237,9 @@ const PeerGeneralInformation = () => {
</h1>
<LoginExpiredBadge loginExpired={peer.login_expired} />
</div>
{(user?.id || user?.email) && (
<div className={"flex items-center gap-8"}>
<Paragraph className={"flex items-center"}>
<Link
href={`/team/user?id=${user?.id}`}
className={
"hover:text-nb-gray-200 transition-all flex items-center gap-1"
}
>
{user?.email || user?.id}
<ArrowRightIcon size={14} />
</Link>
</Paragraph>
</div>
)}
<div className={"flex items-center gap-8"}>
<Paragraph className={"flex items-center"}>{user?.email}</Paragraph>
</div>
</div>
<div className={"flex gap-4"}>
<Button
@@ -265,7 +270,41 @@ const PeerGeneralInformation = () => {
<PeerInformationCard peer={peer} />
<div className={"flex flex-col gap-6 lg:w-1/2 transition-all"}>
<PeerExpirationSettings />
<div>
<PeerExpirationToggle
peer={peer}
value={loginExpiration}
icon={<TimerResetIcon size={16} />}
onChange={(state) => {
setLoginExpiration(state);
!state && setInactivityExpiration(false);
}}
/>
{permission.peers.update && !!peer?.user_id && (
<div
className={cn(
"border border-nb-gray-900 border-t-0 rounded-b-md bg-nb-gray-940 px-[1.28rem] pt-3 pb-5 flex flex-col gap-4 mx-[0.25rem]",
!loginExpiration
? "opacity-50 pointer-events-none"
: "bg-nb-gray-930/80",
)}
>
<PeerExpirationToggle
peer={peer}
variant={"blank"}
value={inactivityExpiration}
onChange={setInactivityExpiration}
title={"Require login after disconnect"}
description={
"Enable to require authentication after users disconnect from management for 10 minutes."
}
className={
!loginExpiration ? "opacity-40 pointer-events-none" : ""
}
/>
</div>
)}
</div>
<PeerSSHToggle />
@@ -329,13 +368,6 @@ const PeerOverviewTabs = () => {
Accessible Peers
</TabsTrigger>
)}
{peer?.id && permission.peers.delete && (
<TabsTrigger value={"peer-job"}>
<RadioTowerIcon size={16} />
Remote Jobs
</TabsTrigger>
)}
</TabsList>
{permission.routes.read && (
@@ -349,11 +381,6 @@ const PeerOverviewTabs = () => {
<AccessiblePeersSection peerID={peer.id} />
</TabsContent>
)}
{peer.id && permission.peers.delete && (
<TabsContent value={"peer-job"} className={"pb-8"}>
<PeerRemoteJobsSection peerID={peer.id} />
</TabsContent>
)}
</Tabs>
);
};
@@ -541,9 +568,9 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
peer.connected
? "just now"
: dayjs(peer.last_seen).format("D MMMM, YYYY [at] h:mm A") +
" (" +
dayjs().to(peer.last_seen) +
")"
" (" +
dayjs().to(peer.last_seen) +
")"
}
/>
-11
View File
@@ -4,7 +4,6 @@ import { RestrictedAccess } from "@components/ui/RestrictedAccess";
import { VerticalTabs } from "@components/VerticalTabs";
import {
AlertOctagonIcon,
FingerprintIcon,
FolderGit2Icon,
LockIcon,
MonitorSmartphoneIcon,
@@ -20,7 +19,6 @@ 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 IdentityProvidersTab from "@/modules/settings/IdentityProvidersTab";
import NetworkSettingsTab from "@/modules/settings/NetworkSettingsTab";
import PermissionsTab from "@/modules/settings/PermissionsTab";
import GroupsSettings from "@/modules/settings/GroupsSettings";
@@ -55,13 +53,6 @@ export default function NetBirdSettings() {
<ShieldIcon size={14} />
Authentication
</VerticalTabs.Trigger>
{account?.settings?.embedded_idp_enabled &&
permission.identity_providers.read && (
<VerticalTabs.Trigger value="identity-providers">
<FingerprintIcon size={14} />
Identity Providers
</VerticalTabs.Trigger>
)}
<VerticalTabs.Trigger value="groups">
<FolderGit2Icon size={14} />
Groups
@@ -89,8 +80,6 @@ export default function NetBirdSettings() {
>
<div className={"border-l border-nb-gray-930 w-full"}>
{account && <AuthenticationTab account={account} />}
{account?.settings?.embedded_idp_enabled &&
permission.identity_providers.read && <IdentityProvidersTab />}
{account && <PermissionsTab account={account} />}
{account && <GroupsSettings account={account} />}
{account && <NetworkSettingsTab account={account} />}
+37 -85
View File
@@ -9,7 +9,6 @@ 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";
@@ -17,15 +16,7 @@ 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,
KeyRoundIcon,
Mail,
MonitorSmartphoneIcon,
User2,
} from "lucide-react";
import { Ban, GalleryHorizontalEnd, History, Mail, User2 } from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import React, { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
@@ -42,7 +33,6 @@ 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() {
@@ -90,7 +80,6 @@ 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;
@@ -102,6 +91,7 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
});
const [role, setRole] = useState(user.role || Role.User);
const { hasChanges, updateRef: updateChangesRef } = useHasChanges([
role,
selectedGroups,
@@ -124,24 +114,13 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
`/${user.id}`,
)
.then(() => {
mutate(`/users?service_user=${isServiceUser}`);
mutate(`/users?service_user=${user.is_service_user}`);
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"}>
@@ -153,7 +132,7 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
icon={<TeamIcon size={13} />}
/>
{isServiceUser ? (
{user.is_service_user ? (
<Breadcrumbs.Item
href={"/team/service-users"}
label={"Service Users"}
@@ -179,7 +158,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={
isServiceUser
user.is_service_user
? {
color: "white",
}
@@ -192,13 +171,13 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
}
}
>
{isServiceUser ? (
{user.is_service_user ? (
<IconSettings2 size={16} />
) : (
user?.name?.charAt(0) || user?.id?.charAt(0)
)}
</div>
<h1 className={"flex items-center gap-3"} title={user?.id}>
<h1 className={"flex items-center gap-3"}>
{user.name || user.id}
</h1>
</div>
@@ -209,7 +188,7 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
variant={"default"}
className={"w-full"}
onClick={() => {
isServiceUser
user.is_service_user
? router.push("/team/service-users")
: router.push("/team/users");
}}
@@ -233,7 +212,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 "}>
{!isServiceUser && isOwnerOrAdmin && (
{!user.is_service_user && isOwnerOrAdmin && (
<div>
<Label>Auto-assigned groups</Label>
<HelpText>
@@ -259,7 +238,7 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
<UserRoleSelector
value={role}
onChange={setRole}
hideOwner={isServiceUser}
hideOwner={user.is_service_user}
currentUser={user}
disabled={isLoggedInUser || !permission.users.update}
/>
@@ -269,65 +248,38 @@ function UserOverview({ user, initialGroups }: Readonly<Props>) {
</div>
</div>
{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"}>
{(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"}>
<div>
<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>
<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>
<AccessTokensTable user={user} />
</div>
<AccessTokensTable user={user} />
</div>
</TabsContent>
)}
</Tabs>
</div>
</>
)}
</PageContainer>
);
}
Loaded 20 of 217 files, more files were not shown because too many files have changed in this diff. Show more