mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff5ca42cbb | ||
|
|
44b27d29f2 | ||
|
|
61834b65c2 | ||
|
|
c178d700a3 | ||
|
|
d76cbd1122 | ||
|
|
c4ef73a338 | ||
|
|
01330e0f58 | ||
|
|
c6721a6467 | ||
|
|
e9ac1a1a23 | ||
|
|
b53802a5c5 | ||
|
|
9addc18956 | ||
|
|
9701e6503b | ||
|
|
0841caecbb | ||
|
|
c7846760d1 | ||
|
|
8c283b6ef9 | ||
|
|
34ae3b4da6 |
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 âś…"
|
||||
@@ -10,6 +10,7 @@
|
||||
"redirectURI": "$AUTH_REDIRECT_URI",
|
||||
"silentRedirectURI": "$AUTH_SILENT_REDIRECT_URI",
|
||||
"tokenSource": "$NETBIRD_TOKEN_SOURCE",
|
||||
"disableServiceWorker": "$NETBIRD_DISABLE_SERVICE_WORKER",
|
||||
"dragQueryParams": "$NETBIRD_DRAG_QUERY_PARAMS",
|
||||
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
|
||||
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID",
|
||||
|
||||
@@ -60,13 +60,14 @@ export NETBIRD_HOTJAR_TRACK_ID=${NETBIRD_HOTJAR_TRACK_ID}
|
||||
export NETBIRD_GOOGLE_ANALYTICS_ID=${NETBIRD_GOOGLE_ANALYTICS_ID}
|
||||
export NETBIRD_GOOGLE_TAG_MANAGER_ID=${NETBIRD_GOOGLE_TAG_MANAGER_ID}
|
||||
export NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken}
|
||||
export NETBIRD_DISABLE_SERVICE_WORKER=${NETBIRD_DISABLE_SERVICE_WORKER:-false}
|
||||
export NETBIRD_DRAG_QUERY_PARAMS=${NETBIRD_DRAG_QUERY_PARAMS:-false}
|
||||
export NETBIRD_WASM_PATH=${NETBIRD_WASM_PATH}
|
||||
|
||||
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
|
||||
|
||||
# replace ENVs in the config
|
||||
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_CLIENT_SECRET \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$NETBIRD_GOOGLE_ANALYTICS_ID \$\$NETBIRD_GOOGLE_TAG_MANAGER_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE \$\$NETBIRD_DRAG_QUERY_PARAMS \$\$NETBIRD_WASM_PATH"
|
||||
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_CLIENT_SECRET \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$NETBIRD_GOOGLE_ANALYTICS_ID \$\$NETBIRD_GOOGLE_TAG_MANAGER_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE \$\$NETBIRD_DISABLE_SERVICE_WORKER \$\$NETBIRD_DRAG_QUERY_PARAMS \$\$NETBIRD_WASM_PATH"
|
||||
|
||||
OIDC_TRUSTED_DOMAINS="/usr/share/nginx/html/OidcTrustedDomains.js"
|
||||
envsubst "$ENV_STR" < "$OIDC_TRUSTED_DOMAINS".tmpl > "$OIDC_TRUSTED_DOMAINS"
|
||||
|
||||
Generated
+38
-80
@@ -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.7",
|
||||
"lucide-react": "^0.566.0",
|
||||
"next": "16.1.7",
|
||||
"next-themes": "^0.2.1",
|
||||
"punycode": "^2.3.1",
|
||||
"react": "^19.2.4",
|
||||
@@ -168,7 +168,6 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -3031,7 +3030,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz",
|
||||
"integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -3041,7 +3039,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@@ -3100,7 +3097,6 @@
|
||||
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
@@ -3244,32 +3240,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",
|
||||
@@ -3607,8 +3577,7 @@
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@xyflow/react": {
|
||||
"version": "12.10.0",
|
||||
@@ -3647,7 +3616,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3998,10 +3966,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 +3996,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": {
|
||||
@@ -4065,7 +4038,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -4320,12 +4292,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",
|
||||
@@ -4694,7 +4660,6 @@
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -5223,7 +5188,6 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -5421,7 +5385,6 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@@ -6715,7 +6678,6 @@
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
@@ -6889,9 +6851,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 +6885,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 +6935,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": {
|
||||
@@ -7412,9 +7377,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"
|
||||
@@ -7470,7 +7435,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -7678,7 +7642,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -7719,7 +7682,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -8614,7 +8576,6 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
||||
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
@@ -8777,11 +8738,10 @@
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -8947,7 +8907,6 @@
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -9275,7 +9234,6 @@
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
+7
-5
@@ -6,11 +6,10 @@
|
||||
"node": ">=20.9.0"
|
||||
},
|
||||
"scripts": {
|
||||
"copy": "copyfiles -f ./node_modules/@axa-fr/react-oidc/dist/OidcServiceWorker.js ./public",
|
||||
"copytrusted": "copyfiles -f ./public/local/OidcTrustedDomains.js ./public",
|
||||
"oidc-gen": "node ./node_modules/@axa-fr/react-oidc/bin/copy-service-worker-files.mjs public",
|
||||
"dev": "next dev -p 3000",
|
||||
"turbo": "next dev -p 3000 --turbo",
|
||||
"build": "next build",
|
||||
"build": "npm run oidc-gen && next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"cypress:open": "cypress open"
|
||||
@@ -67,8 +66,8 @@
|
||||
"ip-cidr": "^3.1.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lodash": "^4.17.23",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next": "^16.1.7",
|
||||
"lucide-react": "^0.566.0",
|
||||
"next": "16.1.7",
|
||||
"next-themes": "^0.2.1",
|
||||
"punycode": "^2.3.1",
|
||||
"react": "^19.2.4",
|
||||
@@ -89,6 +88,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",
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,6 @@
|
||||
|
||||
// Add here trusted domains, access tokens will be send to
|
||||
const trustedDomains = {
|
||||
default:["http://localhost:3001","http://127.0.0.1:3001", "http://0.0.0.0:33071"],
|
||||
auth0:[]
|
||||
};
|
||||
@@ -1,6 +1,9 @@
|
||||
|
||||
// Add here trusted domains, access tokens will be send to
|
||||
|
||||
// Domains used by OIDC server must be also declared here
|
||||
|
||||
const trustedDomains = {
|
||||
default:["$NETBIRD_MGMT_API_ENDPOINT"],
|
||||
auth0:[]
|
||||
};
|
||||
default: [
|
||||
"$NETBIRD_MGMT_API_ENDPOINT",
|
||||
"$NETBIRD_MGMT_GRPC_API_ENDPOINT",
|
||||
].filter((d) => d && d.length > 0),
|
||||
};
|
||||
@@ -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" />,
|
||||
};
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
+97
-10
@@ -2,21 +2,47 @@
|
||||
|
||||
import {
|
||||
AuthorityConfiguration,
|
||||
OidcClient,
|
||||
OidcConfiguration,
|
||||
OidcProvider,
|
||||
} from "@axa-fr/react-oidc";
|
||||
import FullScreenLoading from "@components/ui/FullScreenLoading";
|
||||
import loadConfig, { buildExtras } from "@utils/config";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { usePathname } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { OIDCError } from "@/auth/OIDCError";
|
||||
import { SecureProvider } from "@/auth/SecureProvider";
|
||||
import { storeSessionLostRedirectPath } from "@/auth/sessionRedirect";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const config = loadConfig();
|
||||
const AUTHENTICATED_CALLBACK_REDIRECT_PATH = "/peers";
|
||||
|
||||
const getOidcRoutePath = (href: string, origin: string) => {
|
||||
try {
|
||||
const url = new URL(href, origin);
|
||||
let path = url.pathname;
|
||||
if (path.endsWith("/")) path = path.slice(0, -1);
|
||||
|
||||
let hash = url.hash;
|
||||
if (hash === "#_=_") hash = "";
|
||||
if (hash) path += hash.split("?")[0];
|
||||
|
||||
return path;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const isOidcCallbackRoute = (href: string, redirectUri: string) =>
|
||||
getOidcRoutePath(href, window.location.origin) ===
|
||||
getOidcRoutePath(redirectUri, window.location.origin);
|
||||
|
||||
const hasUsableTokens = (tokens: { expiresAt?: number } | null | undefined) =>
|
||||
typeof tokens?.expiresAt === "number" && tokens.expiresAt > Date.now() / 1000;
|
||||
|
||||
/**
|
||||
* Unfortunately Auth0 https://<DOMAIN>/.well-known/openid-configuration doesn't contain end_session_endpoint that
|
||||
@@ -40,7 +66,6 @@ const onEvent = (configurationName: any, eventName: any, data: any) => {
|
||||
export default function OIDCProvider({ children }: Props) {
|
||||
const [providerConfig, setProviderConfig] = useState<OidcConfiguration>();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const router = useRouter();
|
||||
const path = usePathname();
|
||||
|
||||
const withCustomHistory = () => {
|
||||
@@ -52,16 +77,30 @@ export default function OIDCProvider({ children }: Props) {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setProviderConfig({
|
||||
let isMounted = true;
|
||||
|
||||
// The service worker is disabled in two cases:
|
||||
// 1. tokenSource === "idtoken" — the SW would overwrite the manually-set
|
||||
// idToken header with the access_token, breaking the idToken path.
|
||||
// 2. NETBIRD_DISABLE_SERVICE_WORKER=true — operator escape hatch for
|
||||
// debugging or deployments that hit edge cases with the SW.
|
||||
const useServiceWorker =
|
||||
!config.disableServiceWorker &&
|
||||
config.tokenSource?.toLowerCase() !== "idtoken";
|
||||
|
||||
const nextProviderConfig: OidcConfiguration = {
|
||||
authority: config.authority,
|
||||
client_id: config.clientId,
|
||||
redirect_uri: window.location.origin + config.redirectURI,
|
||||
refresh_time_before_tokens_expiration_in_second: 30,
|
||||
silent_redirect_uri: window.location.origin + config.silentRedirectURI,
|
||||
scope: config.scopesSupported,
|
||||
// disabling service worker
|
||||
//service_worker_relative_url: "/OidcServiceWorker.js",
|
||||
service_worker_only: false,
|
||||
...(useServiceWorker
|
||||
? {
|
||||
service_worker_relative_url: "/OidcServiceWorker.js",
|
||||
service_worker_only: false,
|
||||
}
|
||||
: {}),
|
||||
authority_configuration: config.auth0Auth
|
||||
? auth0AuthorityConfig
|
||||
: undefined,
|
||||
@@ -69,10 +108,53 @@ export default function OIDCProvider({ children }: Props) {
|
||||
...(config.clientSecret
|
||||
? { token_request_extras: { client_secret: config.clientSecret } }
|
||||
: null),
|
||||
});
|
||||
setMounted(true);
|
||||
};
|
||||
|
||||
const finishMounting = () => {
|
||||
if (isMounted) setMounted(true);
|
||||
};
|
||||
|
||||
const redirectAuthenticatedCallback = async () => {
|
||||
// OidcRoutes handles the callback route before OidcSession restores
|
||||
// existing tokens. If a callback URL is reached while the app already
|
||||
// has a valid OIDC session, leave the callback route instead of trying
|
||||
// to process a stale/duplicate callback.
|
||||
if (
|
||||
!isOidcCallbackRoute(
|
||||
window.location.href,
|
||||
nextProviderConfig.redirect_uri,
|
||||
)
|
||||
) {
|
||||
finishMounting();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const oidc = OidcClient.getOrCreate(() => fetch)(nextProviderConfig);
|
||||
await oidc.tryKeepExistingSessionAsync();
|
||||
if (!isMounted) return;
|
||||
if (hasUsableTokens(oidc.tokens)) {
|
||||
window.location.replace(AUTHENTICATED_CALLBACK_REDIRECT_PATH);
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
finishMounting();
|
||||
};
|
||||
|
||||
setProviderConfig(nextProviderConfig);
|
||||
void redirectAuthenticatedCallback();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSessionLost = () => {
|
||||
storeSessionLostRedirectPath();
|
||||
window.location.replace("/");
|
||||
};
|
||||
|
||||
// We bypass authentication for pages that do not require auth.
|
||||
// E.g., when we just want to show installation steps for public.
|
||||
// Or the instance setup wizard for first-time setup.
|
||||
@@ -89,8 +171,13 @@ export default function OIDCProvider({ children }: Props) {
|
||||
loadingComponent={FullScreenLoading}
|
||||
callbackSuccessComponent={FullScreenLoading}
|
||||
onEvent={onEvent}
|
||||
onSessionLost={() => void 0}
|
||||
//sessionLostComponent={SessionLost}
|
||||
// If session is lost, try to re-initiate authentication flow while
|
||||
// preserving the current deep link for the post-login redirect.
|
||||
onSessionLost={handleSessionLost}
|
||||
// Another tab logged out — fires a separate event in @axa-fr/react-oidc,
|
||||
// not covered by onSessionLost. Without this handler the library would
|
||||
// render its default "Session timed out" UI instead of redirecting.
|
||||
onLogoutFromAnotherTab={handleSessionLost}
|
||||
>
|
||||
<SecureProvider>{children}</SecureProvider>
|
||||
</OidcProvider>
|
||||
|
||||
@@ -2,6 +2,10 @@ import { OidcSecure, useOidc } from "@axa-fr/react-oidc";
|
||||
import { usePathname } from "next/navigation";
|
||||
import * as React from "react";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
clearSessionLostRedirectPath,
|
||||
getSessionLostRedirectPath,
|
||||
} from "@/auth/sessionRedirect";
|
||||
|
||||
const QUERY_PARAMS_KEY = "netbird-query-params";
|
||||
const PRESERVE_QUERY_PARAMS_PATHS = ["/peer/ssh", "/peer/rdp"];
|
||||
@@ -27,11 +31,15 @@ type Props = {
|
||||
export const SecureProvider = ({ children }: Props) => {
|
||||
const { isAuthenticated, login } = useOidc();
|
||||
const currentPath = usePathname();
|
||||
const callbackPath = getSessionLostRedirectPath() ?? currentPath;
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated && !PRESERVE_QUERY_PARAMS_PATHS.includes(currentPath)) {
|
||||
localStorage.removeItem(QUERY_PARAMS_KEY);
|
||||
} else if (!isAuthenticated) {
|
||||
if (isAuthenticated) {
|
||||
clearSessionLostRedirectPath();
|
||||
if (!PRESERVE_QUERY_PARAMS_PATHS.includes(currentPath)) {
|
||||
localStorage.removeItem(QUERY_PARAMS_KEY);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const params = window.location.search.substring(1);
|
||||
if (params) {
|
||||
@@ -49,18 +57,18 @@ export const SecureProvider = ({ children }: Props) => {
|
||||
if (!isAuthenticated) {
|
||||
timeout = setTimeout(async () => {
|
||||
if (!isAuthenticated) {
|
||||
await login(currentPath);
|
||||
await login(callbackPath);
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [currentPath, isAuthenticated, login]);
|
||||
}, [callbackPath, isAuthenticated, login]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<OidcSecure callbackPath={currentPath}>{children}</OidcSecure>
|
||||
<OidcSecure callbackPath={callbackPath}>{children}</OidcSecure>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
const SESSION_LOST_REDIRECT_PATH_KEY = "netbird-session-lost-redirect-path";
|
||||
|
||||
const isSafeRelativePath = (path: string) =>
|
||||
path.startsWith("/") && !path.startsWith("//");
|
||||
|
||||
export const storeSessionLostRedirectPath = () => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const redirectPath = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||
|
||||
try {
|
||||
if (
|
||||
!redirectPath ||
|
||||
redirectPath === "/" ||
|
||||
!isSafeRelativePath(redirectPath)
|
||||
) {
|
||||
window.sessionStorage.removeItem(SESSION_LOST_REDIRECT_PATH_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
window.sessionStorage.setItem(
|
||||
SESSION_LOST_REDIRECT_PATH_KEY,
|
||||
redirectPath,
|
||||
);
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const getSessionLostRedirectPath = () => {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
|
||||
try {
|
||||
const redirectPath = window.sessionStorage.getItem(
|
||||
SESSION_LOST_REDIRECT_PATH_KEY,
|
||||
);
|
||||
if (redirectPath && isSafeRelativePath(redirectPath)) return redirectPath;
|
||||
} catch {}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const clearSessionLostRedirectPath = () => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
try {
|
||||
window.sessionStorage.removeItem(SESSION_LOST_REDIRECT_PATH_KEY);
|
||||
} catch {}
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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"}>
|
||||
|
||||
@@ -98,7 +98,7 @@ const SelectItem = React.forwardRef<
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex-shrink-0">{icon}</span>
|
||||
<div className="flex flex-col">
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -53,6 +53,7 @@ declare module "@tanstack/table-core" {
|
||||
}
|
||||
interface SortingFns {
|
||||
checkbox: SortingFn<unknown>;
|
||||
datetime: SortingFn<unknown>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +100,15 @@ const arrIncludesSomeExact: FilterFn<any> = (
|
||||
return value.some((val) => val === rowValue);
|
||||
};
|
||||
|
||||
const datetimeSort: SortingFn<any> = (rowA, rowB, columnId) => {
|
||||
const aConnected = rowA.original?.connected;
|
||||
const bConnected = rowB.original?.connected;
|
||||
if (aConnected !== bConnected) return aConnected ? 1 : -1;
|
||||
const a = dayjs(rowA.getValue(columnId)).valueOf();
|
||||
const b = dayjs(rowB.getValue(columnId)).valueOf();
|
||||
return a - b;
|
||||
};
|
||||
|
||||
const checkboxSort: SortingFn<any> = (rowA, rowB, columnId) => {
|
||||
const valueA =
|
||||
columnId === "select" ? rowA.getIsSelected() : rowA.getValue(columnId);
|
||||
@@ -324,6 +334,7 @@ export function DataTable<TData, TValue>({
|
||||
},
|
||||
sortingFns: {
|
||||
checkbox: checkboxSort,
|
||||
datetime: datetimeSort,
|
||||
},
|
||||
getRowId: useRowId ? (row) => row.id : undefined,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
|
||||
Loaded 20 of 67 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user