Compare commits

..
Author SHA1 Message Date
Eduard Gert d91fc33bfe Add support for multiple targets for the announcement banner 2026-03-06 10:42:04 +01:00
127 changed files with 2022 additions and 6868 deletions

No files matched your search

-12
View File
@@ -1,12 +0,0 @@
## 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/__
-105
View File
@@ -1,105 +0,0 @@
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 âś…"
+1 -1
View File
@@ -7,6 +7,6 @@
"variant": "important",
"isExternal": true,
"closeable": true,
"isCloudOnly": false
"targets": ["selfhosted"]
}
]
+104 -80
View File
@@ -59,8 +59,8 @@
"ip-cidr": "^3.1.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.23",
"lucide-react": "^0.566.0",
"next": "16.1.7",
"lucide-react": "^0.562.0",
"next": "^16.1.6",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
"react": "^19.2.4",
@@ -1213,9 +1213,9 @@
}
},
"node_modules/@next/env": {
"version": "16.1.7",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.7.tgz",
"integrity": "sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg==",
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz",
"integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -1229,9 +1229,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "16.1.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.7.tgz",
"integrity": "sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg==",
"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==",
"cpu": [
"arm64"
],
@@ -1245,9 +1245,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"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==",
"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==",
"cpu": [
"x64"
],
@@ -1261,9 +1261,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"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==",
"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==",
"cpu": [
"arm64"
],
@@ -1277,9 +1277,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"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==",
"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==",
"cpu": [
"arm64"
],
@@ -1293,9 +1293,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"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==",
"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==",
"cpu": [
"x64"
],
@@ -1309,9 +1309,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"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==",
"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==",
"cpu": [
"x64"
],
@@ -1325,9 +1325,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"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==",
"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==",
"cpu": [
"arm64"
],
@@ -1341,9 +1341,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"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==",
"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==",
"cpu": [
"x64"
],
@@ -3244,6 +3244,32 @@
"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.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.2"
},
"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",
@@ -3972,13 +3998,10 @@
}
},
"node_modules/balanced-match": {
"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"
}
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.19",
@@ -4002,15 +4025,13 @@
}
},
"node_modules/brace-expansion": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/braces": {
@@ -4299,6 +4320,12 @@
"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",
@@ -5741,9 +5768,9 @@
}
},
"node_modules/flatted": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"license": "ISC"
},
"node_modules/for-each": {
@@ -6862,9 +6889,9 @@
}
},
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"license": "MIT"
},
"node_modules/lodash.merge": {
@@ -6896,9 +6923,9 @@
}
},
"node_modules/lucide-react": {
"version": "0.566.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.566.0.tgz",
"integrity": "sha512-b18qC/JAh1X9rVKlF5EtSIyumdIYuh78b0JShynZnHbcaWR4AW4oZyi8Ms/aQYVSnLPlAnMhug2hSr19BgVZAw==",
"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==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -6946,18 +6973,15 @@
}
},
"node_modules/minimatch": {
"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",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^5.0.5"
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
"node": "*"
}
},
"node_modules/minimist": {
@@ -7043,14 +7067,14 @@
"license": "MIT"
},
"node_modules/next": {
"version": "16.1.7",
"resolved": "https://registry.npmjs.org/next/-/next-16.1.7.tgz",
"integrity": "sha512-WM0L7WrSvKwoLegLYr6V+mz+RIofqQgVAfHhMp9a88ms0cFX8iX9ew+snpWlSBwpkURJOUdvCEt3uLl3NNzvWg==",
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz",
"integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==",
"license": "MIT",
"dependencies": {
"@next/env": "16.1.7",
"@next/env": "16.1.6",
"@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.9.19",
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
@@ -7062,14 +7086,14 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
"@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",
"@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",
"sharp": "^0.34.4"
},
"peerDependencies": {
@@ -7388,9 +7412,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"license": "MIT",
"engines": {
"node": ">=8.6"
@@ -8753,9 +8777,9 @@
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
+2 -5
View File
@@ -67,8 +67,8 @@
"ip-cidr": "^3.1.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.23",
"lucide-react": "^0.566.0",
"next": "16.1.7",
"lucide-react": "^0.562.0",
"next": "^16.1.6",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
"react": "^19.2.4",
@@ -89,9 +89,6 @@
"timescape": "^0.7.1",
"typescript": "^5"
},
"overrides": {
"minimatch": ">=10.2.1"
},
"devDependencies": {
"@faker-js/faker": "^9.5.1",
"@types/chroma-js": "^3.1.1",
@@ -25,8 +25,6 @@ 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",
}),
[],
);
+88 -89
View File
@@ -12,6 +12,7 @@ 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";
@@ -34,7 +35,6 @@ 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,7 +49,6 @@ import ReverseProxiesProvider, {
flattenReverseProxies,
useReverseProxies,
} from "@/contexts/ReverseProxiesProvider";
import { SkeletonNetwork } from "@components/skeletons/SkeletonNetwork";
export default function NetworkDetailPage() {
const queryParameter = useSearchParams();
@@ -66,7 +65,7 @@ export default function NetworkDetailPage() {
<NetworkOverview network={network} />
</ReverseProxiesProvider>
) : (
<SkeletonNetwork />
<FullScreenLoading />
);
}
@@ -97,103 +96,103 @@ function NetworkOverview({ network }: Readonly<{ network: Network }>) {
return (
<PageContainer>
<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>
<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={"flex justify-between max-w-6xl"}>
<div
className={"w-full lg:w-1/2 flex justify-between items-center"}
>
<div
className={"w-full lg:w-1/2 flex justify-between items-center"}
className={cn(
"flex items-center w-full",
!network.description && "gap-2",
)}
>
<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 />
<NetworkInformationSquare
name={network.name}
active={isActive}
size={"lg"}
description={network.description}
/>
</div>
</div>
<div className={"flex gap-10 w-full mt-8 max-w-6xl items-start"}>
<NetworkInformationCard network={network} />
<NetworkProvider network={network}>
<NetworkActions />
</NetworkProvider>
</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>
<div className={"flex gap-10 w-full mt-8 max-w-6xl items-start"}>
<NetworkInformationCard network={network} />
</div>
</div>
<TabsContent value={"resources"} className={"pb-8"}>
<ResourcesTabContent
data={resources}
isLoading={isResourcesLoading}
<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>
{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>
<TabsContent value={"routing-peers"} className={"pb-8"}>
<NetworkRoutingPeersTabContent
routers={routers}
isLoading={isRoutersLoading}
/>
</TabsContent>
<TabsContent value={"resources"} className={"pb-8"}>
<ResourcesTabContent
data={resources}
isLoading={isResourcesLoading}
/>
</TabsContent>
<TabsContent value={"services"} className={"pb-8"}>
<ReverseProxyFlatTargetsTabContent
targets={services}
isLoading={isServicesLoading}
/>
</TabsContent>
</Tabs>
</NetworkProvider>
</NetworkAccessControlProvider>
<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>
</PageContainer>
);
}
+119 -103
View File
@@ -2,6 +2,7 @@
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";
@@ -71,7 +72,6 @@ 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,55 +469,31 @@ 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 (
<>
<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"}
/>
<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>
<Card className={"w-full xl:w-1/2"}>
<Card.List>
<Card.ListItem
@@ -526,48 +502,35 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
copyText={"NetBird IP Address"}
label={
<>
<MapPin size={16} className={"shrink-0"} />
<MapPin size={16} />
NetBird IP Address
</>
}
valueToCopy={peer.ip}
value={
<EditableValue
value={peer.ip}
canEdit={permission.peers.update}
onEdit={() => setShowEditIPModal(true)}
/>
<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>
}
/>
{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} className={"shrink-0"} />
<NetworkIcon size={16} />
Public IP Address
</>
}
@@ -579,7 +542,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
copyText={"DNS label"}
label={
<>
<Globe size={16} className={"shrink-0"} />
<Globe size={16} />
Domain Name
</>
}
@@ -597,7 +560,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
copyText={"Hostname"}
label={
<>
<MonitorSmartphoneIcon size={16} className={"shrink-0"} />
<MonitorSmartphoneIcon size={16} />
Hostname
</>
}
@@ -607,7 +570,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
<Card.ListItem
label={
<>
<FlagIcon size={16} className={"shrink-0"} />
<FlagIcon size={16} />
Region
</>
}
@@ -637,7 +600,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
<Card.ListItem
label={
<>
<Cpu size={16} className={"shrink-0"} />
<Cpu size={16} />
Operating System
</>
}
@@ -648,7 +611,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
<Card.ListItem
label={
<>
<Barcode size={16} className={"shrink-0"} />
<Barcode size={16} />
Serial Number
</>
}
@@ -660,7 +623,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
<Card.ListItem
label={
<>
<CalendarDays size={16} className={"shrink-0"} />
<CalendarDays size={16} />
Registered on
</>
}
@@ -676,7 +639,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
<Card.ListItem
label={
<>
<History size={16} className={"shrink-0"} />
<History size={16} />
Last seen
</>
}
@@ -693,7 +656,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
<Card.ListItem
label={
<>
<NetBirdIcon size={16} className={"shrink-0"} />
<NetBirdIcon size={16} />
Agent Version
</>
}
@@ -704,7 +667,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
<Card.ListItem
label={
<>
<NetBirdIcon size={16} className={"shrink-0"} />
<NetBirdIcon size={16} />
UI Version
</>
}
@@ -802,29 +765,82 @@ function EditNameModal({ onSuccess, peer, initialName }: Readonly<ModalProps>) {
);
}
function EditableValue({
value,
canEdit,
onEdit,
}: {
value: string;
canEdit: boolean;
onEdit: () => void;
}) {
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]);
return (
<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>
<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>
);
}
+6 -13
View File
@@ -18,7 +18,7 @@ import {
} from "@utils/version";
export default function SSHPage() {
const { peerId, username, port, ipVersion } = useSSHQueryParams();
const { peerId, username, port } = useSSHQueryParams();
const {
data: peer,
@@ -48,7 +48,6 @@ export default function SSHPage() {
peer={peer}
username={username}
port={port}
ipVersion={ipVersion}
/>
) : (
<LoadingMessage message={"Starting ssh session..."} />
@@ -61,10 +60,9 @@ type Props = {
username: string;
port: string;
peer: Peer;
ipVersion: string | null;
};
function SSHTerminal({ username, port, peer, ipVersion }: Props) {
function SSHTerminal({ username, port, peer }: Props) {
const client = useNetBirdClient();
const connected = useRef(false);
const sshConnectedOnce = useRef(false);
@@ -83,12 +81,9 @@ function SSHTerminal({ username, port, peer, ipVersion }: Props) {
const isClientDisconnected = client.status === NetBirdStatus.DISCONNECTED;
const isClientConnecting = client.status === NetBirdStatus.CONNECTING;
// Use the FQDN when an IP version is specified so the dialer resolves to the correct address family.
const sshHost = ipVersion ? peer.dns_label || peer.ip : peer.ip;
useEffect(() => {
document.title = `${username}@${sshHost} - ${peer.hostname}`;
}, [username, peer, client, sshHost]);
document.title = `${username}@${peer.ip} - ${peer.hostname}`;
}, [username, peer, client]);
const handleReconnect = async () => {
if (!peer?.id) return;
@@ -102,10 +97,9 @@ function SSHTerminal({ username, port, peer, ipVersion }: Props) {
const rules = [`${protocol}/${aclPort}`];
await client?.connectTemporary(peer.id, rules);
await ssh({
hostname: sshHost,
hostname: peer.ip,
port: Number(port),
username,
ipVersion: ipVersion || undefined,
});
} catch (error) {
console.error("Reconnection failed:", error);
@@ -129,10 +123,9 @@ function SSHTerminal({ username, port, peer, ipVersion }: Props) {
const rules = [`${protocol}/${aclPort}`];
await client?.connectTemporary(peer.id, rules);
const res = await ssh({
hostname: sshHost,
hostname: peer.ip,
port: Number(port),
username,
ipVersion: ipVersion || undefined,
});
if (res === SSHStatus.CONNECTED) {
sshConnectedOnce.current = true;
@@ -23,7 +23,6 @@ export const idpIcon = (
zitadel: <ZitadelIcon size={size} />,
authentik: <AuthentikIcon size={size} />,
keycloak: <KeycloakIcon size={size} />,
adfs: <MicrosoftIcon size={size} />,
oidc: <KeyRound size={size} className="text-nb-gray-400" />,
};
+92 -25
View File
@@ -3,36 +3,103 @@ import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
export default function LoadingIcon(props: IconProps) {
return (
<svg
viewBox="0 0 919 669"
fill="none"
width="135"
height="140"
viewBox="0 0 135 140"
xmlns="http://www.w3.org/2000/svg"
fillOpacity="0.8"
{...iconProperties(props)}
>
<style>{`
@keyframes nb-loader-pulse {
0%, 100% { transform: scale(0.85); opacity: 0.35; }
50% { transform: scale(1); opacity: 1; }
}
.nb-loader-bird {
animation: nb-loader-pulse 1.75s ease-in-out infinite;
transform-origin: 50% 50%;
transform-box: fill-box;
}
`}</style>
<g className={"nb-loader-bird"}>
<path
d="M653.25 0C542.297 10.255 487.216 74.132 466.346 106.359L456.959 122.609C456.311 123.963 455.886 124.865 455.886 124.865L455.774 124.685L142.056 668.021H533.256L918.947 0H653.25Z"
fill="#F68330"
<rect y="10" width="15" height="120" rx="6">
<animate
attributeName="height"
begin="0.5s"
dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120"
calcMode="linear"
repeatCount="indefinite"
/>
<path
d="M533.256 668.02L0 102.296C0 102.296 603.149 -59.6172 661.827 445.893L533.256 668.02Z"
fill="#F68330"
<animate
attributeName="y"
begin="0.5s"
dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10"
calcMode="linear"
repeatCount="indefinite"
/>
<path
d="M454.3 127.229L290.658 410.664L533.256 668.02L661.757 445.475C641.46 271.63 556.721 176.843 454.3 127.229Z"
fill="#F35E32"
</rect>
<rect x="30" y="10" width="15" height="120" rx="6">
<animate
attributeName="height"
begin="0.25s"
dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120"
calcMode="linear"
repeatCount="indefinite"
/>
</g>
<animate
attributeName="y"
begin="0.25s"
dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10"
calcMode="linear"
repeatCount="indefinite"
/>
</rect>
<rect x="60" width="15" height="140" rx="6">
<animate
attributeName="height"
begin="0s"
dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120"
calcMode="linear"
repeatCount="indefinite"
/>
<animate
attributeName="y"
begin="0s"
dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10"
calcMode="linear"
repeatCount="indefinite"
/>
</rect>
<rect x="90" y="10" width="15" height="120" rx="6">
<animate
attributeName="height"
begin="0.25s"
dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120"
calcMode="linear"
repeatCount="indefinite"
/>
<animate
attributeName="y"
begin="0.25s"
dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10"
calcMode="linear"
repeatCount="indefinite"
/>
</rect>
<rect x="120" y="10" width="15" height="120" rx="6">
<animate
attributeName="height"
begin="0.5s"
dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120"
calcMode="linear"
repeatCount="indefinite"
/>
<animate
attributeName="y"
begin="0.5s"
dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10"
calcMode="linear"
repeatCount="indefinite"
/>
</rect>
</svg>
);
}
}
+1 -5
View File
@@ -8,12 +8,8 @@ export default function ReverseProxyIcon(props: IconProps) {
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
{...iconProperties(props)}
fill={"currentColor"}
>
<path
fill={"currentColor"}
d="M11.4488 2.1499C11.7903 1.95003 12.2097 1.95003 12.5513 2.1499L16.5018 4.46123L12 7.03523L7.49823 4.46123L11.4488 2.1499ZM6.44447 6.46472L6.44444 10.2784L2.93531 12.3315L7.53662 14.8399L10.8889 12.8787V9.00593L6.44447 6.46472ZM2 14.3992V18.7395C2 19.1477 2.21366 19.5247 2.55984 19.7272L6.44446 22V16.8223L2 14.3992ZM8.66668 22L12 20.0497L15.3333 22V16.7994L12 14.8492L8.66668 16.7993V22ZM17.5556 22L21.4401 19.7272C21.7863 19.5247 22 19.1477 22 18.7395V14.3992L17.5556 16.8223V22ZM21.0647 12.3315L17.5556 10.2784V6.46474L13.1111 9.00593V12.8787L16.4634 14.8399L21.0647 12.3315Z"
/>
<path d="M11.4488 2.1499C11.7903 1.95003 12.2097 1.95003 12.5513 2.1499L16.5018 4.46123L12 7.03523L7.49823 4.46123L11.4488 2.1499ZM6.44447 6.46472L6.44444 10.2784L2.93531 12.3315L7.53662 14.8399L10.8889 12.8787V9.00593L6.44447 6.46472ZM2 14.3992V18.7395C2 19.1477 2.21366 19.5247 2.55984 19.7272L6.44446 22V16.8223L2 14.3992ZM8.66668 22L12 20.0497L15.3333 22V16.7994L12 14.8492L8.66668 16.7993V22ZM17.5556 22L21.4401 19.7272C21.7863 19.5247 22 19.1477 22 18.7395V14.3992L17.5556 16.8223V22ZM21.0647 12.3315L17.5556 10.2784V6.46474L13.1111 9.00593V12.8787L16.4634 14.8399L21.0647 12.3315Z" />
</svg>
);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

+14 -36
View File
@@ -2,7 +2,6 @@
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { cn } from "@utils/helpers";
import { motion } from "framer-motion";
import { ChevronDown } from "lucide-react";
import * as React from "react";
@@ -24,7 +23,7 @@ const AccordionTrigger = React.forwardRef<
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center gap-4 font-medium [&[data-state=open]>svg.chevron]:rotate-180 hover:opacity-80 my-2",
"flex flex-1 items-center gap-4 font-medium transition-all [&[data-state=open]>svg.chevron]:rotate-180 hover:opacity-80 my-2",
className,
)}
{...props}
@@ -37,41 +36,20 @@ const AccordionTrigger = React.forwardRef<
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
HTMLDivElement,
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children }, ref) => {
const wrapperRef = React.useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = React.useState(false);
React.useEffect(() => {
const el = wrapperRef.current?.closest("[data-state]");
if (!el) return;
const update = () => setIsOpen(el.getAttribute("data-state") === "open");
update();
const observer = new MutationObserver(update);
observer.observe(el, { attributes: true, attributeFilter: ["data-state"] });
return () => observer.disconnect();
}, []);
return (
<div ref={wrapperRef}>
<motion.div
ref={ref}
initial={false}
animate={{
height: isOpen ? "auto" : 0,
opacity: isOpen ? 1 : 0,
}}
transition={{ duration: 0.15, ease: "easeOut" }}
className={cn("overflow-hidden text-sm", className)}
>
<div className="pt-0">{children}</div>
</motion.div>
</div>
);
});
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className={cn(
"overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
className,
)}
{...props}
>
<div className=" pt-0">{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };
-2
View File
@@ -23,7 +23,6 @@ const variants = cva("", {
purple: ["bg-purple-950/50 border-purple-500 border text-purple-500"],
yellow: ["bg-yellow-950 border-yellow-500 border text-yellow-400"],
gray: ["bg-nb-gray-930/60 border-nb-gray-800/40 text-nb-gray-300 border"],
lightGray: ["bg-nb-gray-910 text-nb-gray-200 border border-nb-gray-900"],
grayer: [
"bg-nb-gray-900/40 border-nb-gray-800/40 text-nb-gray-300 border",
],
@@ -46,7 +45,6 @@ const variants = cva("", {
"blue-darker": ["hover:bg-sky-800"],
red: ["hover:bg-red-950/40"],
gray: ["hover:bg-nb-gray-900"],
lightGray: ["hover:bg-nb-gray-900"],
grayer: ["hover:bg-nb-gray-900"],
"gray-ghost": ["hover:bg-nb-gray-800 cursor-pointer"],
green: ["hover:bg-green-950/50"],
+1 -1
View File
@@ -74,7 +74,7 @@ export const buttonVariants = cva(
"",
],
"danger-text": [
"dark:bg-transparent dark:text-red-500 dark:hover:text-red-600 dark:border-transparent !px-0 !shadow-none !py-0 focus:ring-red-500/30 dark:ring-offset-neutral-950/50 rounded-sm",
"dark:bg-transparent dark:text-red-500 dark:hover:text-red-600 dark:border-transparent !px-0 !shadow-none !py-0 focus:ring-red-500/30 dark:ring-offset-neutral-950/50",
],
"default-outline": [
"dark:ring-offset-nb-gray-950/50 dark:focus:ring-nb-gray-500/20",
+3 -3
View File
@@ -50,11 +50,11 @@ function CardListItem({
return (
<li
className={cn(
"flex justify-between px-4 border-b border-nb-gray-900 py-3.5 last:border-b-0 items-center h-full",
"flex justify-between px-4 border-b border-nb-gray-900 py-4 last:border-b-0 items-center h-full",
className,
)}
>
<div className={"flex gap-2.5 items-center text-[0.84rem]"}>{label}</div>
<div className={"flex gap-2.5 items-center text-sm"}>{label}</div>
<div className={"flex flex-col gap-2"}>
<CardTextItem
label={label}
@@ -100,7 +100,7 @@ const CardTextItem = ({
return (
<div
className={cn(
"text-right text-nb-gray-400 text-[0.84rem] flex items-center gap-2",
"text-right text-nb-gray-400 text-sm flex items-center gap-2",
copy && "cursor-pointer hover:text-nb-gray-300 transition-all",
)}
onClick={() =>
+7 -9
View File
@@ -80,15 +80,13 @@ export const DeviceCard = ({
hideTooltip={true}
/>
</span>
{descriptionText && (
<span
className={
"text-sm font-normal text-nb-gray-400 relative whitespace-nowrap"
}
>
<TruncatedText text={descriptionText} maxWidth={"160px"} />
</span>
)}
<span
className={
"text-sm font-normal text-nb-gray-400 relative whitespace-nowrap"
}
>
<TruncatedText text={descriptionText} maxWidth={"160px"} />
</span>
</div>
</div>
);
+3 -7
View File
@@ -8,7 +8,7 @@ import React from "react";
export const fancyToggleSwitchVariants = cva([], {
variants: {
variant: {
default: ["px-5 py-4 border rounded-md"],
default: ["px-6 py-4 border rounded-md"],
blank: null,
},
state: {
@@ -45,8 +45,6 @@ interface Props extends FancyToggleSwitchVariants {
disabled?: boolean;
dataCy?: string;
className?: string;
labelClassName?: string;
textWrapperClassName?: string;
}
export default function FancyToggleSwitch({
@@ -59,8 +57,6 @@ export default function FancyToggleSwitch({
dataCy,
className,
variant = "default",
labelClassName,
textWrapperClassName = "max-w-sm",
}: Readonly<Props>) {
const handleToggle = () => {
if (disabled) return;
@@ -91,8 +87,8 @@ export default function FancyToggleSwitch({
)}
>
<div className={"flex justify-between gap-10"}>
<div className={cn(textWrapperClassName)}>
<Label className={labelClassName}>{label}</Label>
<div className={"max-w-sm"}>
<Label>{label}</Label>
<HelpText margin={false}>{helpText}</HelpText>
</div>
<div className={"mt-2 pr-1"}>
+8 -47
View File
@@ -1,68 +1,29 @@
import * as React from "react";
import FullTooltip from "@components/FullTooltip";
import { HelpCircle } from "lucide-react";
import { cn } from "@utils/helpers";
import { TooltipVariants } from "@components/Tooltip";
type Props = {
content: React.ReactNode;
children?: React.ReactNode;
children: React.ReactNode;
interactive?: boolean;
className?: string;
triggerClassName?: string;
align?: "start" | "center" | "end";
side?: "top" | "right" | "bottom" | "left";
alignOffset?: number;
sideOffset?: number;
iconSize?: number;
delayDuration?: number;
} & TooltipVariants;
};
export const HelpTooltip = ({
content,
children,
interactive = false,
className,
variant = "default",
triggerClassName,
align = "start",
side = "top",
alignOffset = 0,
sideOffset,
iconSize = 12,
delayDuration = 300,
interactive = true,
}: Props) => {
return (
<>
<FullTooltip
interactive={interactive}
side={side}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
delayDuration={delayDuration}
variant={variant}
side={"top"}
align={"start"}
alignOffset={0}
className={
"inline underline decoration-dashed underline-offset-[3px] decoration-nb-gray-300 cursor-help transition-all hover:decoration-white"
}
content={
<div className={cn("max-w-xs text-xs", className)}>{content}</div>
}
content={content}
>
{children ? (
children
) : (
<span
className={cn(
"p-2 -m-2 inline-flex items-center justify-center relative top-[1px] group/help",
triggerClassName,
)}
>
<HelpCircle
size={iconSize}
className={"text-nb-gray-300 group-hover/help:text-nb-gray-100"}
/>
</span>
)}
{children}
</FullTooltip>
</>
);
Loaded 20 of 127 files, more files were not shown because too many files have changed in this diff. Show more