mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8c6d44452 | ||
|
|
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 âś…"
|
||||||
Generated
+47
-80
@@ -58,9 +58,10 @@
|
|||||||
"ip-address": "^10.1.0",
|
"ip-address": "^10.1.0",
|
||||||
"ip-cidr": "^3.1.0",
|
"ip-cidr": "^3.1.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
|
"js-yaml": "^4.1.1",
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.566.0",
|
||||||
"next": "^16.1.7",
|
"next": "16.1.7",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"punycode": "^2.3.1",
|
"punycode": "^2.3.1",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
@@ -85,6 +86,7 @@
|
|||||||
"@faker-js/faker": "^9.5.1",
|
"@faker-js/faker": "^9.5.1",
|
||||||
"@types/chroma-js": "^3.1.1",
|
"@types/chroma-js": "^3.1.1",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"eslint-config-next": "^16.1.6",
|
"eslint-config-next": "^16.1.6",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
@@ -168,7 +170,6 @@
|
|||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@@ -2998,6 +2999,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/js-yaml": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
@@ -3031,7 +3039,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz",
|
||||||
"integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==",
|
"integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -3041,7 +3048,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
@@ -3100,7 +3106,6 @@
|
|||||||
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
|
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.54.0",
|
"@typescript-eslint/scope-manager": "8.54.0",
|
||||||
"@typescript-eslint/types": "8.54.0",
|
"@typescript-eslint/types": "8.54.0",
|
||||||
@@ -3244,32 +3249,6 @@
|
|||||||
"typescript": ">=4.8.4 <6.0.0"
|
"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": {
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
|
||||||
"version": "7.7.3",
|
"version": "7.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
@@ -3607,8 +3586,7 @@
|
|||||||
"version": "5.5.0",
|
"version": "5.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@xyflow/react": {
|
"node_modules/@xyflow/react": {
|
||||||
"version": "12.10.0",
|
"version": "12.10.0",
|
||||||
@@ -3647,7 +3625,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -3998,10 +3975,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.9.19",
|
"version": "2.9.19",
|
||||||
@@ -4025,13 +4005,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "5.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^4.0.2"
|
||||||
"concat-map": "0.0.1"
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/braces": {
|
"node_modules/braces": {
|
||||||
@@ -4065,7 +4047,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -4320,12 +4301,6 @@
|
|||||||
"node": ">= 10"
|
"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": {
|
"node_modules/convert-source-map": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
@@ -4694,7 +4669,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
@@ -5223,7 +5197,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
||||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -5421,7 +5394,6 @@
|
|||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
@@ -6715,7 +6687,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
@@ -6889,9 +6860,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.23",
|
"version": "4.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
@@ -6923,9 +6894,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lucide-react": {
|
"node_modules/lucide-react": {
|
||||||
"version": "0.562.0",
|
"version": "0.566.0",
|
||||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz",
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.566.0.tgz",
|
||||||
"integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==",
|
"integrity": "sha512-b18qC/JAh1X9rVKlF5EtSIyumdIYuh78b0JShynZnHbcaWR4AW4oZyi8Ms/aQYVSnLPlAnMhug2hSr19BgVZAw==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
@@ -6973,15 +6944,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.5",
|
"version": "10.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||||
"license": "ISC",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^5.0.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minimist": {
|
"node_modules/minimist": {
|
||||||
@@ -7412,9 +7386,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
@@ -7470,7 +7444,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -7678,7 +7651,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -7719,7 +7691,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -8614,7 +8585,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
||||||
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
@@ -8777,11 +8747,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -8947,7 +8916,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -9275,7 +9243,6 @@
|
|||||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-2
@@ -66,9 +66,10 @@
|
|||||||
"ip-address": "^10.1.0",
|
"ip-address": "^10.1.0",
|
||||||
"ip-cidr": "^3.1.0",
|
"ip-cidr": "^3.1.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
|
"js-yaml": "^4.1.1",
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.566.0",
|
||||||
"next": "^16.1.7",
|
"next": "16.1.7",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"punycode": "^2.3.1",
|
"punycode": "^2.3.1",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
@@ -89,10 +90,14 @@
|
|||||||
"timescape": "^0.7.1",
|
"timescape": "^0.7.1",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
},
|
||||||
|
"overrides": {
|
||||||
|
"minimatch": ">=10.2.1"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "^9.5.1",
|
"@faker-js/faker": "^9.5.1",
|
||||||
"@types/chroma-js": "^3.1.1",
|
"@types/chroma-js": "^3.1.1",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
"eslint-config-next": "^16.1.6",
|
"eslint-config-next": "^16.1.6",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { globalMetaTitle } from "@utils/meta";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import BlankLayout from "@/layouts/BlankLayout";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: `Inspection Policies - ${globalMetaTitle}`,
|
||||||
|
};
|
||||||
|
export default BlankLayout;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Breadcrumbs from "@components/Breadcrumbs";
|
||||||
|
import InlineLink from "@components/InlineLink";
|
||||||
|
import Paragraph from "@components/Paragraph";
|
||||||
|
import SkeletonTable from "@components/skeletons/SkeletonTable";
|
||||||
|
import { RestrictedAccess } from "@components/ui/RestrictedAccess";
|
||||||
|
import useFetchApi from "@utils/api";
|
||||||
|
import { ExternalLinkIcon, ShieldCheck } from "lucide-react";
|
||||||
|
import React, { lazy, Suspense } from "react";
|
||||||
|
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
|
||||||
|
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||||
|
import PoliciesProvider from "@/contexts/PoliciesProvider";
|
||||||
|
import { InspectionPolicy } from "@/interfaces/Network";
|
||||||
|
import PageContainer from "@/layouts/PageContainer";
|
||||||
|
|
||||||
|
const InspectionPoliciesTable = lazy(
|
||||||
|
() =>
|
||||||
|
import(
|
||||||
|
"@/modules/networks/inspection/InspectionPoliciesTable"
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function InspectionPoliciesPage() {
|
||||||
|
const { permission } = usePermissions();
|
||||||
|
const { data: policies, isLoading } =
|
||||||
|
useFetchApi<InspectionPolicy[]>("/inspection-policies");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<div className={"p-default py-6"}>
|
||||||
|
<Breadcrumbs>
|
||||||
|
<Breadcrumbs.Item
|
||||||
|
href={"/access-control"}
|
||||||
|
label={"Access Control"}
|
||||||
|
icon={<AccessControlIcon size={14} />}
|
||||||
|
/>
|
||||||
|
<Breadcrumbs.Item
|
||||||
|
href={"/access-control/inspection-policies"}
|
||||||
|
label={"Inspection Policies"}
|
||||||
|
icon={<ShieldCheck size={15} />}
|
||||||
|
/>
|
||||||
|
</Breadcrumbs>
|
||||||
|
<h1>Inspection Policies</h1>
|
||||||
|
<Paragraph>
|
||||||
|
Create reusable inspection rule sets for transparent proxy traffic
|
||||||
|
inspection. Attach them to access control policies to inspect traffic
|
||||||
|
flowing through routing peers.
|
||||||
|
</Paragraph>
|
||||||
|
</div>
|
||||||
|
<PoliciesProvider>
|
||||||
|
<Suspense fallback={<SkeletonTable />}>
|
||||||
|
<InspectionPoliciesTable
|
||||||
|
policies={policies ?? []}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</PoliciesProvider>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import Breadcrumbs from "@components/Breadcrumbs";
|
import Breadcrumbs from "@components/Breadcrumbs";
|
||||||
import Button from "@components/Button";
|
import Button from "@components/Button";
|
||||||
import { Callout } from "@components/Callout";
|
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
import HelpText from "@components/HelpText";
|
import HelpText from "@components/HelpText";
|
||||||
import { Input } from "@components/Input";
|
import { Input } from "@components/Input";
|
||||||
@@ -72,6 +71,7 @@ import ReverseProxiesProvider, {
|
|||||||
useReverseProxies,
|
useReverseProxies,
|
||||||
} from "@/contexts/ReverseProxiesProvider";
|
} from "@/contexts/ReverseProxiesProvider";
|
||||||
import { ReverseProxyFlatTargetsTabContent } from "@/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetsTabContent";
|
import { ReverseProxyFlatTargetsTabContent } from "@/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetsTabContent";
|
||||||
|
import { PeerEditIPModal } from "@/modules/peer/PeerEditIPModal";
|
||||||
import { PeerSSHToggle } from "@/modules/peer/PeerSSHToggle";
|
import { PeerSSHToggle } from "@/modules/peer/PeerSSHToggle";
|
||||||
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
|
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
|
||||||
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
|
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
|
||||||
@@ -469,31 +469,55 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
|||||||
const { update } = usePeer();
|
const { update } = usePeer();
|
||||||
const { mutate } = useSWRConfig();
|
const { mutate } = useSWRConfig();
|
||||||
const [showEditIPModal, setShowEditIPModal] = useState(false);
|
const [showEditIPModal, setShowEditIPModal] = useState(false);
|
||||||
|
const [showEditIPv6Modal, setShowEditIPv6Modal] = useState(false);
|
||||||
const { permission } = usePermissions();
|
const { permission } = usePermissions();
|
||||||
|
|
||||||
const countryText = useMemo(() => {
|
const countryText = useMemo(() => {
|
||||||
return getRegionByPeer(peer);
|
return getRegionByPeer(peer);
|
||||||
}, [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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal open={showEditIPModal} onOpenChange={setShowEditIPModal}>
|
<PeerEditIPModal
|
||||||
<EditIPModal
|
version="v4"
|
||||||
onSuccess={(newIP) => {
|
currentIP={peer.ip}
|
||||||
notify({
|
open={showEditIPModal}
|
||||||
title: peer.name,
|
onOpenChange={setShowEditIPModal}
|
||||||
description: "Peer IP was successfully updated",
|
onSave={handleSaveIP}
|
||||||
promise: update({ ip: newIP }).then(() => {
|
key={showEditIPModal ? "v4-open" : "v4-closed"}
|
||||||
mutate("/peers/" + peer.id);
|
/>
|
||||||
setShowEditIPModal(false);
|
<PeerEditIPModal
|
||||||
}),
|
version="v6"
|
||||||
loadingMessage: "Updating peer IP...",
|
currentIP={peer.ipv6 || ""}
|
||||||
});
|
open={showEditIPv6Modal}
|
||||||
}}
|
onOpenChange={setShowEditIPv6Modal}
|
||||||
peer={peer}
|
onSave={handleSaveIPv6}
|
||||||
key={showEditIPModal ? 1 : 0}
|
key={showEditIPv6Modal ? "v6-open" : "v6-closed"}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
|
||||||
<Card className={"w-full xl:w-1/2"}>
|
<Card className={"w-full xl:w-1/2"}>
|
||||||
<Card.List>
|
<Card.List>
|
||||||
<Card.ListItem
|
<Card.ListItem
|
||||||
@@ -508,23 +532,36 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
|||||||
}
|
}
|
||||||
valueToCopy={peer.ip}
|
valueToCopy={peer.ip}
|
||||||
value={
|
value={
|
||||||
<div className="flex items-center gap-2 justify-between w-full">
|
<EditableValue
|
||||||
<span>{peer.ip}</span>
|
value={peer.ip}
|
||||||
{permission.peers.update && (
|
canEdit={permission.peers.update}
|
||||||
<button
|
onEdit={() => setShowEditIPModal(true)}
|
||||||
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} />
|
||||||
|
NetBird IPv6 Address
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
valueToCopy={peer.ipv6}
|
||||||
|
value={
|
||||||
|
<EditableValue
|
||||||
|
value={peer.ipv6}
|
||||||
|
canEdit={permission.peers.update}
|
||||||
|
onEdit={() => setShowEditIPv6Modal(true)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Card.ListItem
|
<Card.ListItem
|
||||||
copy
|
copy
|
||||||
copyText={"Public IP Address"}
|
copyText={"Public IP Address"}
|
||||||
@@ -765,82 +802,29 @@ function EditNameModal({ onSuccess, peer, initialName }: Readonly<ModalProps>) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EditIPModalProps {
|
function EditableValue({
|
||||||
onSuccess: (ip: string) => void;
|
value,
|
||||||
peer: Peer;
|
canEdit,
|
||||||
}
|
onEdit,
|
||||||
|
}: {
|
||||||
function EditIPModal({ onSuccess, peer }: Readonly<EditIPModalProps>) {
|
value: string;
|
||||||
const [ip, setIP] = useState(peer.ip);
|
canEdit: boolean;
|
||||||
const [error, setError] = useState("");
|
onEdit: () => void;
|
||||||
|
}) {
|
||||||
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 (
|
return (
|
||||||
<ModalContent maxWidthClass={"max-w-md"}>
|
<div className="flex items-center gap-2 justify-between w-full">
|
||||||
<form>
|
<span>{value}</span>
|
||||||
<ModalHeader
|
{canEdit && (
|
||||||
title={"Edit Peer IP Address"}
|
<button
|
||||||
description={"Update the NetBird IP address for this peer."}
|
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"
|
||||||
color={"blue"}
|
onClick={(e) => {
|
||||||
/>
|
e.stopPropagation();
|
||||||
|
onEdit();
|
||||||
<div className={"p-default flex flex-col gap-4"}>
|
}}
|
||||||
<div>
|
>
|
||||||
<Input
|
<PencilIcon size={14} />
|
||||||
placeholder={"e.g., 100.64.0.15"}
|
</button>
|
||||||
value={ip}
|
)}
|
||||||
onChange={(e) => setIP(e.target.value)}
|
</div>
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
} from "@utils/version";
|
} from "@utils/version";
|
||||||
|
|
||||||
export default function SSHPage() {
|
export default function SSHPage() {
|
||||||
const { peerId, username, port } = useSSHQueryParams();
|
const { peerId, username, port, ipVersion } = useSSHQueryParams();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: peer,
|
data: peer,
|
||||||
@@ -48,6 +48,7 @@ export default function SSHPage() {
|
|||||||
peer={peer}
|
peer={peer}
|
||||||
username={username}
|
username={username}
|
||||||
port={port}
|
port={port}
|
||||||
|
ipVersion={ipVersion}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<LoadingMessage message={"Starting ssh session..."} />
|
<LoadingMessage message={"Starting ssh session..."} />
|
||||||
@@ -60,9 +61,10 @@ type Props = {
|
|||||||
username: string;
|
username: string;
|
||||||
port: string;
|
port: string;
|
||||||
peer: Peer;
|
peer: Peer;
|
||||||
|
ipVersion: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function SSHTerminal({ username, port, peer }: Props) {
|
function SSHTerminal({ username, port, peer, ipVersion }: Props) {
|
||||||
const client = useNetBirdClient();
|
const client = useNetBirdClient();
|
||||||
const connected = useRef(false);
|
const connected = useRef(false);
|
||||||
const sshConnectedOnce = useRef(false);
|
const sshConnectedOnce = useRef(false);
|
||||||
@@ -81,9 +83,12 @@ function SSHTerminal({ username, port, peer }: Props) {
|
|||||||
const isClientDisconnected = client.status === NetBirdStatus.DISCONNECTED;
|
const isClientDisconnected = client.status === NetBirdStatus.DISCONNECTED;
|
||||||
const isClientConnecting = client.status === NetBirdStatus.CONNECTING;
|
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(() => {
|
useEffect(() => {
|
||||||
document.title = `${username}@${peer.ip} - ${peer.hostname}`;
|
document.title = `${username}@${sshHost} - ${peer.hostname}`;
|
||||||
}, [username, peer, client]);
|
}, [username, peer, client, sshHost]);
|
||||||
|
|
||||||
const handleReconnect = async () => {
|
const handleReconnect = async () => {
|
||||||
if (!peer?.id) return;
|
if (!peer?.id) return;
|
||||||
@@ -97,9 +102,10 @@ function SSHTerminal({ username, port, peer }: Props) {
|
|||||||
const rules = [`${protocol}/${aclPort}`];
|
const rules = [`${protocol}/${aclPort}`];
|
||||||
await client?.connectTemporary(peer.id, rules);
|
await client?.connectTemporary(peer.id, rules);
|
||||||
await ssh({
|
await ssh({
|
||||||
hostname: peer.ip,
|
hostname: sshHost,
|
||||||
port: Number(port),
|
port: Number(port),
|
||||||
username,
|
username,
|
||||||
|
ipVersion: ipVersion || undefined,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Reconnection failed:", error);
|
console.error("Reconnection failed:", error);
|
||||||
@@ -123,9 +129,10 @@ function SSHTerminal({ username, port, peer }: Props) {
|
|||||||
const rules = [`${protocol}/${aclPort}`];
|
const rules = [`${protocol}/${aclPort}`];
|
||||||
await client?.connectTemporary(peer.id, rules);
|
await client?.connectTemporary(peer.id, rules);
|
||||||
const res = await ssh({
|
const res = await ssh({
|
||||||
hostname: peer.ip,
|
hostname: sshHost,
|
||||||
port: Number(port),
|
port: Number(port),
|
||||||
username,
|
username,
|
||||||
|
ipVersion: ipVersion || undefined,
|
||||||
});
|
});
|
||||||
if (res === SSHStatus.CONNECTED) {
|
if (res === SSHStatus.CONNECTED) {
|
||||||
sshConnectedOnce.current = true;
|
sshConnectedOnce.current = true;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const variants = cva("", {
|
|||||||
purple: ["bg-purple-950/50 border-purple-500 border text-purple-500"],
|
purple: ["bg-purple-950/50 border-purple-500 border text-purple-500"],
|
||||||
yellow: ["bg-yellow-950 border-yellow-500 border text-yellow-400"],
|
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"],
|
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: [
|
grayer: [
|
||||||
"bg-nb-gray-900/40 border-nb-gray-800/40 text-nb-gray-300 border",
|
"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"],
|
"blue-darker": ["hover:bg-sky-800"],
|
||||||
red: ["hover:bg-red-950/40"],
|
red: ["hover:bg-red-950/40"],
|
||||||
gray: ["hover:bg-nb-gray-900"],
|
gray: ["hover:bg-nb-gray-900"],
|
||||||
|
lightGray: ["hover:bg-nb-gray-900"],
|
||||||
grayer: ["hover:bg-nb-gray-900"],
|
grayer: ["hover:bg-nb-gray-900"],
|
||||||
"gray-ghost": ["hover:bg-nb-gray-800 cursor-pointer"],
|
"gray-ghost": ["hover:bg-nb-gray-800 cursor-pointer"],
|
||||||
green: ["hover:bg-green-950/50"],
|
green: ["hover:bg-green-950/50"],
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export const buttonVariants = cva(
|
|||||||
"",
|
"",
|
||||||
],
|
],
|
||||||
"danger-text": [
|
"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": [
|
"default-outline": [
|
||||||
"dark:ring-offset-nb-gray-950/50 dark:focus:ring-nb-gray-500/20",
|
"dark:ring-offset-nb-gray-950/50 dark:focus:ring-nb-gray-500/20",
|
||||||
|
|||||||
@@ -28,8 +28,13 @@ export const DeviceCard = ({
|
|||||||
const descriptionText = useMemo(() => {
|
const descriptionText = useMemo(() => {
|
||||||
return description !== undefined
|
return description !== undefined
|
||||||
? description
|
? description
|
||||||
: address || device?.ip || resource?.address;
|
: address ||
|
||||||
}, [description, address, device]);
|
(device?.ip
|
||||||
|
? device.ipv6
|
||||||
|
? `${device.ip}, ${device.ipv6}`
|
||||||
|
: device.ip
|
||||||
|
: resource?.address);
|
||||||
|
}, [description, address, device, resource]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import React from "react";
|
|||||||
export const fancyToggleSwitchVariants = cva([], {
|
export const fancyToggleSwitchVariants = cva([], {
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: ["px-6 py-4 border rounded-md"],
|
default: ["px-5 py-4 border rounded-md"],
|
||||||
blank: null,
|
blank: null,
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
@@ -45,6 +45,8 @@ interface Props extends FancyToggleSwitchVariants {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
dataCy?: string;
|
dataCy?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
labelClassName?: string;
|
||||||
|
textWrapperClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FancyToggleSwitch({
|
export default function FancyToggleSwitch({
|
||||||
@@ -57,6 +59,8 @@ export default function FancyToggleSwitch({
|
|||||||
dataCy,
|
dataCy,
|
||||||
className,
|
className,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
|
labelClassName,
|
||||||
|
textWrapperClassName = "max-w-sm",
|
||||||
}: Readonly<Props>) {
|
}: Readonly<Props>) {
|
||||||
const handleToggle = () => {
|
const handleToggle = () => {
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
@@ -87,8 +91,8 @@ export default function FancyToggleSwitch({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={"flex justify-between gap-10"}>
|
<div className={"flex justify-between gap-10"}>
|
||||||
<div className={"max-w-sm"}>
|
<div className={cn(textWrapperClassName)}>
|
||||||
<Label>{label}</Label>
|
<Label className={labelClassName}>{label}</Label>
|
||||||
<HelpText margin={false}>{helpText}</HelpText>
|
<HelpText margin={false}>{helpText}</HelpText>
|
||||||
</div>
|
</div>
|
||||||
<div className={"mt-2 pr-1"}>
|
<div className={"mt-2 pr-1"}>
|
||||||
|
|||||||
@@ -968,7 +968,8 @@ const ResourcesList = ({
|
|||||||
const peersSearchPredicate = (item: Peer, query: string) => {
|
const peersSearchPredicate = (item: Peer, query: string) => {
|
||||||
const lowerCaseQuery = query.toLowerCase();
|
const lowerCaseQuery = query.toLowerCase();
|
||||||
if (item.name.toLowerCase().includes(lowerCaseQuery)) return true;
|
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 = ({
|
const PeersList = ({
|
||||||
@@ -1059,6 +1060,7 @@ const PeersList = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{res.ip}
|
{res.ip}
|
||||||
|
{res.ipv6 && `, ${res.ipv6}`}
|
||||||
<RadioItem value={res.id} />
|
<RadioItem value={res.id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ const searchPredicate = (item: Peer, query: string) => {
|
|||||||
const lowerCaseQuery = query.toLowerCase();
|
const lowerCaseQuery = query.toLowerCase();
|
||||||
if (item.name.toLowerCase().includes(lowerCaseQuery)) return true;
|
if (item.name.toLowerCase().includes(lowerCaseQuery)) return true;
|
||||||
if (item.hostname.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({
|
export function PeerSelector({
|
||||||
@@ -126,6 +127,7 @@ export function PeerSelector({
|
|||||||
>
|
>
|
||||||
<MapPinIcon />
|
<MapPinIcon />
|
||||||
{value.ip}
|
{value.ip}
|
||||||
|
{value.ipv6 && `, ${value.ipv6}`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -240,6 +242,7 @@ export function PeerSelector({
|
|||||||
>
|
>
|
||||||
<MapPinIcon />
|
<MapPinIcon />
|
||||||
{option.ip}
|
{option.ip}
|
||||||
|
{option.ipv6 && `, ${option.ipv6}`}
|
||||||
</div>
|
</div>
|
||||||
</FullTooltip>
|
</FullTooltip>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ const SelectItem = React.forwardRef<
|
|||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
</SelectPrimitive.ItemIndicator>
|
</SelectPrimitive.ItemIndicator>
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2">
|
||||||
<span className="flex-shrink-0">{icon}</span>
|
<span className="flex-shrink-0">{icon}</span>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ interface SelectDropdownProps {
|
|||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
triggerClassName?: string;
|
triggerClassName?: string;
|
||||||
|
iconSize?: number;
|
||||||
|
truncate?: boolean;
|
||||||
|
compact?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SelectDropdown({
|
export function SelectDropdown({
|
||||||
@@ -68,6 +71,9 @@ export function SelectDropdown({
|
|||||||
children,
|
children,
|
||||||
maxHeight,
|
maxHeight,
|
||||||
triggerClassName,
|
triggerClassName,
|
||||||
|
iconSize = 14,
|
||||||
|
truncate = false,
|
||||||
|
compact = false,
|
||||||
}: Readonly<SelectDropdownProps>) {
|
}: Readonly<SelectDropdownProps>) {
|
||||||
const [inputRef, { width }] = useElementSize<HTMLButtonElement>();
|
const [inputRef, { width }] = useElementSize<HTMLButtonElement>();
|
||||||
|
|
||||||
@@ -107,15 +113,18 @@ export function SelectDropdown({
|
|||||||
|
|
||||||
const SelectedItem = () => {
|
const SelectedItem = () => {
|
||||||
return (
|
return (
|
||||||
<div className={"flex items-center gap-2.5"}>
|
<div className={cn("flex items-center gap-2.5", truncate && "min-w-0")}>
|
||||||
{selected?.icon && <selected.icon size={14} width={14} />}
|
{selected?.icon && <selected.icon size={iconSize} width={iconSize} />}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col text-sm font-medium",
|
"flex flex-col text-sm font-medium",
|
||||||
size === "xs" && "text-xs",
|
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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -216,20 +225,22 @@ export function SelectDropdown({
|
|||||||
|
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
className={cn(
|
className={cn(
|
||||||
"overflow-y-auto flex flex-col gap-1 pl-2 pr-3",
|
"overflow-y-auto flex flex-col gap-1",
|
||||||
!showSearch && "pt-2",
|
compact ? "pl-1 pr-1" : "pl-2 pr-3",
|
||||||
|
!showSearch && (compact ? "pt-1" : "pt-2"),
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
maxHeight: maxHeight ?? 380,
|
maxHeight: maxHeight ?? 380,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CommandGroup>
|
<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) => (
|
{filteredItems.map((option) => (
|
||||||
<SelectDropdownItem
|
<SelectDropdownItem
|
||||||
option={option}
|
option={option}
|
||||||
toggle={toggle}
|
toggle={toggle}
|
||||||
key={option.value}
|
key={option.value}
|
||||||
|
iconSize={iconSize}
|
||||||
showValue={showValues}
|
showValue={showValues}
|
||||||
size={size}
|
size={size}
|
||||||
/>
|
/>
|
||||||
@@ -249,11 +260,13 @@ const SelectDropdownItem = ({
|
|||||||
toggle,
|
toggle,
|
||||||
showValue = false,
|
showValue = false,
|
||||||
size = "sm",
|
size = "sm",
|
||||||
|
iconSize = 14,
|
||||||
}: {
|
}: {
|
||||||
option: SelectOption;
|
option: SelectOption;
|
||||||
toggle: (value: string) => void;
|
toggle: (value: string) => void;
|
||||||
showValue?: boolean;
|
showValue?: boolean;
|
||||||
size: "xs" | "sm";
|
size: "xs" | "sm";
|
||||||
|
iconSize?: number;
|
||||||
}) => {
|
}) => {
|
||||||
const value = option.value || "" + option.label || "";
|
const value = option.value || "" + option.label || "";
|
||||||
const elementRef = useRef<HTMLDivElement>(null);
|
const elementRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -285,7 +298,12 @@ const SelectDropdownItem = ({
|
|||||||
option?.disabled && "cursor-not-allowed",
|
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 && option.renderItem()}
|
||||||
{!option?.renderItem && (
|
{!option?.renderItem && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ declare module "@tanstack/table-core" {
|
|||||||
}
|
}
|
||||||
interface SortingFns {
|
interface SortingFns {
|
||||||
checkbox: SortingFn<unknown>;
|
checkbox: SortingFn<unknown>;
|
||||||
|
datetime: SortingFn<unknown>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +100,15 @@ const arrIncludesSomeExact: FilterFn<any> = (
|
|||||||
return value.some((val) => val === rowValue);
|
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 checkboxSort: SortingFn<any> = (rowA, rowB, columnId) => {
|
||||||
const valueA =
|
const valueA =
|
||||||
columnId === "select" ? rowA.getIsSelected() : rowA.getValue(columnId);
|
columnId === "select" ? rowA.getIsSelected() : rowA.getValue(columnId);
|
||||||
@@ -324,6 +334,7 @@ export function DataTable<TData, TValue>({
|
|||||||
},
|
},
|
||||||
sortingFns: {
|
sortingFns: {
|
||||||
checkbox: checkboxSort,
|
checkbox: checkboxSort,
|
||||||
|
datetime: datetimeSort,
|
||||||
},
|
},
|
||||||
getRowId: useRowId ? (row) => row.id : undefined,
|
getRowId: useRowId ? (row) => row.id : undefined,
|
||||||
onRowSelectionChange: setRowSelection,
|
onRowSelectionChange: setRowSelection,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type Props = {
|
|||||||
center?: boolean;
|
center?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
sorting?: boolean;
|
sorting?: boolean;
|
||||||
|
onSort?: () => void;
|
||||||
name?: string;
|
name?: string;
|
||||||
};
|
};
|
||||||
export default function DataTableHeader({
|
export default function DataTableHeader({
|
||||||
@@ -23,14 +24,20 @@ export default function DataTableHeader({
|
|||||||
center,
|
center,
|
||||||
className,
|
className,
|
||||||
sorting = true,
|
sorting = true,
|
||||||
|
onSort,
|
||||||
name,
|
name,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const serverPagination = useOptionalServerPagination();
|
const serverPagination = useOptionalServerPagination();
|
||||||
|
|
||||||
const handleSort = () => {
|
const handleSort = () => {
|
||||||
const direction = column.getIsSorted() === "asc" ? "desc" : "asc";
|
if (onSort) {
|
||||||
column.toggleSorting(direction === "desc");
|
onSort();
|
||||||
|
} else {
|
||||||
|
const direction = column.getIsSorted() === "asc" ? "desc" : "asc";
|
||||||
|
column.toggleSorting(direction === "desc");
|
||||||
|
}
|
||||||
if (name && serverPagination?.setSort) {
|
if (name && serverPagination?.setSort) {
|
||||||
|
const direction = column.getIsSorted() === "asc" ? "desc" : "asc";
|
||||||
serverPagination.setSort(name, direction);
|
serverPagination.setSort(name, direction);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,8 +9,11 @@ import { useCountries } from "@/contexts/CountryProvider";
|
|||||||
type Props = {
|
type Props = {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
|
iconSize?: number;
|
||||||
|
popoverWidth?: "auto" | "content" | number;
|
||||||
|
truncate?: boolean;
|
||||||
};
|
};
|
||||||
export const CountrySelector = ({ value, onChange }: Props) => {
|
export const CountrySelector = ({ value, onChange, iconSize = 20, popoverWidth, truncate }: Props) => {
|
||||||
const { countries, isLoading } = useCountries();
|
const { countries, isLoading } = useCountries();
|
||||||
|
|
||||||
const countryList = useMemo(() => {
|
const countryList = useMemo(() => {
|
||||||
@@ -22,7 +25,7 @@ export const CountrySelector = ({ value, onChange }: Props) => {
|
|||||||
}) =>
|
}) =>
|
||||||
createElement(RoundedFlag, {
|
createElement(RoundedFlag, {
|
||||||
country: country.country_code,
|
country: country.country_code,
|
||||||
size: 20,
|
size: iconSize,
|
||||||
...props,
|
...props,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@@ -42,7 +45,10 @@ export const CountrySelector = ({ value, onChange }: Props) => {
|
|||||||
searchPlaceholder={"Search country..."}
|
searchPlaceholder={"Search country..."}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
iconSize={iconSize}
|
||||||
options={countryList || []}
|
options={countryList || []}
|
||||||
|
popoverWidth={popoverWidth}
|
||||||
|
truncate={truncate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ const CountryContext = React.createContext(
|
|||||||
countries: Country[] | undefined;
|
countries: Country[] | undefined;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
getRegionByPeer: (peer: Peer) => string;
|
getRegionByPeer: (peer: Peer) => string;
|
||||||
getRegionText: (country_code: string, city_name: string) => string;
|
getRegionText: (
|
||||||
|
country_code: string,
|
||||||
|
city_name: string,
|
||||||
|
subdivision_code?: string,
|
||||||
|
) => string;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -21,7 +25,11 @@ export default function CountryProvider({ children }: Props) {
|
|||||||
const { isRestricted } = usePermissions();
|
const { isRestricted } = usePermissions();
|
||||||
|
|
||||||
const getRegionByPeer = (peer: Peer) => "Unknown";
|
const getRegionByPeer = (peer: Peer) => "Unknown";
|
||||||
const getRegionText = (country_code: string, city_name: string) => "Unknown";
|
const getRegionText = (
|
||||||
|
country_code: string,
|
||||||
|
city_name: string,
|
||||||
|
subdivision_code?: string,
|
||||||
|
) => "Unknown";
|
||||||
|
|
||||||
return isRestricted ? (
|
return isRestricted ? (
|
||||||
<CountryContext.Provider
|
<CountryContext.Provider
|
||||||
@@ -47,12 +55,14 @@ function CountryProviderContent({ children }: Props) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const getRegionText = useCallback(
|
const getRegionText = useCallback(
|
||||||
(country_code: string, city_name: string) => {
|
(country_code: string, city_name: string, subdivision_code?: string) => {
|
||||||
if (!countries) return "Unknown";
|
if (!countries) return "Unknown";
|
||||||
const country = countries.find((c) => c.country_code === country_code);
|
const country = countries.find((c) => c.country_code === country_code);
|
||||||
if (!country) return "Unknown";
|
if (!country) return "Unknown";
|
||||||
if (!city_name) return country.country_name;
|
const parts = [country.country_name];
|
||||||
return `${country.country_name}, ${city_name}`;
|
if (subdivision_code) parts.push(subdivision_code);
|
||||||
|
if (city_name) parts.push(city_name);
|
||||||
|
return parts.join(", ");
|
||||||
},
|
},
|
||||||
[countries],
|
[countries],
|
||||||
);
|
);
|
||||||
|
|||||||
Loaded 20 of 85 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user