mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5be79b9955 | ||
|
|
dc1adebd27 | ||
|
|
d76cbd1122 | ||
|
|
01330e0f58 | ||
|
|
e9ac1a1a23 | ||
|
|
b53802a5c5 | ||
|
|
9addc18956 | ||
|
|
9701e6503b | ||
|
|
0841caecbb | ||
|
|
c7846760d1 | ||
|
|
8c283b6ef9 | ||
|
|
34ae3b4da6 | ||
|
|
aff2365ef7 | ||
|
|
bad057d415 | ||
|
|
4d846e2c94 |
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 âś…"
|
||||
Generated
+80
-104
@@ -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.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",
|
||||
@@ -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.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"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",
|
||||
|
||||
+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>
|
||||
);
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from "@utils/version";
|
||||
|
||||
export default function SSHPage() {
|
||||
const { peerId, username, port } = useSSHQueryParams();
|
||||
const { peerId, username, port, ipVersion } = useSSHQueryParams();
|
||||
|
||||
const {
|
||||
data: peer,
|
||||
@@ -48,6 +48,7 @@ export default function SSHPage() {
|
||||
peer={peer}
|
||||
username={username}
|
||||
port={port}
|
||||
ipVersion={ipVersion}
|
||||
/>
|
||||
) : (
|
||||
<LoadingMessage message={"Starting ssh session..."} />
|
||||
@@ -60,9 +61,10 @@ type Props = {
|
||||
username: string;
|
||||
port: string;
|
||||
peer: Peer;
|
||||
ipVersion: string | null;
|
||||
};
|
||||
|
||||
function SSHTerminal({ username, port, peer }: Props) {
|
||||
function SSHTerminal({ username, port, peer, ipVersion }: Props) {
|
||||
const client = useNetBirdClient();
|
||||
const connected = useRef(false);
|
||||
const sshConnectedOnce = useRef(false);
|
||||
@@ -81,9 +83,12 @@ function SSHTerminal({ username, port, peer }: Props) {
|
||||
const isClientDisconnected = client.status === NetBirdStatus.DISCONNECTED;
|
||||
const isClientConnecting = client.status === NetBirdStatus.CONNECTING;
|
||||
|
||||
// Use the FQDN when an IP version is specified so the dialer resolves to the correct address family.
|
||||
const sshHost = ipVersion ? peer.dns_label || peer.ip : peer.ip;
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${username}@${peer.ip} - ${peer.hostname}`;
|
||||
}, [username, peer, client]);
|
||||
document.title = `${username}@${sshHost} - ${peer.hostname}`;
|
||||
}, [username, peer, client, sshHost]);
|
||||
|
||||
const handleReconnect = async () => {
|
||||
if (!peer?.id) return;
|
||||
@@ -97,9 +102,10 @@ function SSHTerminal({ username, port, peer }: Props) {
|
||||
const rules = [`${protocol}/${aclPort}`];
|
||||
await client?.connectTemporary(peer.id, rules);
|
||||
await ssh({
|
||||
hostname: peer.ip,
|
||||
hostname: sshHost,
|
||||
port: Number(port),
|
||||
username,
|
||||
ipVersion: ipVersion || undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Reconnection failed:", error);
|
||||
@@ -123,9 +129,10 @@ function SSHTerminal({ username, port, peer }: Props) {
|
||||
const rules = [`${protocol}/${aclPort}`];
|
||||
await client?.connectTemporary(peer.id, rules);
|
||||
const res = await ssh({
|
||||
hostname: peer.ip,
|
||||
hostname: sshHost,
|
||||
port: Number(port),
|
||||
username,
|
||||
ipVersion: ipVersion || undefined,
|
||||
});
|
||||
if (res === SSHStatus.CONNECTED) {
|
||||
sshConnectedOnce.current = true;
|
||||
|
||||
@@ -23,6 +23,7 @@ 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" />,
|
||||
};
|
||||
|
||||
|
||||
@@ -3,103 +3,36 @@ import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
|
||||
export default function LoadingIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
width="135"
|
||||
height="140"
|
||||
viewBox="0 0 135 140"
|
||||
viewBox="0 0 919 669"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fillOpacity="0.8"
|
||||
{...iconProperties(props)}
|
||||
>
|
||||
<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"
|
||||
<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"
|
||||
/>
|
||||
<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="M533.256 668.02L0 102.296C0 102.296 603.149 -59.6172 661.827 445.893L533.256 668.02Z"
|
||||
fill="#F68330"
|
||||
/>
|
||||
</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"
|
||||
<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"
|
||||
/>
|
||||
<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>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,12 @@ export default function ReverseProxyIcon(props: IconProps) {
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...iconProperties(props)}
|
||||
fill={"currentColor"}
|
||||
>
|
||||
<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" />
|
||||
<path
|
||||
fill={"currentColor"}
|
||||
d="M11.4488 2.1499C11.7903 1.95003 12.2097 1.95003 12.5513 2.1499L16.5018 4.46123L12 7.03523L7.49823 4.46123L11.4488 2.1499ZM6.44447 6.46472L6.44444 10.2784L2.93531 12.3315L7.53662 14.8399L10.8889 12.8787V9.00593L6.44447 6.46472ZM2 14.3992V18.7395C2 19.1477 2.21366 19.5247 2.55984 19.7272L6.44446 22V16.8223L2 14.3992ZM8.66668 22L12 20.0497L15.3333 22V16.7994L12 14.8492L8.66668 16.7993V22ZM17.5556 22L21.4401 19.7272C21.7863 19.5247 22 19.1477 22 18.7395V14.3992L17.5556 16.8223V22ZM21.0647 12.3315L17.5556 10.2784V6.46474L13.1111 9.00593V12.8787L16.4634 14.8399L21.0647 12.3315Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -23,6 +23,7 @@ const variants = cva("", {
|
||||
purple: ["bg-purple-950/50 border-purple-500 border text-purple-500"],
|
||||
yellow: ["bg-yellow-950 border-yellow-500 border text-yellow-400"],
|
||||
gray: ["bg-nb-gray-930/60 border-nb-gray-800/40 text-nb-gray-300 border"],
|
||||
lightGray: ["bg-nb-gray-910 text-nb-gray-200 border border-nb-gray-900"],
|
||||
grayer: [
|
||||
"bg-nb-gray-900/40 border-nb-gray-800/40 text-nb-gray-300 border",
|
||||
],
|
||||
@@ -45,6 +46,7 @@ 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"],
|
||||
|
||||
@@ -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",
|
||||
"dark:bg-transparent dark:text-red-500 dark:hover:text-red-600 dark:border-transparent !px-0 !shadow-none !py-0 focus:ring-red-500/30 dark:ring-offset-neutral-950/50 rounded-sm",
|
||||
],
|
||||
"default-outline": [
|
||||
"dark:ring-offset-nb-gray-950/50 dark:focus:ring-nb-gray-500/20",
|
||||
|
||||
@@ -50,11 +50,11 @@ function CardListItem({
|
||||
return (
|
||||
<li
|
||||
className={cn(
|
||||
"flex justify-between px-4 border-b border-nb-gray-900 py-4 last:border-b-0 items-center h-full",
|
||||
"flex justify-between px-4 border-b border-nb-gray-900 py-3.5 last:border-b-0 items-center h-full",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className={"flex gap-2.5 items-center text-sm"}>{label}</div>
|
||||
<div className={"flex gap-2.5 items-center text-[0.84rem]"}>{label}</div>
|
||||
<div className={"flex flex-col gap-2"}>
|
||||
<CardTextItem
|
||||
label={label}
|
||||
@@ -100,7 +100,7 @@ const CardTextItem = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"text-right text-nb-gray-400 text-sm flex items-center gap-2",
|
||||
"text-right text-nb-gray-400 text-[0.84rem] flex items-center gap-2",
|
||||
copy && "cursor-pointer hover:text-nb-gray-300 transition-all",
|
||||
)}
|
||||
onClick={() =>
|
||||
|
||||
@@ -80,13 +80,15 @@ export const DeviceCard = ({
|
||||
hideTooltip={true}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
"text-sm font-normal text-nb-gray-400 relative whitespace-nowrap"
|
||||
}
|
||||
>
|
||||
<TruncatedText text={descriptionText} maxWidth={"160px"} />
|
||||
</span>
|
||||
{descriptionText && (
|
||||
<span
|
||||
className={
|
||||
"text-sm font-normal text-nb-gray-400 relative whitespace-nowrap"
|
||||
}
|
||||
>
|
||||
<TruncatedText text={descriptionText} maxWidth={"160px"} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import React from "react";
|
||||
export const fancyToggleSwitchVariants = cva([], {
|
||||
variants: {
|
||||
variant: {
|
||||
default: ["px-6 py-4 border rounded-md"],
|
||||
default: ["px-5 py-4 border rounded-md"],
|
||||
blank: null,
|
||||
},
|
||||
state: {
|
||||
@@ -45,6 +45,8 @@ interface Props extends FancyToggleSwitchVariants {
|
||||
disabled?: boolean;
|
||||
dataCy?: string;
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
textWrapperClassName?: string;
|
||||
}
|
||||
|
||||
export default function FancyToggleSwitch({
|
||||
@@ -57,6 +59,8 @@ export default function FancyToggleSwitch({
|
||||
dataCy,
|
||||
className,
|
||||
variant = "default",
|
||||
labelClassName,
|
||||
textWrapperClassName = "max-w-sm",
|
||||
}: Readonly<Props>) {
|
||||
const handleToggle = () => {
|
||||
if (disabled) return;
|
||||
@@ -87,8 +91,8 @@ export default function FancyToggleSwitch({
|
||||
)}
|
||||
>
|
||||
<div className={"flex justify-between gap-10"}>
|
||||
<div className={"max-w-sm"}>
|
||||
<Label>{label}</Label>
|
||||
<div className={cn(textWrapperClassName)}>
|
||||
<Label className={labelClassName}>{label}</Label>
|
||||
<HelpText margin={false}>{helpText}</HelpText>
|
||||
</div>
|
||||
<div className={"mt-2 pr-1"}>
|
||||
|
||||
@@ -290,7 +290,7 @@ export function PeerGroupSelector({
|
||||
const searchPlaceholder = useMemo(() => {
|
||||
if (tab === "groups") return placeholderForSearch;
|
||||
if (tab === "resources") return "Search resource...";
|
||||
if (tab === "peers") return "Search peer...";
|
||||
if (tab === "peers") return "Search peer by name or ip...";
|
||||
return "Search...";
|
||||
}, [tab, placeholderForSearch]);
|
||||
|
||||
@@ -537,9 +537,6 @@ export function PeerGroupSelector({
|
||||
const isSelected =
|
||||
values.find((group) => group.name == option.name) !=
|
||||
undefined;
|
||||
const peerCount =
|
||||
option.peers?.length ?? option?.peers_count ?? 0;
|
||||
|
||||
const isDisabled = disabledGroups
|
||||
? disabledGroups?.findIndex(
|
||||
(g) => g.id === option.id,
|
||||
@@ -968,7 +965,8 @@ const ResourcesList = ({
|
||||
const peersSearchPredicate = (item: Peer, query: string) => {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
if (item.name.toLowerCase().includes(lowerCaseQuery)) return true;
|
||||
return item.ip.toLowerCase().includes(lowerCaseQuery);
|
||||
if (item.ip.toLowerCase().includes(lowerCaseQuery)) return true;
|
||||
return item.ipv6?.toLowerCase().includes(lowerCaseQuery) ?? false;
|
||||
};
|
||||
|
||||
const PeersList = ({
|
||||
|
||||
@@ -30,7 +30,8 @@ const searchPredicate = (item: Peer, query: string) => {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
if (item.name.toLowerCase().includes(lowerCaseQuery)) return true;
|
||||
if (item.hostname.toLowerCase().includes(lowerCaseQuery)) return true;
|
||||
return item.ip.toLowerCase().startsWith(lowerCaseQuery);
|
||||
if (item.ip.toLowerCase().startsWith(lowerCaseQuery)) return true;
|
||||
return !!item.ipv6?.toLowerCase().startsWith(lowerCaseQuery);
|
||||
};
|
||||
|
||||
export function PeerSelector({
|
||||
@@ -124,7 +125,6 @@ export function PeerSelector({
|
||||
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-1 font-mono text-[10px]"
|
||||
}
|
||||
>
|
||||
<MapPinIcon />
|
||||
{value.ip}
|
||||
</div>
|
||||
</div>
|
||||
@@ -238,7 +238,6 @@ export function PeerSelector({
|
||||
!isSupported && "opacity-50",
|
||||
)}
|
||||
>
|
||||
<MapPinIcon />
|
||||
{option.ip}
|
||||
</div>
|
||||
</FullTooltip>
|
||||
|
||||
@@ -8,6 +8,7 @@ type Props = {
|
||||
description: ReactNode;
|
||||
icon?: ReactNode;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const RadioCard = ({
|
||||
@@ -16,15 +17,18 @@ export const RadioCard = ({
|
||||
description,
|
||||
className,
|
||||
icon,
|
||||
disabled,
|
||||
}: Props) => {
|
||||
return (
|
||||
<RadioGroup.Item
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"peer relative block cursor-pointer rounded-lg border border-nb-gray-900 bg-nb-gray-930/60 px-5 py-3 transition-all focus:outline-none",
|
||||
"data-[state=checked]:border-nb-gray-400 data-[state=checked]:bg-nb-gray-920",
|
||||
"outline-none focus:ring-0 focus:bg-nb-gray-930 focus:border-nb-gray-920",
|
||||
"hover:bg-nb-gray-930",
|
||||
"disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-nb-gray-930/60",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -77,23 +77,56 @@ const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> & {
|
||||
extra?: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
}
|
||||
>(({ className, children, extra, ...props }, ref) => (
|
||||
>(({ className, children, extra, icon, description, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full select-none items-center rounded-md py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-gray-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-nb-gray-900 dark:focus:text-neutral-50 dark:text-gray-400 cursor-pointer",
|
||||
"relative flex w-full select-none items-center rounded-md py-1.5 text-sm outline-none focus:bg-gray-100 focus:text-neutral-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-nb-gray-900 dark:focus:text-neutral-50 dark:text-gray-400 cursor-pointer",
|
||||
icon ? "pl-2 pr-8" : "pl-8 pr-2",
|
||||
description && "py-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
{icon ? (
|
||||
<>
|
||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex-shrink-0">{icon}</span>
|
||||
<div className="flex flex-col">
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
{description && (
|
||||
<span className="text-xs text-nb-gray-300 font-normal">
|
||||
{description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<div className="flex flex-col">
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
{description && (
|
||||
<span className="text-xs text-nb-gray-300 font-normal">
|
||||
{description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{extra}
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
|
||||
@@ -48,6 +48,9 @@ interface SelectDropdownProps {
|
||||
children?: React.ReactNode;
|
||||
maxHeight?: number;
|
||||
triggerClassName?: string;
|
||||
iconSize?: number;
|
||||
truncate?: boolean;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export function SelectDropdown({
|
||||
@@ -68,6 +71,9 @@ export function SelectDropdown({
|
||||
children,
|
||||
maxHeight,
|
||||
triggerClassName,
|
||||
iconSize = 14,
|
||||
truncate = false,
|
||||
compact = false,
|
||||
}: Readonly<SelectDropdownProps>) {
|
||||
const [inputRef, { width }] = useElementSize<HTMLButtonElement>();
|
||||
|
||||
@@ -107,15 +113,18 @@ export function SelectDropdown({
|
||||
|
||||
const SelectedItem = () => {
|
||||
return (
|
||||
<div className={"flex items-center gap-2.5"}>
|
||||
{selected?.icon && <selected.icon size={14} width={14} />}
|
||||
<div className={cn("flex items-center gap-2.5", truncate && "min-w-0")}>
|
||||
{selected?.icon && <selected.icon size={iconSize} width={iconSize} />}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col text-sm font-medium",
|
||||
size === "xs" && "text-xs",
|
||||
truncate && "min-w-0",
|
||||
)}
|
||||
>
|
||||
<span className={"text-nb-gray-200"}>{selected?.label}</span>
|
||||
<span className={cn("text-nb-gray-200", truncate && "truncate")}>
|
||||
{selected?.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -216,20 +225,22 @@ export function SelectDropdown({
|
||||
|
||||
<ScrollArea
|
||||
className={cn(
|
||||
"overflow-y-auto flex flex-col gap-1 pl-2 pr-3",
|
||||
!showSearch && "pt-2",
|
||||
"overflow-y-auto flex flex-col gap-1",
|
||||
compact ? "pl-1 pr-1" : "pl-2 pr-3",
|
||||
!showSearch && (compact ? "pt-1" : "pt-2"),
|
||||
)}
|
||||
style={{
|
||||
maxHeight: maxHeight ?? 380,
|
||||
}}
|
||||
>
|
||||
<CommandGroup>
|
||||
<div className={"grid grid-cols-1 gap-1 pb-2 w-full"}>
|
||||
<div className={cn("grid grid-cols-1 gap-1 w-full", compact ? "pb-1" : "pb-2")}>
|
||||
{filteredItems.map((option) => (
|
||||
<SelectDropdownItem
|
||||
option={option}
|
||||
toggle={toggle}
|
||||
key={option.value}
|
||||
iconSize={iconSize}
|
||||
showValue={showValues}
|
||||
size={size}
|
||||
/>
|
||||
@@ -249,11 +260,13 @@ const SelectDropdownItem = ({
|
||||
toggle,
|
||||
showValue = false,
|
||||
size = "sm",
|
||||
iconSize = 14,
|
||||
}: {
|
||||
option: SelectOption;
|
||||
toggle: (value: string) => void;
|
||||
showValue?: boolean;
|
||||
size: "xs" | "sm";
|
||||
iconSize?: number;
|
||||
}) => {
|
||||
const value = option.value || "" + option.label || "";
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
@@ -285,7 +298,12 @@ const SelectDropdownItem = ({
|
||||
option?.disabled && "cursor-not-allowed",
|
||||
)}
|
||||
>
|
||||
{option.icon && <option.icon size={14} width={14} />}
|
||||
{option.icon && (
|
||||
<div className={"shrink-0"}>
|
||||
<option.icon size={iconSize} width={iconSize} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{option?.renderItem && option.renderItem()}
|
||||
{!option?.renderItem && (
|
||||
<div
|
||||
|
||||
Loaded 20 of 95 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user