mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
40
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da4e7d4fbf | ||
|
|
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 |
No files matched your search
@@ -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/__
|
||||
@@ -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 âś…"
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
[
|
||||
{
|
||||
"tag": "New",
|
||||
"text": "Custom DNS Zones for Private Network Resolution",
|
||||
"link": "https://netbird.io/knowledge-hub/custom-dns-zones",
|
||||
"linkText": "Read Release Article",
|
||||
"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,
|
||||
|
||||
Generated
+83
-107
@@ -59,8 +59,8 @@
|
||||
"ip-cidr": "^3.1.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lodash": "^4.17.23",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next": "^16.1.6",
|
||||
"lucide-react": "^0.566.0",
|
||||
"next": "16.1.7",
|
||||
"next-themes": "^0.2.1",
|
||||
"punycode": "^2.3.1",
|
||||
"react": "^19.2.4",
|
||||
@@ -1213,9 +1213,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz",
|
||||
"integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==",
|
||||
"version": "16.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.7.tgz",
|
||||
"integrity": "sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
@@ -1229,9 +1229,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz",
|
||||
"integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==",
|
||||
"version": "16.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.7.tgz",
|
||||
"integrity": "sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1245,9 +1245,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz",
|
||||
"integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==",
|
||||
"version": "16.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.7.tgz",
|
||||
"integrity": "sha512-zcnVaaZulS1WL0Ss38R5Q6D2gz7MtBu8GZLPfK+73D/hp4GFMrC2sudLky1QibfV7h6RJBJs/gOFvYP0X7UVlQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1261,9 +1261,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz",
|
||||
"integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==",
|
||||
"version": "16.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.7.tgz",
|
||||
"integrity": "sha512-2ant89Lux/Q3VyC8vNVg7uBaFVP9SwoK2jJOOR0L8TQnX8CAYnh4uctAScy2Hwj2dgjVHqHLORQZJ2wH6VxhSQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1277,9 +1277,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz",
|
||||
"integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==",
|
||||
"version": "16.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.7.tgz",
|
||||
"integrity": "sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1293,9 +1293,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz",
|
||||
"integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==",
|
||||
"version": "16.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.7.tgz",
|
||||
"integrity": "sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1309,9 +1309,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz",
|
||||
"integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==",
|
||||
"version": "16.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.7.tgz",
|
||||
"integrity": "sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1325,9 +1325,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz",
|
||||
"integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==",
|
||||
"version": "16.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.7.tgz",
|
||||
"integrity": "sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1341,9 +1341,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz",
|
||||
"integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==",
|
||||
"version": "16.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.7.tgz",
|
||||
"integrity": "sha512-mwgtg8CNZGYm06LeEd+bNnOUfwOyNem/rOiP14Lsz+AnUY92Zq/LXwtebtUiaeVkhbroRCQ0c8GlR4UT1U+0yg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3244,32 +3244,6 @@
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
@@ -3665,9 +3639,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
@@ -3998,10 +3972,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.9.19",
|
||||
@@ -4025,13 +4002,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
@@ -4320,12 +4299,6 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@@ -5768,9 +5741,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
@@ -6889,9 +6862,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
@@ -6923,9 +6896,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.562.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz",
|
||||
"integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==",
|
||||
"version": "0.566.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.566.0.tgz",
|
||||
"integrity": "sha512-b18qC/JAh1X9rVKlF5EtSIyumdIYuh78b0JShynZnHbcaWR4AW4oZyi8Ms/aQYVSnLPlAnMhug2hSr19BgVZAw==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
@@ -6973,15 +6946,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"license": "ISC",
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
@@ -7067,14 +7043,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "16.1.6",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz",
|
||||
"integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==",
|
||||
"version": "16.1.7",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-16.1.7.tgz",
|
||||
"integrity": "sha512-WM0L7WrSvKwoLegLYr6V+mz+RIofqQgVAfHhMp9a88ms0cFX8iX9ew+snpWlSBwpkURJOUdvCEt3uLl3NNzvWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "16.1.6",
|
||||
"@next/env": "16.1.7",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
"postcss": "8.4.31",
|
||||
"styled-jsx": "5.1.6"
|
||||
@@ -7086,14 +7062,14 @@
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "16.1.6",
|
||||
"@next/swc-darwin-x64": "16.1.6",
|
||||
"@next/swc-linux-arm64-gnu": "16.1.6",
|
||||
"@next/swc-linux-arm64-musl": "16.1.6",
|
||||
"@next/swc-linux-x64-gnu": "16.1.6",
|
||||
"@next/swc-linux-x64-musl": "16.1.6",
|
||||
"@next/swc-win32-arm64-msvc": "16.1.6",
|
||||
"@next/swc-win32-x64-msvc": "16.1.6",
|
||||
"@next/swc-darwin-arm64": "16.1.7",
|
||||
"@next/swc-darwin-x64": "16.1.7",
|
||||
"@next/swc-linux-arm64-gnu": "16.1.7",
|
||||
"@next/swc-linux-arm64-musl": "16.1.7",
|
||||
"@next/swc-linux-x64-gnu": "16.1.7",
|
||||
"@next/swc-linux-x64-musl": "16.1.7",
|
||||
"@next/swc-win32-arm64-msvc": "16.1.7",
|
||||
"@next/swc-win32-x64-msvc": "16.1.7",
|
||||
"sharp": "^0.34.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -7412,9 +7388,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -8777,9 +8753,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
|
||||
+5
-2
@@ -67,8 +67,8 @@
|
||||
"ip-cidr": "^3.1.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lodash": "^4.17.23",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next": "^16.1.6",
|
||||
"lucide-react": "^0.566.0",
|
||||
"next": "16.1.7",
|
||||
"next-themes": "^0.2.1",
|
||||
"punycode": "^2.3.1",
|
||||
"react": "^19.2.4",
|
||||
@@ -89,6 +89,9 @@
|
||||
"timescape": "^0.7.1",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"overrides": {
|
||||
"minimatch": ">=10.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^9.5.1",
|
||||
"@types/chroma-js": "^3.1.1",
|
||||
|
||||
@@ -6,5 +6,5 @@ import React from "react";
|
||||
|
||||
export default function Redirect() {
|
||||
useRedirect("/events/audit");
|
||||
return <FullScreenLoading height={"auto"} />;
|
||||
return <FullScreenLoading fullScreen={false} />;
|
||||
}
|
||||
@@ -11,5 +11,5 @@ export default function DNS() {
|
||||
router.push("/dns/nameservers");
|
||||
}, [router]);
|
||||
|
||||
return <FullScreenLoading height={"auto"} />;
|
||||
return <FullScreenLoading fullScreen={false} />;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { ExternalLinkIcon } from "lucide-react";
|
||||
import ReverseProxyIcon from "@/assets/icons/ReverseProxyIcon";
|
||||
import React, { useMemo } from "react";
|
||||
import ActivityIcon from "@/assets/icons/ActivityIcon";
|
||||
import PeersProvider from "@/contexts/PeersProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import ServerPaginationProvider from "@/contexts/ServerPaginationProvider";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
@@ -25,6 +26,8 @@ export default function ProxyEventsPage() {
|
||||
() => ({
|
||||
start_date: dayjs().subtract(7, "day").startOf("day").toISOString(),
|
||||
end_date: dayjs().endOf("day").toISOString(),
|
||||
sort_by: "timestamp",
|
||||
sort_order: "desc",
|
||||
}),
|
||||
[],
|
||||
);
|
||||
@@ -70,7 +73,9 @@ export default function ProxyEventsPage() {
|
||||
defaultPageSize={10}
|
||||
defaultFilters={defaultFilters}
|
||||
>
|
||||
<ReverseProxyEventsTable headingTarget={portalTarget} />
|
||||
<PeersProvider>
|
||||
<ReverseProxyEventsTable headingTarget={portalTarget} />
|
||||
</PeersProvider>
|
||||
</ServerPaginationProvider>
|
||||
</RestrictedAccess>
|
||||
</PageContainer>
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from "@components/DropdownMenu";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import useRedirect from "@hooks/useRedirect";
|
||||
import useFetchApi from "@utils/api";
|
||||
import { cn, singularize } from "@utils/helpers";
|
||||
@@ -35,6 +34,7 @@ import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { Network, NetworkResource, NetworkRouter } from "@/interfaces/Network";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
import { NetworkInformationSquare } from "@/modules/networks/misc/NetworkInformationSquare";
|
||||
import { NetworkAccessControlProvider } from "@/modules/networks/NetworkAccessControlProvider";
|
||||
import {
|
||||
NetworkProvider,
|
||||
useNetworksContext,
|
||||
@@ -49,6 +49,7 @@ import ReverseProxiesProvider, {
|
||||
flattenReverseProxies,
|
||||
useReverseProxies,
|
||||
} from "@/contexts/ReverseProxiesProvider";
|
||||
import { SkeletonNetwork } from "@components/skeletons/SkeletonNetwork";
|
||||
|
||||
export default function NetworkDetailPage() {
|
||||
const queryParameter = useSearchParams();
|
||||
@@ -65,7 +66,7 @@ export default function NetworkDetailPage() {
|
||||
<NetworkOverview network={network} />
|
||||
</ReverseProxiesProvider>
|
||||
) : (
|
||||
<FullScreenLoading />
|
||||
<SkeletonNetwork />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -96,103 +97,103 @@ function NetworkOverview({ network }: Readonly<{ network: Network }>) {
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<NetworkProvider network={network}>
|
||||
<div className={"p-default py-6"}>
|
||||
<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={"Networks"}
|
||||
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={"w-full lg:w-1/2 flex justify-between items-center"}
|
||||
>
|
||||
<div className={"flex justify-between max-w-6xl"}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center w-full",
|
||||
!network.description && "gap-2",
|
||||
)}
|
||||
className={"w-full lg:w-1/2 flex justify-between items-center"}
|
||||
>
|
||||
<NetworkInformationSquare
|
||||
name={network.name}
|
||||
active={isActive}
|
||||
size={"lg"}
|
||||
description={network.description}
|
||||
/>
|
||||
</div>
|
||||
<NetworkProvider network={network}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center w-full",
|
||||
!network.description && "gap-2",
|
||||
)}
|
||||
>
|
||||
<NetworkInformationSquare
|
||||
name={network.name}
|
||||
active={isActive}
|
||||
size={"lg"}
|
||||
description={network.description}
|
||||
/>
|
||||
</div>
|
||||
<NetworkActions />
|
||||
</NetworkProvider>
|
||||
</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("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("Routing Peers", 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("Services", services.length)}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<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("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"
|
||||
}
|
||||
<TabsContent value={"resources"} className={"pb-8"}>
|
||||
<ResourcesTabContent
|
||||
data={resources}
|
||||
isLoading={isResourcesLoading}
|
||||
/>
|
||||
{singularize("Routing Peers", 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"
|
||||
}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={"routing-peers"} className={"pb-8"}>
|
||||
<NetworkRoutingPeersTabContent
|
||||
routers={routers}
|
||||
isLoading={isRoutersLoading}
|
||||
/>
|
||||
{singularize("Services", services.length)}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</TabsContent>
|
||||
|
||||
<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>
|
||||
<TabsContent value={"services"} className={"pb-8"}>
|
||||
<ReverseProxyFlatTargetsTabContent
|
||||
targets={services}
|
||||
isLoading={isServicesLoading}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</NetworkProvider>
|
||||
</NetworkAccessControlProvider>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
+103
-119
@@ -2,7 +2,6 @@
|
||||
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import Button from "@components/Button";
|
||||
import { Callout } from "@components/Callout";
|
||||
import Card from "@components/Card";
|
||||
import HelpText from "@components/HelpText";
|
||||
import { Input } from "@components/Input";
|
||||
@@ -72,6 +71,7 @@ import ReverseProxiesProvider, {
|
||||
useReverseProxies,
|
||||
} from "@/contexts/ReverseProxiesProvider";
|
||||
import { ReverseProxyFlatTargetsTabContent } from "@/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetsTabContent";
|
||||
import { PeerEditIPModal } from "@/modules/peer/PeerEditIPModal";
|
||||
import { PeerSSHToggle } from "@/modules/peer/PeerSSHToggle";
|
||||
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
|
||||
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
|
||||
@@ -469,31 +469,55 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
const { update } = usePeer();
|
||||
const { mutate } = useSWRConfig();
|
||||
const [showEditIPModal, setShowEditIPModal] = useState(false);
|
||||
const [showEditIPv6Modal, setShowEditIPv6Modal] = useState(false);
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const countryText = useMemo(() => {
|
||||
return getRegionByPeer(peer);
|
||||
}, [getRegionByPeer, peer]);
|
||||
|
||||
const handleSaveIP = (newIP: string) => {
|
||||
notify({
|
||||
title: peer.name,
|
||||
description: "NetBird Peer IP was successfully updated",
|
||||
promise: update({ ip: newIP }).then(() => {
|
||||
mutate("/peers/" + peer.id);
|
||||
setShowEditIPModal(false);
|
||||
}),
|
||||
loadingMessage: "Updating peer IP...",
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveIPv6 = (newIPv6: string) => {
|
||||
notify({
|
||||
title: peer.name,
|
||||
description: "NetBird Peer IPv6 was successfully updated",
|
||||
promise: update({ ipv6: newIPv6 }).then(() => {
|
||||
mutate("/peers/" + peer.id);
|
||||
setShowEditIPv6Modal(false);
|
||||
}),
|
||||
loadingMessage: "Updating peer IPv6...",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal open={showEditIPModal} onOpenChange={setShowEditIPModal}>
|
||||
<EditIPModal
|
||||
onSuccess={(newIP) => {
|
||||
notify({
|
||||
title: peer.name,
|
||||
description: "Peer IP was successfully updated",
|
||||
promise: update({ ip: newIP }).then(() => {
|
||||
mutate("/peers/" + peer.id);
|
||||
setShowEditIPModal(false);
|
||||
}),
|
||||
loadingMessage: "Updating peer IP...",
|
||||
});
|
||||
}}
|
||||
peer={peer}
|
||||
key={showEditIPModal ? 1 : 0}
|
||||
/>
|
||||
</Modal>
|
||||
<PeerEditIPModal
|
||||
version="v4"
|
||||
currentIP={peer.ip}
|
||||
open={showEditIPModal}
|
||||
onOpenChange={setShowEditIPModal}
|
||||
onSave={handleSaveIP}
|
||||
key={showEditIPModal ? "v4-open" : "v4-closed"}
|
||||
/>
|
||||
<PeerEditIPModal
|
||||
version="v6"
|
||||
currentIP={peer.ipv6 || ""}
|
||||
open={showEditIPv6Modal}
|
||||
onOpenChange={setShowEditIPv6Modal}
|
||||
onSave={handleSaveIPv6}
|
||||
key={showEditIPv6Modal ? "v6-open" : "v6-closed"}
|
||||
/>
|
||||
<Card className={"w-full xl:w-1/2"}>
|
||||
<Card.List>
|
||||
<Card.ListItem
|
||||
@@ -502,35 +526,48 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
copyText={"NetBird IP Address"}
|
||||
label={
|
||||
<>
|
||||
<MapPin size={16} />
|
||||
<MapPin size={16} className={"shrink-0"} />
|
||||
NetBird IP Address
|
||||
</>
|
||||
}
|
||||
valueToCopy={peer.ip}
|
||||
value={
|
||||
<div className="flex items-center gap-2 justify-between w-full">
|
||||
<span>{peer.ip}</span>
|
||||
{permission.peers.update && (
|
||||
<button
|
||||
className="flex w-7 h-7 items-center justify-center gap-2 text-nb-gray-400 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowEditIPModal(true);
|
||||
}}
|
||||
>
|
||||
<PencilIcon size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<EditableValue
|
||||
value={peer.ip}
|
||||
canEdit={permission.peers.update}
|
||||
onEdit={() => setShowEditIPModal(true)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{peer.ipv6 && (
|
||||
<Card.ListItem
|
||||
copy
|
||||
tooltip={false}
|
||||
copyText={"NetBird IPv6 Address"}
|
||||
label={
|
||||
<>
|
||||
<MapPin size={16} className={"shrink-0"} />
|
||||
NetBird IPv6 Address
|
||||
</>
|
||||
}
|
||||
valueToCopy={peer.ipv6}
|
||||
value={
|
||||
<EditableValue
|
||||
value={peer.ipv6}
|
||||
canEdit={permission.peers.update}
|
||||
onEdit={() => setShowEditIPv6Modal(true)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"Public IP Address"}
|
||||
label={
|
||||
<>
|
||||
<NetworkIcon size={16} />
|
||||
<NetworkIcon size={16} className={"shrink-0"} />
|
||||
Public IP Address
|
||||
</>
|
||||
}
|
||||
@@ -542,7 +579,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
copyText={"DNS label"}
|
||||
label={
|
||||
<>
|
||||
<Globe size={16} />
|
||||
<Globe size={16} className={"shrink-0"} />
|
||||
Domain Name
|
||||
</>
|
||||
}
|
||||
@@ -560,7 +597,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
copyText={"Hostname"}
|
||||
label={
|
||||
<>
|
||||
<MonitorSmartphoneIcon size={16} />
|
||||
<MonitorSmartphoneIcon size={16} className={"shrink-0"} />
|
||||
Hostname
|
||||
</>
|
||||
}
|
||||
@@ -570,7 +607,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<FlagIcon size={16} />
|
||||
<FlagIcon size={16} className={"shrink-0"} />
|
||||
Region
|
||||
</>
|
||||
}
|
||||
@@ -600,7 +637,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<Cpu size={16} />
|
||||
<Cpu size={16} className={"shrink-0"} />
|
||||
Operating System
|
||||
</>
|
||||
}
|
||||
@@ -611,7 +648,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<Barcode size={16} />
|
||||
<Barcode size={16} className={"shrink-0"} />
|
||||
Serial Number
|
||||
</>
|
||||
}
|
||||
@@ -623,7 +660,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<CalendarDays size={16} />
|
||||
<CalendarDays size={16} className={"shrink-0"} />
|
||||
Registered on
|
||||
</>
|
||||
}
|
||||
@@ -639,7 +676,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<History size={16} />
|
||||
<History size={16} className={"shrink-0"} />
|
||||
Last seen
|
||||
</>
|
||||
}
|
||||
@@ -656,7 +693,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<NetBirdIcon size={16} />
|
||||
<NetBirdIcon size={16} className={"shrink-0"} />
|
||||
Agent Version
|
||||
</>
|
||||
}
|
||||
@@ -667,7 +704,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<NetBirdIcon size={16} />
|
||||
<NetBirdIcon size={16} className={"shrink-0"} />
|
||||
UI Version
|
||||
</>
|
||||
}
|
||||
@@ -765,82 +802,29 @@ function EditNameModal({ onSuccess, peer, initialName }: Readonly<ModalProps>) {
|
||||
);
|
||||
}
|
||||
|
||||
interface EditIPModalProps {
|
||||
onSuccess: (ip: string) => void;
|
||||
peer: Peer;
|
||||
}
|
||||
|
||||
function EditIPModal({ onSuccess, peer }: Readonly<EditIPModalProps>) {
|
||||
const [ip, setIP] = useState(peer.ip);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const validateIP = (ipAddress: string) => {
|
||||
const ipRegex =
|
||||
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
return ipRegex.test(ipAddress);
|
||||
};
|
||||
|
||||
const isDisabled = useMemo(() => {
|
||||
if (ip === peer.ip) return true;
|
||||
const trimmedIP = trim(ip);
|
||||
return trimmedIP.length === 0 || !validateIP(ip);
|
||||
}, [ip, peer.ip]);
|
||||
|
||||
React.useEffect(() => {
|
||||
switch (true) {
|
||||
case ip === peer.ip:
|
||||
setError("");
|
||||
break;
|
||||
case !validateIP(ip):
|
||||
setError("Please enter a valid IP, e.g., 100.64.0.15");
|
||||
break;
|
||||
default:
|
||||
setError("");
|
||||
break;
|
||||
}
|
||||
}, [ip, peer.ip]);
|
||||
|
||||
function EditableValue({
|
||||
value,
|
||||
canEdit,
|
||||
onEdit,
|
||||
}: {
|
||||
value: string;
|
||||
canEdit: boolean;
|
||||
onEdit: () => void;
|
||||
}) {
|
||||
return (
|
||||
<ModalContent maxWidthClass={"max-w-md"}>
|
||||
<form>
|
||||
<ModalHeader
|
||||
title={"Edit Peer IP Address"}
|
||||
description={"Update the NetBird IP address for this peer."}
|
||||
color={"blue"}
|
||||
/>
|
||||
|
||||
<div className={"p-default flex flex-col gap-4"}>
|
||||
<div>
|
||||
<Input
|
||||
placeholder={"e.g., 100.64.0.15"}
|
||||
value={ip}
|
||||
onChange={(e) => setIP(e.target.value)}
|
||||
error={error}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Callout>Changes take effect when the peer reconnects.</Callout>
|
||||
</div>
|
||||
|
||||
<ModalFooter className={"items-center"} separator={false}>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"} className={"w-full"}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"w-full"}
|
||||
onClick={() => onSuccess(ip)}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</ModalContent>
|
||||
<div className="flex items-center gap-2 justify-between w-full">
|
||||
<span>{value}</span>
|
||||
{canEdit && (
|
||||
<button
|
||||
className="flex w-7 h-7 items-center justify-center gap-2 text-nb-gray-400 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit();
|
||||
}}
|
||||
>
|
||||
<PencilIcon size={14} />
|
||||
</button>
|
||||
)}
|
||||
</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 stepper-bg-variant"
|
||||
}
|
||||
>
|
||||
<SetupModalContent header={false} footer={false} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
export default function PeersIndex() {
|
||||
redirect("/peers/users");
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,115 @@
|
||||
"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 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 { 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={"Peers"}
|
||||
icon={<PeerIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/peers/servers"}
|
||||
label={"Servers"}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>Servers</h1>
|
||||
<Paragraph>
|
||||
Servers, VMs, autonomous agents and other unattended machines with no
|
||||
user behind them, typically enrolled with a{" "}
|
||||
<InlineLink href={"/settings?tab=setup-keys"}>setup key.</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<PeersTable
|
||||
isLoading={isLoading}
|
||||
peers={peersWithUser}
|
||||
headingTarget={portalTarget}
|
||||
kind={"servers"}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ServersBlockedView() {
|
||||
return (
|
||||
<div className={"flex items-center justify-center flex-col"}>
|
||||
<div className={"p-default py-6 max-w-3xl text-center"}>
|
||||
<h1>Add new server to your network</h1>
|
||||
<Paragraph className={"inline"}>
|
||||
To get started, install NetBird on the server and enroll it using a
|
||||
setup key. 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 stepper-bg-variant"
|
||||
}
|
||||
>
|
||||
<SetupModalContent header={false} footer={false} isUserDevice={false} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,122 @@
|
||||
"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 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 { 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={"Peers"}
|
||||
icon={<PeerIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/peers/users"}
|
||||
label={"User Devices"}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>User Devices</h1>
|
||||
<Paragraph>
|
||||
Laptops, phones and other personal devices with a user behind them,
|
||||
typically added when the user signs in with SSO.{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/add-machines-to-your-network"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Learn more
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<PeersTable
|
||||
isLoading={isLoading}
|
||||
peers={peersWithUser}
|
||||
headingTarget={portalTarget}
|
||||
kind={"users"}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function UserDevicesBlockedView() {
|
||||
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 stepper-bg-variant"
|
||||
}
|
||||
>
|
||||
<SetupModalContent header={false} footer={false} isUserDevice />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 React, { lazy, Suspense } from "react";
|
||||
import ReverseProxyIcon from "@/assets/icons/ReverseProxyIcon";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
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 { permission } = usePermissions();
|
||||
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/reverse-proxy/services"}
|
||||
label={"Reverse Proxy"}
|
||||
icon={<ReverseProxyIcon size={16} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/reverse-proxy/clusters"}
|
||||
label={"Clusters"}
|
||||
active={true}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>Clusters</h1>
|
||||
<Paragraph>
|
||||
Proxy clusters that route inbound traffic to your services. Shared
|
||||
clusters are deployed at the server level; account clusters are
|
||||
self-hosted on your own infrastructure.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Learn more about
|
||||
<InlineLink href={REVERSE_PROXY_CLUSTERS_DOCS_LINK} target={"_blank"}>
|
||||
Clusters
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
in our documentation.
|
||||
</Paragraph>
|
||||
</div>
|
||||
<RestrictedAccess
|
||||
page={"Clusters"}
|
||||
hasAccess={permission?.services?.read}
|
||||
>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<ClustersTable headingTarget={portalTarget} />
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -11,5 +11,5 @@ export default function ReverseProxyRedirectPage() {
|
||||
router.replace("/reverse-proxy/services");
|
||||
}, [router]);
|
||||
|
||||
return <FullScreenLoading height={"auto"} />;
|
||||
return <FullScreenLoading fullScreen={false} />;
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
import PageContainer from "@/layouts/PageContainer";
|
||||
@@ -23,6 +24,7 @@ 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 SetupKeysTab from "@/modules/settings/SetupKeysTab";
|
||||
import GroupsSettings from "@/modules/settings/GroupsSettings";
|
||||
|
||||
export default function NetBirdSettings() {
|
||||
@@ -55,6 +57,12 @@ export default function NetBirdSettings() {
|
||||
<ShieldIcon size={14} />
|
||||
Authentication
|
||||
</VerticalTabs.Trigger>
|
||||
{permission.setup_keys.read && (
|
||||
<VerticalTabs.Trigger value="setup-keys">
|
||||
<SetupKeysIcon size={14} />
|
||||
Setup Keys
|
||||
</VerticalTabs.Trigger>
|
||||
)}
|
||||
{account?.settings?.embedded_idp_enabled &&
|
||||
permission?.identity_providers?.read && (
|
||||
<VerticalTabs.Trigger value="identity-providers">
|
||||
@@ -89,6 +97,7 @@ export default function NetBirdSettings() {
|
||||
>
|
||||
<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} />}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
Loaded 20 of 189 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user