i18n: localize Peers, Activity, Setup Keys modules
- PeerActionCell.tsx: replace ~20 hardcoded strings with t() calls (approve, bypass compliance, session expiration, SSH, delete actions) - Activity module: internationalize 5 files including ActivityDescription.tsx with ~104 activity event description templates using t.rich() - Setup Keys: replace ~18 hardcoded strings in SetupKeyActionCell and SetupKeyGroupsCell with translation keys - Add Chinese translations with natural grammar and consistent terminology
This commit is contained in:
136
package-lock.json
generated
136
package-lock.json
generated
@@ -32,7 +32,7 @@
|
|||||||
"@tanstack/react-table": "^8.10.7",
|
"@tanstack/react-table": "^8.10.7",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"@types/lodash": "^4.14.200",
|
"@types/lodash": "4.17.24",
|
||||||
"@types/node": "20.10.6",
|
"@types/node": "20.10.6",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
@@ -47,6 +47,7 @@
|
|||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
@@ -55,16 +56,18 @@
|
|||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||||
"framer-motion": "^12.29.2",
|
"framer-motion": "^12.29.2",
|
||||||
"ip-address": "^10.1.0",
|
"ip-address": "^10.2.0",
|
||||||
"ip-cidr": "^3.1.0",
|
"ip-cidr": "^3.1.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.7",
|
||||||
"lodash": "^4.17.23",
|
"lodash": "4.18.1",
|
||||||
"lucide-react": "^0.566.0",
|
"lucide-react": "^0.566.0",
|
||||||
"next": "16.1.7",
|
"next": "16.1.7",
|
||||||
"next-intl": "^4.13.0",
|
"next-intl": "^4.13.0",
|
||||||
"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",
|
||||||
|
"react-chartjs-2": "^5.3.0",
|
||||||
|
"react-confetti-explosion": "^3.0.3",
|
||||||
"react-day-picker": "^9.13.0",
|
"react-day-picker": "^9.13.0",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-ga4": "^2.1.0",
|
"react-ga4": "^2.1.0",
|
||||||
@@ -84,6 +87,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "^9.5.1",
|
"@faker-js/faker": "^9.5.1",
|
||||||
|
"@playwright/test": "^1.52.0",
|
||||||
"@types/chroma-js": "^3.1.1",
|
"@types/chroma-js": "^3.1.1",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
@@ -1738,6 +1742,22 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.61.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/number": {
|
"node_modules/@radix-ui/number": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||||
@@ -3562,9 +3582,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/lodash": {
|
"node_modules/@types/lodash": {
|
||||||
"version": "4.17.23",
|
"version": "4.17.24",
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz",
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
|
||||||
"integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==",
|
"integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
@@ -4850,6 +4870,24 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cross-env": {
|
||||||
|
"version": "7.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
||||||
|
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cross-spawn": "^7.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"cross-env": "src/bin/cross-env.js",
|
||||||
|
"cross-env-shell": "src/bin/cross-env-shell.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.14",
|
||||||
|
"npm": ">=6",
|
||||||
|
"yarn": ">=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -6760,9 +6798,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ip-address": {
|
"node_modules/ip-address": {
|
||||||
"version": "10.1.0",
|
"version": "10.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
@@ -7258,13 +7296,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/js-cookie": {
|
"node_modules/js-cookie": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz",
|
||||||
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
"integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
@@ -8084,6 +8119,53 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.61.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/po-parser": {
|
"node_modules/po-parser": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz",
|
||||||
@@ -8330,6 +8412,26 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-chartjs-2": {
|
||||||
|
"version": "5.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz",
|
||||||
|
"integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"chart.js": "^4.1.1",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-confetti-explosion": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-confetti-explosion/-/react-confetti-explosion-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-ow5ns/1ttzXsIlbbfJmWJNiyQK8lTHBL6lRSUXGaK44K/3NIMngR57Ja96l+D6txTeFhfe0BfXGvORMxhtRDng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-day-picker": {
|
"node_modules/react-day-picker": {
|
||||||
"version": "9.13.0",
|
"version": "9.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.13.0.tgz",
|
||||||
|
|||||||
@@ -472,6 +472,25 @@ export default {
|
|||||||
"Access other networks without installing NetBird on every resource.",
|
"Access other networks without installing NetBird on every resource.",
|
||||||
remoteJobsDesc:
|
remoteJobsDesc:
|
||||||
"Remotely trigger actions such as debug bundles or other tasks on this peer, without requiring CLI access.",
|
"Remotely trigger actions such as debug bundles or other tasks on this peer, without requiring CLI access.",
|
||||||
|
revoke: "Revoke",
|
||||||
|
bypassCompliance: "Bypass Compliance",
|
||||||
|
bypassComplianceConfirmTitle: "Bypass compliance for '{name}'?",
|
||||||
|
bypassComplianceConfirmDescription:
|
||||||
|
"This will override the compliance check and allow this peer to connect. The bypass will be automatically removed if the device becomes compliant.",
|
||||||
|
bypassComplianceSuccess: "Compliance bypassed for {name}",
|
||||||
|
bypassComplianceSuccessDescription:
|
||||||
|
"This peer can now connect to other peers.",
|
||||||
|
bypassComplianceLoading: "Bypassing compliance...",
|
||||||
|
revokeBypass: "Revoke Bypass",
|
||||||
|
revokeBypassConfirmTitle: "Revoke compliance bypass for '{name}'?",
|
||||||
|
revokeBypassConfirmDescription:
|
||||||
|
"This peer will be subject to normal compliance validation. If still non-compliant, it will lose network access.",
|
||||||
|
revokeBypassSuccess: "Compliance bypass revoked",
|
||||||
|
revokeBypassSuccessDescription:
|
||||||
|
"Peer {name} is now subject to normal compliance validation.",
|
||||||
|
revokeBypassLoading: "Revoking compliance bypass...",
|
||||||
|
bypassTooltip:
|
||||||
|
"Bypass {integrationName} compliance check and allow this peer to connect. The bypass is automatically removed when the device becomes compliant.",
|
||||||
},
|
},
|
||||||
policies: {
|
policies: {
|
||||||
title: "Policies",
|
title: "Policies",
|
||||||
@@ -1552,6 +1571,25 @@ export default {
|
|||||||
groups: "Groups",
|
groups: "Groups",
|
||||||
usage: "Usage",
|
usage: "Usage",
|
||||||
lastUsedOn: "Last used on",
|
lastUsedOn: "Last used on",
|
||||||
|
// SetupKeyActionCell
|
||||||
|
revoke: "Revoke",
|
||||||
|
openActionsMenu: "Open actions menu",
|
||||||
|
revokeConfirmTitle: "Revoke '{name}'?",
|
||||||
|
revokeConfirmDescription:
|
||||||
|
"Are you sure you want to revoke the setup key? This action cannot be undone.",
|
||||||
|
revokeSuccessDescription: "Setup key was successfully revoked",
|
||||||
|
revokeLoading: "Revoking the setup key...",
|
||||||
|
deleteConfirmTitle: "Delete '{name}'?",
|
||||||
|
deleteConfirmDescription:
|
||||||
|
"Are you sure you want to delete the setup key? This action cannot be undone.",
|
||||||
|
deleteSuccessDescription: "Setup key was successfully deleted",
|
||||||
|
deleteLoading: "Deleting the setup key...",
|
||||||
|
// SetupKeyGroupsCell
|
||||||
|
autoAssignedGroups: "Auto-assigned Groups",
|
||||||
|
autoAssignedGroupsDescription:
|
||||||
|
"These groups will be automatically assigned to peers enrolled with this key",
|
||||||
|
groupsSavedDescription: "Groups of the setup key were successfully saved",
|
||||||
|
groupsSaving: "Saving the groups of the setup key...",
|
||||||
},
|
},
|
||||||
activity: {
|
activity: {
|
||||||
title: "Activity",
|
title: "Activity",
|
||||||
@@ -1569,6 +1607,281 @@ export default {
|
|||||||
ipAddress: "IP Address",
|
ipAddress: "IP Address",
|
||||||
details: "Details",
|
details: "Details",
|
||||||
code: "Code",
|
code: "Code",
|
||||||
|
|
||||||
|
// UI strings used across activity components
|
||||||
|
allEventTypes: "All Event Types",
|
||||||
|
allUsers: "All Users",
|
||||||
|
includeAllUsers: "Include all users",
|
||||||
|
searchEvent: "Search event...",
|
||||||
|
searchUser: "Search user...",
|
||||||
|
noUsersAvailable: "No users available to select.",
|
||||||
|
noUsersMatching: "There are no users matching your search.",
|
||||||
|
system: "System",
|
||||||
|
external: "External",
|
||||||
|
serviceUser: "Service User",
|
||||||
|
typeCount: "{count} types",
|
||||||
|
eventCount: "{count} Event(s)",
|
||||||
|
activityCode: "Activity Code",
|
||||||
|
meta: "Meta",
|
||||||
|
from: "from",
|
||||||
|
unknown: "Unknown",
|
||||||
|
|
||||||
|
// Activity event descriptions (used by ActivityDescription.tsx)
|
||||||
|
|
||||||
|
// Setup Key
|
||||||
|
desc_setupkey_revoke:
|
||||||
|
"Setup-Key <Value>{name}</Value> with key <Value>{key}</Value> was revoked",
|
||||||
|
desc_setupkey_delete:
|
||||||
|
"Setup-Key <Value>{name}</Value> with key <Value>{key}</Value> was deleted",
|
||||||
|
desc_setupkey_add:
|
||||||
|
"Setup-Key <Value>{name}</Value> with key <Value>{key}</Value> was created",
|
||||||
|
desc_peer_setupkey_add:
|
||||||
|
"Peer <Value>{name}</Value> from <peerConnectionInfo></peerConnectionInfo> was added with the NetBird IP <Value>{ip}</Value> using the setup key <Value>{setup_key_name}</Value>",
|
||||||
|
desc_setupkey_group_delete:
|
||||||
|
"Group <Value>{group}</Value> was removed from the <Value>{setupkey}</Value> setup key",
|
||||||
|
desc_setupkey_group_add:
|
||||||
|
"Group <Value>{group}</Value> was added to the <Value>{setupkey}</Value> setup key",
|
||||||
|
|
||||||
|
// Dashboard
|
||||||
|
desc_dashboard_login:
|
||||||
|
"<Value>{username}</Value> logged in to the dashboard",
|
||||||
|
|
||||||
|
// Policy
|
||||||
|
desc_policy_update: "Policy <Value>{name}</Value> has been updated",
|
||||||
|
desc_policy_delete: "Policy <Value>{name}</Value> was deleted",
|
||||||
|
desc_policy_add: "Policy <Value>{name}</Value> was created",
|
||||||
|
|
||||||
|
// Route
|
||||||
|
desc_route_delete_domains:
|
||||||
|
"Route <Value>{name}</Value> with the domain(s) <Value>{domains}</Value> was deleted",
|
||||||
|
desc_route_delete_range:
|
||||||
|
"Route <Value>{name}</Value> with the range <Value>{network_range}</Value> was deleted",
|
||||||
|
desc_route_update_domains:
|
||||||
|
"Route <Value>{name}</Value> with the domain(s) <Value>{domains}</Value> was updated",
|
||||||
|
desc_route_update_range:
|
||||||
|
"Route <Value>{name}</Value> with the range <Value>{network_range}</Value> was updated",
|
||||||
|
desc_route_add_domains:
|
||||||
|
"Route <Value>{name}</Value> with the domain(s) <Value>{domains}</Value> was created",
|
||||||
|
desc_route_add_range:
|
||||||
|
"Route <Value>{name}</Value> with the range <Value>{network_range}</Value> was created",
|
||||||
|
|
||||||
|
// User / Peer
|
||||||
|
desc_user_peer_delete:
|
||||||
|
"Peer <Value>{name}</Value> from <peerConnectionInfo></peerConnectionInfo> with NetBird IP <Value>{ip}</Value> was deleted",
|
||||||
|
desc_user_peer_add:
|
||||||
|
"Peer <Value>{name}</Value> from <peerConnectionInfo></peerConnectionInfo> was added with the NetBird IP <Value>{ip}</Value>",
|
||||||
|
desc_user_peer_update:
|
||||||
|
"Peer <Value>{name}</Value> from <peerConnectionInfo></peerConnectionInfo> with NetBird IP <Value>{ip}</Value> was updated",
|
||||||
|
desc_user_join: "User <Value>{username}</Value> joined NetBird",
|
||||||
|
desc_user_invite:
|
||||||
|
"<Value>{username}</Value> <Value>{email}</Value> was invited.",
|
||||||
|
desc_user_create:
|
||||||
|
"<Value>{username}</Value> <Value>{email}</Value> was created by <Value>{initiator}</Value>",
|
||||||
|
desc_user_group_add:
|
||||||
|
"Group <Value>{group}</Value> was added to user <Value>{username}</Value>",
|
||||||
|
desc_user_block:
|
||||||
|
"User <Value>{username}</Value> <Value>{email}</Value> was blocked",
|
||||||
|
desc_user_unblock:
|
||||||
|
"User <Value>{username}</Value> <Value>{email}</Value> was unblocked",
|
||||||
|
desc_user_delete:
|
||||||
|
"User <Value>{username}</Value> <Value>{email}</Value> was deleted",
|
||||||
|
desc_user_group_delete:
|
||||||
|
"Group <Value>{group}</Value> was removed from user <Value>{username}</Value> <Value>{email}</Value>",
|
||||||
|
desc_user_role_update:
|
||||||
|
"Role <Value>{role}</Value> was updated of user <Value>{username}</Value> <Value>{email}</Value>",
|
||||||
|
desc_user_approve:
|
||||||
|
"User <Value>{username}</Value> <Value>{email}</Value> was approved",
|
||||||
|
desc_user_reject:
|
||||||
|
"User <Value>{username}</Value> <Value>{email}</Value> was rejected",
|
||||||
|
desc_user_password_change:
|
||||||
|
"Password was changed for user <Value>{username}</Value> <Value>{email}</Value>",
|
||||||
|
|
||||||
|
// Invite Link
|
||||||
|
desc_user_invite_link_create:
|
||||||
|
"Invite link was created for <Value>{username}</Value> <Value>{email}</Value>",
|
||||||
|
desc_user_invite_link_accept:
|
||||||
|
"Invite link was accepted by <Value>{username}</Value> <Value>{email}</Value>",
|
||||||
|
desc_user_invite_link_regenerate:
|
||||||
|
"Invite link was regenerated for <Value>{username}</Value> <Value>{email}</Value>",
|
||||||
|
desc_user_invite_link_delete:
|
||||||
|
"Invite link was deleted for <Value>{username}</Value> <Value>{email}</Value>",
|
||||||
|
|
||||||
|
// Service User
|
||||||
|
desc_service_user_create: "Service user <Value>{name}</Value> was created",
|
||||||
|
desc_service_user_delete: "Service user <Value>{name}</Value> was deleted",
|
||||||
|
|
||||||
|
// Peer
|
||||||
|
desc_peer_group_delete:
|
||||||
|
"Group <Value>{group}</Value> was removed from the peer with the NetBird IP <Value>{peer_ip}</Value>",
|
||||||
|
desc_peer_group_add:
|
||||||
|
"Group <Value>{group}</Value> was added to the peer with the NetBird IP <Value>{peer_ip}</Value>",
|
||||||
|
desc_peer_login_expire:
|
||||||
|
"Login of the peer <Value>{name}</Value> is expired",
|
||||||
|
desc_peer_ssh_disable:
|
||||||
|
"SSH Server of peer <Value>{name}</Value> was disabled",
|
||||||
|
desc_peer_ssh_enable:
|
||||||
|
"SSH Server of peer <Value>{name}</Value> was enabled",
|
||||||
|
desc_peer_login_expiration_disable:
|
||||||
|
"Login expiration of peer <Value>{name}</Value> was disabled",
|
||||||
|
desc_peer_login_expiration_enable:
|
||||||
|
"Login expiration of peer <Value>{name}</Value> was enabled",
|
||||||
|
desc_peer_rename:
|
||||||
|
"Peer with the NetBird IP <Value>{ip}</Value> was renamed to <Value>{name}</Value>",
|
||||||
|
desc_peer_approve:
|
||||||
|
"Peer with the NetBird IP <Value>{ip}</Value> was approved",
|
||||||
|
desc_peer_ip_update:
|
||||||
|
"Peer <Value>{name}</Value> IP address was updated from <Value>{old_ip}</Value> to <Value>{ip}</Value>",
|
||||||
|
desc_peer_user_add:
|
||||||
|
"Peer <Value>{name}</Value> from <peerConnectionInfo></peerConnectionInfo> was added with the NetBird IP <Value>{ip}</Value>",
|
||||||
|
|
||||||
|
// Group
|
||||||
|
desc_group_add: "Group <Value>{name}</Value> was created",
|
||||||
|
desc_group_delete: "Group <Value>{name}</Value> was deleted",
|
||||||
|
desc_group_update:
|
||||||
|
"Group <Value>{old_name}</Value> was renamed to <Value>{new_name}</Value>",
|
||||||
|
|
||||||
|
// Account
|
||||||
|
desc_account_create: "<Value>{initiator}</Value> created an account",
|
||||||
|
desc_account_setting_peer_login_expiration_update:
|
||||||
|
"Global login expiration was updated",
|
||||||
|
desc_account_setting_peer_login_expiration_enable:
|
||||||
|
"Global login expiration was enabled",
|
||||||
|
desc_account_setting_peer_login_expiration_disable:
|
||||||
|
"Global login expiration was disabled",
|
||||||
|
desc_account_network_range_update:
|
||||||
|
"Account network range was updated from <Value>{old_network_range}</Value> to <Value>{new_network_range}</Value>",
|
||||||
|
|
||||||
|
// Nameserver
|
||||||
|
desc_nameserver_group_add: "Nameserver <Value>{name}</Value> was added",
|
||||||
|
desc_nameserver_group_delete: "Nameserver <Value>{name}</Value> was deleted",
|
||||||
|
desc_nameserver_group_update: "Nameserver <Value>{name}</Value> was updated",
|
||||||
|
|
||||||
|
// Personal Access Token
|
||||||
|
desc_personal_access_token_create:
|
||||||
|
"Access token <Value>{name}</Value> for user <Value>{username}</Value> was created",
|
||||||
|
desc_personal_access_token_delete:
|
||||||
|
"Access token <Value>{name}</Value> for user <Value>{username}</Value> was deleted",
|
||||||
|
|
||||||
|
// Integration
|
||||||
|
desc_integration_create_platform:
|
||||||
|
"<Value>{platform}</Value> integration created",
|
||||||
|
desc_integration_create: "Integration created",
|
||||||
|
desc_integration_delete_platform:
|
||||||
|
"<Value>{platform}</Value> integration deleted",
|
||||||
|
desc_integration_delete: "Integration deleted",
|
||||||
|
desc_integration_update_platform:
|
||||||
|
"<Value>{platform}</Value> integration updated",
|
||||||
|
desc_integration_update: "Integration updated",
|
||||||
|
|
||||||
|
// DNS
|
||||||
|
desc_dns_setting_disabled_management_group_add:
|
||||||
|
"Group <Value>{group}</Value> was added to disabled DNS group setting",
|
||||||
|
desc_dns_setting_disabled_management_group_delete:
|
||||||
|
"Group <Value>{group}</Value> was removed from disabled DNS group setting",
|
||||||
|
|
||||||
|
// Posture Checks
|
||||||
|
desc_posture_check_updated:
|
||||||
|
"Posture check <Value>{name}</Value> was updated",
|
||||||
|
desc_posture_check_created:
|
||||||
|
"Posture check <Value>{name}</Value> was created",
|
||||||
|
desc_posture_check_deleted:
|
||||||
|
"Posture check <Value>{name}</Value> was deleted",
|
||||||
|
desc_transferred_owner_role: "Owner role was transferred",
|
||||||
|
|
||||||
|
// EDR / Integrated Validator
|
||||||
|
desc_integrated_validator_api_created:
|
||||||
|
"<Value>{platform}</Value> integration created",
|
||||||
|
desc_integrated_validator_api_updated:
|
||||||
|
"<Value>{platform}</Value> integration updated",
|
||||||
|
desc_integrated_validator_api_deleted:
|
||||||
|
"<Value>{platform}</Value> integration deleted",
|
||||||
|
desc_integrated_validator_host_check_approved:
|
||||||
|
"Peer approved by <Value>{platform}</Value> integration",
|
||||||
|
desc_integrated_validator_host_check_denied:
|
||||||
|
"Peer rejected by <Value>{platform}</Value> integration",
|
||||||
|
desc_integrated_validator_peer_compliance_bypassed:
|
||||||
|
"Peer <Value>{name}</Value> with the NetBird IP <Value>{ip}</Value> compliance bypassed for <Value>{platform}</Value> integration{original_reason}",
|
||||||
|
desc_integrated_validator_peer_compliance_bypass_revoked:
|
||||||
|
"Peer <Value>{name}</Value> with the NetBird IP <Value>{ip}</Value> compliance bypass revoked for <Value>{platform}</Value> integration",
|
||||||
|
desc_compliance_original_reason:
|
||||||
|
" (original non-compliant reason: <Value>{reason}</Value>)",
|
||||||
|
|
||||||
|
// Resource
|
||||||
|
desc_resource_group_add:
|
||||||
|
"Group <Value>{resource_name}</Value> added to resource <Value>{name}</Value>",
|
||||||
|
desc_resource_group_delete:
|
||||||
|
"Group <Value>{resource_name}</Value> removed from resource <Value>{name}</Value>",
|
||||||
|
|
||||||
|
// Reverse Proxy (peer expose)
|
||||||
|
desc_service_peer_expose:
|
||||||
|
"Peer <Value>{peer_name}</Value> exposed service <Value>{domain}</Value> with auth <Value>{auth}</Value>",
|
||||||
|
desc_service_peer_unexpose:
|
||||||
|
"Peer <Value>{peer_name}</Value> unexposed service <Value>{domain}</Value>",
|
||||||
|
desc_service_peer_expose_expire:
|
||||||
|
"Service <Value>{domain}</Value> exposed by peer <Value>{peer_name}</Value> was removed due to renewal expiration",
|
||||||
|
|
||||||
|
// Networks
|
||||||
|
desc_network_resource_create:
|
||||||
|
"Resource <Value>{name}</Value> created for network <Value>{network_name}</Value>",
|
||||||
|
desc_network_resource_update:
|
||||||
|
"Resource <Value>{name}</Value> updated for network <Value>{network_name}</Value>",
|
||||||
|
desc_network_resource_delete:
|
||||||
|
"Resource <Value>{name}</Value> deleted from network <Value>{network_name}</Value>",
|
||||||
|
desc_network_router_create:
|
||||||
|
"Routing peer created for network <Value>{network_name}</Value>",
|
||||||
|
desc_network_router_delete:
|
||||||
|
"Routing peer deleted from network <Value>{network_name}</Value>",
|
||||||
|
desc_network_router_update:
|
||||||
|
"Routing peer updated from network <Value>{network_name}</Value>",
|
||||||
|
desc_network_create:
|
||||||
|
"Network with name <Value>{name}</Value> created",
|
||||||
|
desc_network_delete:
|
||||||
|
"Network with name <Value>{name}</Value> deleted",
|
||||||
|
desc_network_update:
|
||||||
|
"Network with name <Value>{name}</Value> updated",
|
||||||
|
|
||||||
|
// Jobs
|
||||||
|
desc_peer_job_create:
|
||||||
|
"Remote job <Value>{job_type}</Value> created for peer <Value>{for_peer_name}</Value>",
|
||||||
|
|
||||||
|
// Flow Settings
|
||||||
|
desc_account_settings_extra_flow_group_remove:
|
||||||
|
"Limit traffic event group <Value>{group_name}</Value> removed",
|
||||||
|
desc_account_settings_extra_flow_group_add:
|
||||||
|
"Limit traffic event group <Value>{group_name}</Value> added",
|
||||||
|
|
||||||
|
// Identity Provider
|
||||||
|
desc_identityprovider_create:
|
||||||
|
"Identity provider <Value>{name}</Value> was created",
|
||||||
|
desc_identityprovider_update:
|
||||||
|
"Identity provider <Value>{name}</Value> was updated",
|
||||||
|
desc_identityprovider_delete:
|
||||||
|
"Identity provider <Value>{name}</Value> was deleted",
|
||||||
|
|
||||||
|
// Service (proxy cluster)
|
||||||
|
desc_service_create:
|
||||||
|
"Service <Value>{domain}</Value> in cluster <Value>{proxy_cluster}</Value> was created with authentication <Value>{auth}</Value>",
|
||||||
|
desc_service_update:
|
||||||
|
"Service <Value>{domain}</Value> in cluster <Value>{proxy_cluster}</Value> was updated with authentication <Value>{auth}</Value>",
|
||||||
|
desc_service_delete:
|
||||||
|
"Service <Value>{domain}</Value> in cluster <Value>{proxy_cluster}</Value> was deleted",
|
||||||
|
|
||||||
|
// Reseller / Distributor
|
||||||
|
desc_reseller_msp_created:
|
||||||
|
"Customer <Value>{msp_name}</Value> with domain <Value>{msp_domain}</Value> was created",
|
||||||
|
desc_reseller_activated: "Distributor account was activated",
|
||||||
|
desc_reseller_msp_deleted:
|
||||||
|
"Customer <Value>{msp_name}</Value> with domain <Value>{msp_domain}</Value> was deleted",
|
||||||
|
desc_reseller_msp_unlinked:
|
||||||
|
"Customer <Value>{msp_name}</Value> with domain <Value>{msp_domain}</Value> was unlinked",
|
||||||
|
desc_reseller_msp_invite_requested:
|
||||||
|
"Invite requested for customer <Value>{msp_name}</Value> with domain <Value>{msp_domain}</Value>",
|
||||||
|
desc_reseller_msp_invite_accepted:
|
||||||
|
"Invite accepted by customer <Value>{msp_name}</Value> with domain <Value>{msp_domain}</Value>",
|
||||||
|
desc_reseller_msp_invite_declined:
|
||||||
|
"Invite declined by customer <Value>{msp_name}</Value> with domain <Value>{msp_domain}</Value>",
|
||||||
|
desc_reseller_msp_updated:
|
||||||
|
"Customer <Value>{msp_name}</Value> with domain <Value>{msp_domain}</Value> was updated",
|
||||||
},
|
},
|
||||||
controlCenter: {
|
controlCenter: {
|
||||||
title: "Control Center",
|
title: "Control Center",
|
||||||
|
|||||||
@@ -456,6 +456,24 @@ export default {
|
|||||||
networkRoutesDesc: "无需在每个资源上安装 NetBird 即可访问其他网络。",
|
networkRoutesDesc: "无需在每个资源上安装 NetBird 即可访问其他网络。",
|
||||||
remoteJobsDesc:
|
remoteJobsDesc:
|
||||||
"远程触发此节点上的操作,如调试包或其他任务,无需 CLI 访问。",
|
"远程触发此节点上的操作,如调试包或其他任务,无需 CLI 访问。",
|
||||||
|
revoke: "撤销",
|
||||||
|
bypassCompliance: "绕过合规检查",
|
||||||
|
bypassComplianceConfirmTitle: "绕过节点 '{name}' 的合规检查?",
|
||||||
|
bypassComplianceConfirmDescription:
|
||||||
|
"此操作将覆盖合规检查,允许此节点进行连接。当设备恢复合规时,绕过将自动移除。",
|
||||||
|
bypassComplianceSuccess: "已为 {name} 绕过合规检查",
|
||||||
|
bypassComplianceSuccessDescription: "此节点现在可以连接到其他节点。",
|
||||||
|
bypassComplianceLoading: "正在绕过合规检查...",
|
||||||
|
revokeBypass: "撤销绕过",
|
||||||
|
revokeBypassConfirmTitle: "撤销节点 '{name}' 的合规绕过?",
|
||||||
|
revokeBypassConfirmDescription:
|
||||||
|
"此节点将接受正常的合规验证。如果仍然不合规,它将失去网络访问权限。",
|
||||||
|
revokeBypassSuccess: "合规绕过已撤销",
|
||||||
|
revokeBypassSuccessDescription:
|
||||||
|
"节点 {name} 现在接受正常的合规验证。",
|
||||||
|
revokeBypassLoading: "正在撤销合规绕过...",
|
||||||
|
bypassTooltip:
|
||||||
|
"绕过 {integrationName} 合规检查并允许此节点连接。当设备恢复合规时,绕过将自动移除。",
|
||||||
},
|
},
|
||||||
policies: {
|
policies: {
|
||||||
title: "策略",
|
title: "策略",
|
||||||
@@ -1459,6 +1477,25 @@ export default {
|
|||||||
groups: "组",
|
groups: "组",
|
||||||
usage: "使用情况",
|
usage: "使用情况",
|
||||||
lastUsedOn: "上次使用于",
|
lastUsedOn: "上次使用于",
|
||||||
|
// SetupKeyActionCell
|
||||||
|
revoke: "撤销",
|
||||||
|
openActionsMenu: "打开操作菜单",
|
||||||
|
revokeConfirmTitle: "撤销安装密钥「{name}」?",
|
||||||
|
revokeConfirmDescription:
|
||||||
|
"确定要撤销此安装密钥吗?此操作无法撤销。",
|
||||||
|
revokeSuccessDescription: "安装密钥已成功撤销",
|
||||||
|
revokeLoading: "正在撤销安装密钥...",
|
||||||
|
deleteConfirmTitle: "删除安装密钥「{name}」?",
|
||||||
|
deleteConfirmDescription:
|
||||||
|
"确定要删除此安装密钥吗?此操作无法撤销。",
|
||||||
|
deleteSuccessDescription: "安装密钥已成功删除",
|
||||||
|
deleteLoading: "正在删除安装密钥...",
|
||||||
|
// SetupKeyGroupsCell
|
||||||
|
autoAssignedGroups: "自动分配的组",
|
||||||
|
autoAssignedGroupsDescription:
|
||||||
|
"使用此密钥注册的节点将自动分配这些组",
|
||||||
|
groupsSavedDescription: "安装密钥的组已成功保存",
|
||||||
|
groupsSaving: "正在保存安装密钥的组...",
|
||||||
},
|
},
|
||||||
activity: {
|
activity: {
|
||||||
title: "活动",
|
title: "活动",
|
||||||
@@ -1476,6 +1513,281 @@ export default {
|
|||||||
ipAddress: "IP 地址",
|
ipAddress: "IP 地址",
|
||||||
details: "详情",
|
details: "详情",
|
||||||
code: "代码",
|
code: "代码",
|
||||||
|
|
||||||
|
// UI strings used across activity components
|
||||||
|
allEventTypes: "所有事件类型",
|
||||||
|
allUsers: "所有用户",
|
||||||
|
includeAllUsers: "包含所有用户",
|
||||||
|
searchEvent: "搜索事件...",
|
||||||
|
searchUser: "搜索用户...",
|
||||||
|
noUsersAvailable: "没有可选择的用户。",
|
||||||
|
noUsersMatching: "没有符合条件的用户。",
|
||||||
|
system: "系统",
|
||||||
|
external: "外部",
|
||||||
|
serviceUser: "服务用户",
|
||||||
|
typeCount: "{count} 种类型",
|
||||||
|
eventCount: "{count} 个事件",
|
||||||
|
activityCode: "活动代码",
|
||||||
|
meta: "元数据",
|
||||||
|
from: "来自",
|
||||||
|
unknown: "未知",
|
||||||
|
|
||||||
|
// Activity event descriptions (used by ActivityDescription.tsx)
|
||||||
|
|
||||||
|
// Setup Key
|
||||||
|
desc_setupkey_revoke:
|
||||||
|
"安装密钥 <Value>{name}</Value>(密钥:<Value>{key}</Value>)已被撤销",
|
||||||
|
desc_setupkey_delete:
|
||||||
|
"安装密钥 <Value>{name}</Value>(密钥:<Value>{key}</Value>)已被删除",
|
||||||
|
desc_setupkey_add:
|
||||||
|
"安装密钥 <Value>{name}</Value>(密钥:<Value>{key}</Value>)已创建",
|
||||||
|
desc_peer_setupkey_add:
|
||||||
|
"节点 <Value>{name}</Value><peerConnectionInfo></peerConnectionInfo>已通过 NetBird IP <Value>{ip}</Value> 添加,使用安装密钥 <Value>{setup_key_name}</Value>",
|
||||||
|
desc_setupkey_group_delete:
|
||||||
|
"组 <Value>{group}</Value> 已从安装密钥 <Value>{setupkey}</Value> 中移除",
|
||||||
|
desc_setupkey_group_add:
|
||||||
|
"组 <Value>{group}</Value> 已添加到安装密钥 <Value>{setupkey}</Value>",
|
||||||
|
|
||||||
|
// Dashboard
|
||||||
|
desc_dashboard_login:
|
||||||
|
"<Value>{username}</Value> 登录到仪表板",
|
||||||
|
|
||||||
|
// Policy
|
||||||
|
desc_policy_update: "策略 <Value>{name}</Value> 已更新",
|
||||||
|
desc_policy_delete: "策略 <Value>{name}</Value> 已被删除",
|
||||||
|
desc_policy_add: "策略 <Value>{name}</Value> 已创建",
|
||||||
|
|
||||||
|
// Route
|
||||||
|
desc_route_delete_domains:
|
||||||
|
"路由 <Value>{name}</Value>(域名:<Value>{domains}</Value>)已被删除",
|
||||||
|
desc_route_delete_range:
|
||||||
|
"路由 <Value>{name}</Value>(范围:<Value>{network_range}</Value>)已被删除",
|
||||||
|
desc_route_update_domains:
|
||||||
|
"路由 <Value>{name}</Value>(域名:<Value>{domains}</Value>)已更新",
|
||||||
|
desc_route_update_range:
|
||||||
|
"路由 <Value>{name}</Value>(范围:<Value>{network_range}</Value>)已更新",
|
||||||
|
desc_route_add_domains:
|
||||||
|
"路由 <Value>{name}</Value>(域名:<Value>{domains}</Value>)已创建",
|
||||||
|
desc_route_add_range:
|
||||||
|
"路由 <Value>{name}</Value>(范围:<Value>{network_range}</Value>)已创建",
|
||||||
|
|
||||||
|
// User / Peer
|
||||||
|
desc_user_peer_delete:
|
||||||
|
"节点 <Value>{name}</Value><peerConnectionInfo></peerConnectionInfo>(NetBird IP:<Value>{ip}</Value>)已被删除",
|
||||||
|
desc_user_peer_add:
|
||||||
|
"节点 <Value>{name}</Value><peerConnectionInfo></peerConnectionInfo>已通过 NetBird IP <Value>{ip}</Value> 添加",
|
||||||
|
desc_user_peer_update:
|
||||||
|
"节点 <Value>{name}</Value><peerConnectionInfo></peerConnectionInfo>(NetBird IP:<Value>{ip}</Value>)已更新",
|
||||||
|
desc_user_join: "用户 <Value>{username}</Value> 加入了 NetBird",
|
||||||
|
desc_user_invite:
|
||||||
|
"<Value>{username}</Value> <Value>{email}</Value> 已被邀请。",
|
||||||
|
desc_user_create:
|
||||||
|
"<Value>{username}</Value> <Value>{email}</Value> 已由 <Value>{initiator}</Value> 创建",
|
||||||
|
desc_user_group_add:
|
||||||
|
"组 <Value>{group}</Value> 已添加到用户 <Value>{username}</Value>",
|
||||||
|
desc_user_block:
|
||||||
|
"用户 <Value>{username}</Value> <Value>{email}</Value> 已被阻止",
|
||||||
|
desc_user_unblock:
|
||||||
|
"用户 <Value>{username}</Value> <Value>{email}</Value> 已解除阻止",
|
||||||
|
desc_user_delete:
|
||||||
|
"用户 <Value>{username}</Value> <Value>{email}</Value> 已被删除",
|
||||||
|
desc_user_group_delete:
|
||||||
|
"组 <Value>{group}</Value> 已从用户 <Value>{username}</Value> <Value>{email}</Value> 中移除",
|
||||||
|
desc_user_role_update:
|
||||||
|
"用户 <Value>{username}</Value> <Value>{email}</Value> 的角色已更新为 <Value>{role}</Value>",
|
||||||
|
desc_user_approve:
|
||||||
|
"用户 <Value>{username}</Value> <Value>{email}</Value> 已获批准",
|
||||||
|
desc_user_reject:
|
||||||
|
"用户 <Value>{username}</Value> <Value>{email}</Value> 已被拒绝",
|
||||||
|
desc_user_password_change:
|
||||||
|
"用户 <Value>{username}</Value> <Value>{email}</Value> 的密码已更改",
|
||||||
|
|
||||||
|
// Invite Link
|
||||||
|
desc_user_invite_link_create:
|
||||||
|
"已为 <Value>{username}</Value> <Value>{email}</Value> 创建邀请链接",
|
||||||
|
desc_user_invite_link_accept:
|
||||||
|
"邀请链接已被 <Value>{username}</Value> <Value>{email}</Value> 接受",
|
||||||
|
desc_user_invite_link_regenerate:
|
||||||
|
"已为 <Value>{username}</Value> <Value>{email}</Value> 重新生成邀请链接",
|
||||||
|
desc_user_invite_link_delete:
|
||||||
|
"已删除 <Value>{username}</Value> <Value>{email}</Value> 的邀请链接",
|
||||||
|
|
||||||
|
// Service User
|
||||||
|
desc_service_user_create: "服务用户 <Value>{name}</Value> 已创建",
|
||||||
|
desc_service_user_delete: "服务用户 <Value>{name}</Value> 已被删除",
|
||||||
|
|
||||||
|
// Peer
|
||||||
|
desc_peer_group_delete:
|
||||||
|
"组 <Value>{group}</Value> 已从 NetBird IP 为 <Value>{peer_ip}</Value> 的节点中移除",
|
||||||
|
desc_peer_group_add:
|
||||||
|
"组 <Value>{group}</Value> 已添加到 NetBird IP 为 <Value>{peer_ip}</Value> 的节点",
|
||||||
|
desc_peer_login_expire:
|
||||||
|
"节点 <Value>{name}</Value> 的登录已过期",
|
||||||
|
desc_peer_ssh_disable:
|
||||||
|
"节点 <Value>{name}</Value> 的 SSH 服务器已禁用",
|
||||||
|
desc_peer_ssh_enable:
|
||||||
|
"节点 <Value>{name}</Value> 的 SSH 服务器已启用",
|
||||||
|
desc_peer_login_expiration_disable:
|
||||||
|
"节点 <Value>{name}</Value> 的登录过期已禁用",
|
||||||
|
desc_peer_login_expiration_enable:
|
||||||
|
"节点 <Value>{name}</Value> 的登录过期已启用",
|
||||||
|
desc_peer_rename:
|
||||||
|
"NetBird IP 为 <Value>{ip}</Value> 的节点已重命名为 <Value>{name}</Value>",
|
||||||
|
desc_peer_approve:
|
||||||
|
"NetBird IP 为 <Value>{ip}</Value> 的节点已获批准",
|
||||||
|
desc_peer_ip_update:
|
||||||
|
"节点 <Value>{name}</Value> 的 IP 地址已从 <Value>{old_ip}</Value> 更新为 <Value>{ip}</Value>",
|
||||||
|
desc_peer_user_add:
|
||||||
|
"节点 <Value>{name}</Value><peerConnectionInfo></peerConnectionInfo>已通过 NetBird IP <Value>{ip}</Value> 添加",
|
||||||
|
|
||||||
|
// Group
|
||||||
|
desc_group_add: "组 <Value>{name}</Value> 已创建",
|
||||||
|
desc_group_delete: "组 <Value>{name}</Value> 已被删除",
|
||||||
|
desc_group_update:
|
||||||
|
"组 <Value>{old_name}</Value> 已重命名为 <Value>{new_name}</Value>",
|
||||||
|
|
||||||
|
// Account
|
||||||
|
desc_account_create: "<Value>{initiator}</Value> 创建了账户",
|
||||||
|
desc_account_setting_peer_login_expiration_update:
|
||||||
|
"全局登录过期已更新",
|
||||||
|
desc_account_setting_peer_login_expiration_enable:
|
||||||
|
"全局登录过期已启用",
|
||||||
|
desc_account_setting_peer_login_expiration_disable:
|
||||||
|
"全局登录过期已禁用",
|
||||||
|
desc_account_network_range_update:
|
||||||
|
"账户网络范围已从 <Value>{old_network_range}</Value> 更新为 <Value>{new_network_range}</Value>",
|
||||||
|
|
||||||
|
// Nameserver
|
||||||
|
desc_nameserver_group_add: "名称服务器 <Value>{name}</Value> 已添加",
|
||||||
|
desc_nameserver_group_delete: "名称服务器 <Value>{name}</Value> 已被删除",
|
||||||
|
desc_nameserver_group_update: "名称服务器 <Value>{name}</Value> 已更新",
|
||||||
|
|
||||||
|
// Personal Access Token
|
||||||
|
desc_personal_access_token_create:
|
||||||
|
"用户 <Value>{username}</Value> 的访问令牌 <Value>{name}</Value> 已创建",
|
||||||
|
desc_personal_access_token_delete:
|
||||||
|
"用户 <Value>{username}</Value> 的访问令牌 <Value>{name}</Value> 已被删除",
|
||||||
|
|
||||||
|
// Integration
|
||||||
|
desc_integration_create_platform:
|
||||||
|
"<Value>{platform}</Value> 集成已创建",
|
||||||
|
desc_integration_create: "集成已创建",
|
||||||
|
desc_integration_delete_platform:
|
||||||
|
"<Value>{platform}</Value> 集成已被删除",
|
||||||
|
desc_integration_delete: "集成已被删除",
|
||||||
|
desc_integration_update_platform:
|
||||||
|
"<Value>{platform}</Value> 集成已更新",
|
||||||
|
desc_integration_update: "集成已更新",
|
||||||
|
|
||||||
|
// DNS
|
||||||
|
desc_dns_setting_disabled_management_group_add:
|
||||||
|
"组 <Value>{group}</Value> 已添加到禁用的 DNS 组设置",
|
||||||
|
desc_dns_setting_disabled_management_group_delete:
|
||||||
|
"组 <Value>{group}</Value> 已从禁用的 DNS 组设置中移除",
|
||||||
|
|
||||||
|
// Posture Checks
|
||||||
|
desc_posture_check_updated:
|
||||||
|
"姿态检查 <Value>{name}</Value> 已更新",
|
||||||
|
desc_posture_check_created:
|
||||||
|
"姿态检查 <Value>{name}</Value> 已创建",
|
||||||
|
desc_posture_check_deleted:
|
||||||
|
"姿态检查 <Value>{name}</Value> 已被删除",
|
||||||
|
desc_transferred_owner_role: "所有者角色已转让",
|
||||||
|
|
||||||
|
// EDR / Integrated Validator
|
||||||
|
desc_integrated_validator_api_created:
|
||||||
|
"<Value>{platform}</Value> 集成已创建",
|
||||||
|
desc_integrated_validator_api_updated:
|
||||||
|
"<Value>{platform}</Value> 集成已更新",
|
||||||
|
desc_integrated_validator_api_deleted:
|
||||||
|
"<Value>{platform}</Value> 集成已被删除",
|
||||||
|
desc_integrated_validator_host_check_approved:
|
||||||
|
"节点已通过 <Value>{platform}</Value> 集成批准",
|
||||||
|
desc_integrated_validator_host_check_denied:
|
||||||
|
"节点已被 <Value>{platform}</Value> 集成拒绝",
|
||||||
|
desc_integrated_validator_peer_compliance_bypassed:
|
||||||
|
"NetBird IP 为 <Value>{ip}</Value> 的节点 <Value>{name}</Value> 已绕过 <Value>{platform}</Value> 的合规检查{original_reason}",
|
||||||
|
desc_integrated_validator_peer_compliance_bypass_revoked:
|
||||||
|
"NetBird IP 为 <Value>{ip}</Value> 的节点 <Value>{name}</Value> 的合规绕过已对 <Value>{platform}</Value> 撤销",
|
||||||
|
desc_compliance_original_reason:
|
||||||
|
"(原始不合规原因:<Value>{reason}</Value>)",
|
||||||
|
|
||||||
|
// Resource
|
||||||
|
desc_resource_group_add:
|
||||||
|
"组 <Value>{resource_name}</Value> 已添加到资源 <Value>{name}</Value>",
|
||||||
|
desc_resource_group_delete:
|
||||||
|
"组 <Value>{resource_name}</Value> 已从资源 <Value>{name}</Value> 中移除",
|
||||||
|
|
||||||
|
// Reverse Proxy (peer expose)
|
||||||
|
desc_service_peer_expose:
|
||||||
|
"节点 <Value>{peer_name}</Value> 暴露了服务 <Value>{domain}</Value>,认证状态:<Value>{auth}</Value>",
|
||||||
|
desc_service_peer_unexpose:
|
||||||
|
"节点 <Value>{peer_name}</Value> 取消了服务 <Value>{domain}</Value> 的暴露",
|
||||||
|
desc_service_peer_expose_expire:
|
||||||
|
"节点 <Value>{peer_name}</Value> 暴露的服务 <Value>{domain}</Value> 因续期过期已被移除",
|
||||||
|
|
||||||
|
// Networks
|
||||||
|
desc_network_resource_create:
|
||||||
|
"资源 <Value>{name}</Value> 已为网络 <Value>{network_name}</Value> 创建",
|
||||||
|
desc_network_resource_update:
|
||||||
|
"资源 <Value>{name}</Value> 已为网络 <Value>{network_name}</Value> 更新",
|
||||||
|
desc_network_resource_delete:
|
||||||
|
"资源 <Value>{name}</Value> 已从网络 <Value>{network_name}</Value> 中删除",
|
||||||
|
desc_network_router_create:
|
||||||
|
"已为网络 <Value>{network_name}</Value> 创建路由节点",
|
||||||
|
desc_network_router_delete:
|
||||||
|
"已从网络 <Value>{network_name}</Value> 中删除路由节点",
|
||||||
|
desc_network_router_update:
|
||||||
|
"已更新网络 <Value>{network_name}</Value> 的路由节点",
|
||||||
|
desc_network_create:
|
||||||
|
"名为 <Value>{name}</Value> 的网络已创建",
|
||||||
|
desc_network_delete:
|
||||||
|
"名为 <Value>{name}</Value> 的网络已被删除",
|
||||||
|
desc_network_update:
|
||||||
|
"名为 <Value>{name}</Value> 的网络已更新",
|
||||||
|
|
||||||
|
// Jobs
|
||||||
|
desc_peer_job_create:
|
||||||
|
"已为节点 <Value>{for_peer_name}</Value> 创建远程任务 <Value>{job_type}</Value>",
|
||||||
|
|
||||||
|
// Flow Settings
|
||||||
|
desc_account_settings_extra_flow_group_remove:
|
||||||
|
"流量事件限制组 <Value>{group_name}</Value> 已移除",
|
||||||
|
desc_account_settings_extra_flow_group_add:
|
||||||
|
"流量事件限制组 <Value>{group_name}</Value> 已添加",
|
||||||
|
|
||||||
|
// Identity Provider
|
||||||
|
desc_identityprovider_create:
|
||||||
|
"身份提供者 <Value>{name}</Value> 已创建",
|
||||||
|
desc_identityprovider_update:
|
||||||
|
"身份提供者 <Value>{name}</Value> 已更新",
|
||||||
|
desc_identityprovider_delete:
|
||||||
|
"身份提供者 <Value>{name}</Value> 已被删除",
|
||||||
|
|
||||||
|
// Service (proxy cluster)
|
||||||
|
desc_service_create:
|
||||||
|
"集群 <Value>{proxy_cluster}</Value> 中的服务 <Value>{domain}</Value> 已创建,认证方式:<Value>{auth}</Value>",
|
||||||
|
desc_service_update:
|
||||||
|
"集群 <Value>{proxy_cluster}</Value> 中的服务 <Value>{domain}</Value> 已更新,认证方式:<Value>{auth}</Value>",
|
||||||
|
desc_service_delete:
|
||||||
|
"集群 <Value>{proxy_cluster}</Value> 中的服务 <Value>{domain}</Value> 已被删除",
|
||||||
|
|
||||||
|
// Reseller / Distributor
|
||||||
|
desc_reseller_msp_created:
|
||||||
|
"客户 <Value>{msp_name}</Value>(域名:<Value>{msp_domain}</Value>)已创建",
|
||||||
|
desc_reseller_activated: "分销商账户已激活",
|
||||||
|
desc_reseller_msp_deleted:
|
||||||
|
"客户 <Value>{msp_name}</Value>(域名:<Value>{msp_domain}</Value>)已被删除",
|
||||||
|
desc_reseller_msp_unlinked:
|
||||||
|
"客户 <Value>{msp_name}</Value>(域名:<Value>{msp_domain}</Value>)已取消关联",
|
||||||
|
desc_reseller_msp_invite_requested:
|
||||||
|
"已为客户 <Value>{msp_name}</Value>(域名:<Value>{msp_domain}</Value>)请求邀请",
|
||||||
|
desc_reseller_msp_invite_accepted:
|
||||||
|
"客户 <Value>{msp_name}</Value>(域名:<Value>{msp_domain}</Value>)已接受邀请",
|
||||||
|
desc_reseller_msp_invite_declined:
|
||||||
|
"客户 <Value>{msp_name}</Value>(域名:<Value>{msp_domain}</Value>)已拒绝邀请",
|
||||||
|
desc_reseller_msp_updated:
|
||||||
|
"客户 <Value>{msp_name}</Value>(域名:<Value>{msp_domain}</Value>)已更新",
|
||||||
},
|
},
|
||||||
controlCenter: {
|
controlCenter: {
|
||||||
title: "控制中心",
|
title: "控制中心",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import TextWithTooltip from "@components/ui/TextWithTooltip";
|
|||||||
import { cn, generateColorFromUser } from "@utils/helpers";
|
import { cn, generateColorFromUser } from "@utils/helpers";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { AlertCircle, ArrowUpRight, Cog, PlusIcon, XIcon } from "lucide-react";
|
import { AlertCircle, ArrowUpRight, Cog, PlusIcon, XIcon } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { useUsers } from "@/contexts/UsersProvider";
|
import { useUsers } from "@/contexts/UsersProvider";
|
||||||
import { ActivityEvent } from "@/interfaces/ActivityEvent";
|
import { ActivityEvent } from "@/interfaces/ActivityEvent";
|
||||||
@@ -23,6 +24,7 @@ const ActionIcons: Record<ActionColor, React.ReactNode> = {
|
|||||||
|
|
||||||
export const ActivityEntryRow = ({ event }: { event: ActivityEvent }) => {
|
export const ActivityEntryRow = ({ event }: { event: ActivityEvent }) => {
|
||||||
const { users } = useUsers();
|
const { users } = useUsers();
|
||||||
|
const t = useTranslations("activity");
|
||||||
|
|
||||||
const getActivityUser = () => {
|
const getActivityUser = () => {
|
||||||
let user;
|
let user;
|
||||||
@@ -95,7 +97,7 @@ export const ActivityEntryRow = ({ event }: { event: ActivityEvent }) => {
|
|||||||
|
|
||||||
<span className={"text-sm text-nb-gray-200"}>
|
<span className={"text-sm text-nb-gray-200"}>
|
||||||
<TextWithTooltip
|
<TextWithTooltip
|
||||||
text={user?.name || user?.id || "System"}
|
text={user?.name || user?.id || t("system")}
|
||||||
maxChars={20}
|
maxChars={20}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
@@ -105,7 +107,7 @@ export const ActivityEntryRow = ({ event }: { event: ActivityEvent }) => {
|
|||||||
{isExternal && (
|
{isExternal && (
|
||||||
<span className={"flex items-center"}>
|
<span className={"flex items-center"}>
|
||||||
<SmallBadge
|
<SmallBadge
|
||||||
text={"External"}
|
text={t("external")}
|
||||||
variant={"sky"}
|
variant={"sky"}
|
||||||
className={
|
className={
|
||||||
"text-[10px] py-[0.2rem] px-1.5 rounded-full leading-none -top-0"
|
"text-[10px] py-[0.2rem] px-1.5 rounded-full leading-none -top-0"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { cn } from "@utils/helpers";
|
|||||||
import { Command, CommandGroup, CommandInput, CommandList } from "cmdk";
|
import { Command, CommandGroup, CommandInput, CommandList } from "cmdk";
|
||||||
import { trim, uniqBy } from "lodash";
|
import { trim, uniqBy } from "lodash";
|
||||||
import { ChevronsUpDown, Layers, SearchIcon } from "lucide-react";
|
import { ChevronsUpDown, Layers, SearchIcon } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useElementSize } from "@/hooks/useElementSize";
|
import { useElementSize } from "@/hooks/useElementSize";
|
||||||
@@ -27,6 +28,7 @@ export function ActivityEventCodeSelector({
|
|||||||
popoverWidth = 400,
|
popoverWidth = 400,
|
||||||
events,
|
events,
|
||||||
}: MultiSelectProps) {
|
}: MultiSelectProps) {
|
||||||
|
const t = useTranslations("activity");
|
||||||
const searchRef = React.useRef<HTMLInputElement>(null);
|
const searchRef = React.useRef<HTMLInputElement>(null);
|
||||||
const [inputRef, { width }] = useElementSize<HTMLButtonElement>();
|
const [inputRef, { width }] = useElementSize<HTMLButtonElement>();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@@ -50,7 +52,7 @@ export function ActivityEventCodeSelector({
|
|||||||
activity_code: event.activity_code,
|
activity_code: event.activity_code,
|
||||||
activity: event.activity,
|
activity: event.activity,
|
||||||
group: event.activity_code.startsWith("service.user")
|
group: event.activity_code.startsWith("service.user")
|
||||||
? "Service User"
|
? t("serviceUser")
|
||||||
: event.activity_code.split(".")[0],
|
: event.activity_code.split(".")[0],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -81,9 +83,9 @@ export function ActivityEventCodeSelector({
|
|||||||
<Layers size={16} className={"shrink-0"} />
|
<Layers size={16} className={"shrink-0"} />
|
||||||
<div className={"w-full flex justify-between"}>
|
<div className={"w-full flex justify-between"}>
|
||||||
{values.length > 0 ? (
|
{values.length > 0 ? (
|
||||||
<div>{values.length} Event(s)</div>
|
<div>{t("eventCount", { count: values.length })}</div>
|
||||||
) : (
|
) : (
|
||||||
"All Event Types"
|
t("allEventTypes")
|
||||||
)}
|
)}
|
||||||
<div className={"pl-2"}>
|
<div className={"pl-2"}>
|
||||||
<ChevronsUpDown size={18} className={"shrink-0"} />
|
<ChevronsUpDown size={18} className={"shrink-0"} />
|
||||||
@@ -122,7 +124,7 @@ export function ActivityEventCodeSelector({
|
|||||||
ref={searchRef}
|
ref={searchRef}
|
||||||
value={search}
|
value={search}
|
||||||
onValueChange={setSearch}
|
onValueChange={setSearch}
|
||||||
placeholder={"Search event..."}
|
placeholder={t("searchEvent")}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export default function ActivityTable({
|
|||||||
events={events ?? []}
|
events={events ?? []}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
formatChip: (v) => formatActivityTypeChip(v as string[] | undefined),
|
formatChip: (v) => formatActivityTypeChip(v as string[] | undefined, t),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "initiator_email",
|
id: "initiator_email",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { cn } from "@utils/helpers";
|
|||||||
import { Command, CommandGroup, CommandInput, CommandList } from "cmdk";
|
import { Command, CommandGroup, CommandInput, CommandList } from "cmdk";
|
||||||
import { trim, uniqBy } from "lodash";
|
import { trim, uniqBy } from "lodash";
|
||||||
import { SearchIcon } from "lucide-react";
|
import { SearchIcon } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useMemo, useRef } from "react";
|
import { useMemo, useRef } from "react";
|
||||||
import { ActivityEvent } from "@/interfaces/ActivityEvent";
|
import { ActivityEvent } from "@/interfaces/ActivityEvent";
|
||||||
@@ -34,6 +35,7 @@ export function ActivityTypePicker({
|
|||||||
onChange,
|
onChange,
|
||||||
events,
|
events,
|
||||||
}: Readonly<Props>) {
|
}: Readonly<Props>) {
|
||||||
|
const t = useTranslations("activity");
|
||||||
const searchRef = useRef<HTMLInputElement>(null);
|
const searchRef = useRef<HTMLInputElement>(null);
|
||||||
const selected = value ?? [];
|
const selected = value ?? [];
|
||||||
|
|
||||||
@@ -43,7 +45,7 @@ export function ActivityTypePicker({
|
|||||||
activity_code: event.activity_code,
|
activity_code: event.activity_code,
|
||||||
activity: event.activity,
|
activity: event.activity,
|
||||||
group: event.activity_code.startsWith("service.user")
|
group: event.activity_code.startsWith("service.user")
|
||||||
? "Service User"
|
? t("serviceUser")
|
||||||
: event.activity_code.split(".")[0],
|
: event.activity_code.split(".")[0],
|
||||||
}));
|
}));
|
||||||
return items.reduce<Record<string, GroupedItem[]>>((acc, item) => {
|
return items.reduce<Record<string, GroupedItem[]>>((acc, item) => {
|
||||||
@@ -80,7 +82,7 @@ export function ActivityTypePicker({
|
|||||||
"dark:placeholder:text-nb-gray-400 font-light placeholder:text-neutral-500 pl-9",
|
"dark:placeholder:text-nb-gray-400 font-light placeholder:text-neutral-500 pl-9",
|
||||||
)}
|
)}
|
||||||
ref={searchRef}
|
ref={searchRef}
|
||||||
placeholder={"Search event..."}
|
placeholder={t("searchEvent")}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
@@ -150,8 +152,9 @@ export function ActivityTypePicker({
|
|||||||
|
|
||||||
export function formatActivityTypeChip(
|
export function formatActivityTypeChip(
|
||||||
value: string[] | undefined,
|
value: string[] | undefined,
|
||||||
|
t?: (key: string, params?: Record<string, any>) => string,
|
||||||
): string | null {
|
): string | null {
|
||||||
if (!value || value.length === 0) return null;
|
if (!value || value.length === 0) return null;
|
||||||
if (value.length === 1) return value[0];
|
if (value.length === 1) return value[0];
|
||||||
return `${value.length} types`;
|
return t ? t("typeCount", { count: value.length }) : `${value.length} types`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useSearch } from "@hooks/useSearch";
|
|||||||
import { generateColorFromString } from "@utils/helpers";
|
import { generateColorFromString } from "@utils/helpers";
|
||||||
import { sortBy, uniqBy } from "lodash";
|
import { sortBy, uniqBy } from "lodash";
|
||||||
import { ChevronsUpDown, Cog, UserCircle2 } from "lucide-react";
|
import { ChevronsUpDown, Cog, UserCircle2 } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useElementSize } from "@/hooks/useElementSize";
|
import { useElementSize } from "@/hooks/useElementSize";
|
||||||
@@ -48,11 +49,12 @@ export function UsersDropdownSelector({
|
|||||||
popoverWidth = 250,
|
popoverWidth = 250,
|
||||||
options,
|
options,
|
||||||
}: Readonly<Props>) {
|
}: Readonly<Props>) {
|
||||||
|
const t = useTranslations("activity");
|
||||||
const [filteredItems, search, setSearch] = useSearch(
|
const [filteredItems, search, setSearch] = useSearch(
|
||||||
options.concat({
|
options.concat({
|
||||||
id: "all-users",
|
id: "all-users",
|
||||||
name: "All Users",
|
name: t("allUsers"),
|
||||||
email: "Include all users",
|
email: t("includeAllUsers"),
|
||||||
}),
|
}),
|
||||||
searchPredicate,
|
searchPredicate,
|
||||||
{ filter: true, debounce: 150 },
|
{ filter: true, debounce: 150 },
|
||||||
@@ -107,7 +109,7 @@ export function UsersDropdownSelector({
|
|||||||
{!selectedUser ? (
|
{!selectedUser ? (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<UserCircle2 size={16} />
|
<UserCircle2 size={16} />
|
||||||
All Users
|
{t("allUsers")}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
) : (
|
) : (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
@@ -136,7 +138,7 @@ export function UsersDropdownSelector({
|
|||||||
<TextWithTooltip
|
<TextWithTooltip
|
||||||
text={
|
text={
|
||||||
selectedUser?.email === "NetBird"
|
selectedUser?.email === "NetBird"
|
||||||
? "System"
|
? t("system")
|
||||||
: selectedUser?.name
|
: selectedUser?.name
|
||||||
}
|
}
|
||||||
maxChars={20}
|
maxChars={20}
|
||||||
@@ -165,14 +167,14 @@ export function UsersDropdownSelector({
|
|||||||
<DropdownInput
|
<DropdownInput
|
||||||
value={search}
|
value={search}
|
||||||
onChange={setSearch}
|
onChange={setSearch}
|
||||||
placeholder={"Search user..."}
|
placeholder={t("searchUser")}
|
||||||
hideEnterIcon={true}
|
hideEnterIcon={true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{options.length == 0 && !search && (
|
{options.length == 0 && !search && (
|
||||||
<div className={"max-w-xs mx-auto"}>
|
<div className={"max-w-xs mx-auto"}>
|
||||||
<DropdownInfoText>
|
<DropdownInfoText>
|
||||||
{"No users available to select."}
|
{t("noUsersAvailable")}
|
||||||
</DropdownInfoText>
|
</DropdownInfoText>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -180,7 +182,7 @@ export function UsersDropdownSelector({
|
|||||||
{filteredItems.length == 0 && search != "" && (
|
{filteredItems.length == 0 && search != "" && (
|
||||||
<div className={"px-10"}>
|
<div className={"px-10"}>
|
||||||
<DropdownInfoText>
|
<DropdownInfoText>
|
||||||
There are no users matching your search.
|
{t("noUsersMatching")}
|
||||||
</DropdownInfoText>
|
</DropdownInfoText>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -227,7 +229,7 @@ export function UsersDropdownSelector({
|
|||||||
>
|
>
|
||||||
<TextWithTooltip
|
<TextWithTooltip
|
||||||
text={
|
text={
|
||||||
isSystemUser ? "System" : user?.name || user?.id
|
isSystemUser ? t("system") : user?.name || user?.id
|
||||||
}
|
}
|
||||||
maxChars={20}
|
maxChars={20}
|
||||||
/>
|
/>
|
||||||
@@ -246,7 +248,7 @@ export function UsersDropdownSelector({
|
|||||||
{user.external && (
|
{user.external && (
|
||||||
<span className={"flex items-center ml-auto relative"}>
|
<span className={"flex items-center ml-auto relative"}>
|
||||||
<SmallBadge
|
<SmallBadge
|
||||||
text={"External"}
|
text={t("external")}
|
||||||
variant={"sky"}
|
variant={"sky"}
|
||||||
className={
|
className={
|
||||||
"text-[8.5px] py-[0.15rem] px-[.32rem] leading-none rounded-full -top-0"
|
"text-[8.5px] py-[0.15rem] px-[.32rem] leading-none rounded-full -top-0"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import Button from "@components/Button";
|
import Button from "@components/Button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -7,6 +9,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@components/DropdownMenu";
|
} from "@components/DropdownMenu";
|
||||||
import FullTooltip from "@components/FullTooltip";
|
import FullTooltip from "@components/FullTooltip";
|
||||||
|
import InlineLink from "@components/InlineLink";
|
||||||
import { notify } from "@components/Notification";
|
import { notify } from "@components/Notification";
|
||||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||||
import { IconInfoCircle } from "@tabler/icons-react";
|
import { IconInfoCircle } from "@tabler/icons-react";
|
||||||
@@ -21,19 +24,19 @@ import {
|
|||||||
TimerResetIcon,
|
TimerResetIcon,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { useSWRConfig } from "swr";
|
import { useSWRConfig } from "swr";
|
||||||
import { useBypass, useBypassedPeers } from "@/cloud/edr/useBypass";
|
import { useBypass, useBypassedPeers } from "@/cloud/edr/useBypass";
|
||||||
import { usePeer } from "@/contexts/PeerProvider";
|
import { usePeer } from "@/contexts/PeerProvider";
|
||||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||||
|
import { useDialog } from "@/contexts/DialogProvider";
|
||||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||||
import { ExitNodeDropdownButton } from "@/modules/exit-node/ExitNodeDropdownButton";
|
import { ExitNodeDropdownButton } from "@/modules/exit-node/ExitNodeDropdownButton";
|
||||||
import { useIntegrations } from "@/modules/integrations/edr/useIntegrations";
|
import { useIntegrations } from "@/modules/integrations/edr/useIntegrations";
|
||||||
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";
|
||||||
import InlineLink from "@components/InlineLink";
|
|
||||||
import { useDialog } from "@/contexts/DialogProvider";
|
|
||||||
|
|
||||||
export default function PeerActionCell() {
|
export default function PeerActionCell() {
|
||||||
const { peer, deletePeer, update, toggleSSH, setSSHInstructionsModal } =
|
const { peer, deletePeer, update, toggleSSH, setSSHInstructionsModal } =
|
||||||
@@ -42,6 +45,8 @@ export default function PeerActionCell() {
|
|||||||
const { mutate } = useSWRConfig();
|
const { mutate } = useSWRConfig();
|
||||||
const { permission } = usePermissions();
|
const { permission } = usePermissions();
|
||||||
const { confirm } = useDialog();
|
const { confirm } = useDialog();
|
||||||
|
const t = useTranslations("peers");
|
||||||
|
const tCommon = useTranslations("common");
|
||||||
|
|
||||||
// Approval / EDR-bypass state. We pull this directly so the action
|
// Approval / EDR-bypass state. We pull this directly so the action
|
||||||
// menu can offer Approve / Bypass / Revoke without the inline badges
|
// menu can offer Approve / Bypass / Revoke without the inline badges
|
||||||
@@ -55,16 +60,16 @@ export default function PeerActionCell() {
|
|||||||
|
|
||||||
const approvePeer = async () => {
|
const approvePeer = async () => {
|
||||||
const choice = await confirm({
|
const choice = await confirm({
|
||||||
title: `Approve peer '${peer.name}'?`,
|
title: t("confirmApprove", { name: peer.name }),
|
||||||
description: "Are you sure you want to approve this peer?",
|
description: t("confirmApproveDescription"),
|
||||||
confirmText: "Approve",
|
confirmText: t("approve"),
|
||||||
cancelText: "Cancel",
|
cancelText: tCommon("cancel"),
|
||||||
type: "default",
|
type: "default",
|
||||||
});
|
});
|
||||||
if (!choice) return;
|
if (!choice) return;
|
||||||
notify({
|
notify({
|
||||||
title: `Peer ${peer.name} approved`,
|
title: t("approveSuccess", { name: peer.name }),
|
||||||
description: `This peer was approved and can now connect to other peers.`,
|
description: t("approveSuccessDescription"),
|
||||||
promise: update({
|
promise: update({
|
||||||
name: peer.name,
|
name: peer.name,
|
||||||
ssh: peer.ssh_enabled,
|
ssh: peer.ssh_enabled,
|
||||||
@@ -74,45 +79,41 @@ export default function PeerActionCell() {
|
|||||||
mutate("/peers");
|
mutate("/peers");
|
||||||
mutate("/groups");
|
mutate("/groups");
|
||||||
}),
|
}),
|
||||||
loadingMessage: "Approving peer...",
|
loadingMessage: t("approveLoading"),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBypassCompliance = async () => {
|
const handleBypassCompliance = async () => {
|
||||||
const choice = await confirm({
|
const choice = await confirm({
|
||||||
title: `Bypass compliance for '${peer.name}'?`,
|
title: t("bypassComplianceConfirmTitle", { name: peer.name }),
|
||||||
description:
|
description: t("bypassComplianceConfirmDescription"),
|
||||||
"This will override the compliance check and allow this peer to connect. " +
|
confirmText: t("bypassCompliance"),
|
||||||
"The bypass will be automatically removed if the device becomes compliant.",
|
cancelText: tCommon("cancel"),
|
||||||
confirmText: "Bypass Compliance",
|
|
||||||
cancelText: "Cancel",
|
|
||||||
type: "warning",
|
type: "warning",
|
||||||
});
|
});
|
||||||
if (!choice || !peer.id) return;
|
if (!choice || !peer.id) return;
|
||||||
notify({
|
notify({
|
||||||
title: `Compliance bypassed for ${peer.name}`,
|
title: t("bypassComplianceSuccess", { name: peer.name }),
|
||||||
description: `This peer can now connect to other peers.`,
|
description: t("bypassComplianceSuccessDescription"),
|
||||||
promise: bypassCompliance(peer.id),
|
promise: bypassCompliance(peer.id),
|
||||||
loadingMessage: "Bypassing compliance...",
|
loadingMessage: t("bypassComplianceLoading"),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRevokeBypass = async () => {
|
const handleRevokeBypass = async () => {
|
||||||
const choice = await confirm({
|
const choice = await confirm({
|
||||||
title: `Revoke compliance bypass for '${peer.name}'?`,
|
title: t("revokeBypassConfirmTitle", { name: peer.name }),
|
||||||
description:
|
description: t("revokeBypassConfirmDescription"),
|
||||||
"This peer will be subject to normal compliance validation. " +
|
confirmText: t("revoke"),
|
||||||
"If still non-compliant, it will lose network access.",
|
cancelText: tCommon("cancel"),
|
||||||
confirmText: "Revoke",
|
|
||||||
cancelText: "Cancel",
|
|
||||||
type: "warning",
|
type: "warning",
|
||||||
});
|
});
|
||||||
if (!choice || !peer.id) return;
|
if (!choice || !peer.id) return;
|
||||||
notify({
|
notify({
|
||||||
title: `Compliance bypass revoked`,
|
title: t("revokeBypassSuccess"),
|
||||||
description: `Peer ${peer.name} is now subject to normal compliance validation.`,
|
description: t("revokeBypassSuccessDescription", { name: peer.name }),
|
||||||
promise: revokeBypass(peer.id),
|
promise: revokeBypass(peer.id),
|
||||||
loadingMessage: "Revoking compliance bypass...",
|
loadingMessage: t("revokeBypassLoading"),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -143,11 +144,16 @@ export default function PeerActionCell() {
|
|||||||
const showRemoteAccessItems = !isMobile && !!peer.connected;
|
const showRemoteAccessItems = !isMobile && !!peer.connected;
|
||||||
|
|
||||||
const toggleLoginExpiration = async () => {
|
const toggleLoginExpiration = async () => {
|
||||||
const text = peer.login_expiration_enabled ? "disabled" : "enabled";
|
const state = peer.login_expiration_enabled
|
||||||
|
? tCommon("disabled")
|
||||||
|
: tCommon("enabled");
|
||||||
const disableLoginExpiration = peer.login_expiration_enabled;
|
const disableLoginExpiration = peer.login_expiration_enabled;
|
||||||
notify({
|
notify({
|
||||||
title: `Session expiration is ${text}`,
|
title: t("loginExpirationUpdated", { state }),
|
||||||
description: `Session expiration for peer ${peer.name} was successfully ${text}.`,
|
description: t("loginExpirationUpdateDescription", {
|
||||||
|
name: peer.name,
|
||||||
|
state,
|
||||||
|
}),
|
||||||
promise: update({
|
promise: update({
|
||||||
loginExpiration: !peer.login_expiration_enabled,
|
loginExpiration: !peer.login_expiration_enabled,
|
||||||
inactivityExpiration: disableLoginExpiration
|
inactivityExpiration: disableLoginExpiration
|
||||||
@@ -157,31 +163,28 @@ export default function PeerActionCell() {
|
|||||||
mutate("/peers");
|
mutate("/peers");
|
||||||
mutate("/groups");
|
mutate("/groups");
|
||||||
}),
|
}),
|
||||||
loadingMessage: "Updating session expiration...",
|
loadingMessage: t("loginExpirationUpdating"),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const disableDashboardSSH = async () => {
|
const disableDashboardSSH = async () => {
|
||||||
const choice = await confirm({
|
const choice = await confirm({
|
||||||
title: `Disable SSH Access?`,
|
title: t("disableSSHConfirmation"),
|
||||||
description: (
|
description: (
|
||||||
<div>
|
<div>
|
||||||
Starting from NetBird v0.61.0, once SSH access is disabled, you cannot
|
{t("disableSSHDescription")}{" "}
|
||||||
re-enable it again from the dashboard. You'll need to create an
|
|
||||||
explicit access control policy and update your NetBird client to
|
|
||||||
restore SSH functionality.{" "}
|
|
||||||
<InlineLink
|
<InlineLink
|
||||||
href={"https://docs.netbird.io/manage/peers/ssh"}
|
href={"https://docs.netbird.io/manage/peers/ssh"}
|
||||||
target={"_blank"}
|
target={"_blank"}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
Learn more
|
{tCommon("learnMore")}
|
||||||
<ExternalLinkIcon size={12} />
|
<ExternalLinkIcon size={12} />
|
||||||
</InlineLink>
|
</InlineLink>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
confirmText: "Disable",
|
confirmText: tCommon("disable"),
|
||||||
cancelText: "Cancel",
|
cancelText: tCommon("cancel"),
|
||||||
type: "warning",
|
type: "warning",
|
||||||
maxWidthClass: "max-w-xl",
|
maxWidthClass: "max-w-xl",
|
||||||
});
|
});
|
||||||
@@ -210,7 +213,7 @@ export default function PeerActionCell() {
|
|||||||
>
|
>
|
||||||
<div className={"flex gap-3 items-center"}>
|
<div className={"flex gap-3 items-center"}>
|
||||||
<MonitorIcon size={14} className={"shrink-0"} />
|
<MonitorIcon size={14} className={"shrink-0"} />
|
||||||
View Details
|
{t("viewDetails")}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
@@ -221,7 +224,7 @@ export default function PeerActionCell() {
|
|||||||
<DropdownMenuItem onClick={approvePeer}>
|
<DropdownMenuItem onClick={approvePeer}>
|
||||||
<div className={"flex gap-3 items-center"}>
|
<div className={"flex gap-3 items-center"}>
|
||||||
<CheckCircle2 size={14} className={"shrink-0"} />
|
<CheckCircle2 size={14} className={"shrink-0"} />
|
||||||
Approve
|
{t("approve")}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
@@ -230,16 +233,16 @@ export default function PeerActionCell() {
|
|||||||
className={"w-full block"}
|
className={"w-full block"}
|
||||||
content={
|
content={
|
||||||
<div className={"text-xs max-w-xs"}>
|
<div className={"text-xs max-w-xs"}>
|
||||||
Bypass {activeIntegrationName} compliance check and
|
{t("bypassTooltip", {
|
||||||
allow this peer to connect. The bypass is automatically
|
integrationName: activeIntegrationName,
|
||||||
removed when the device becomes compliant.
|
})}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<DropdownMenuItem onClick={handleBypassCompliance}>
|
<DropdownMenuItem onClick={handleBypassCompliance}>
|
||||||
<div className={"flex gap-3 items-center w-full"}>
|
<div className={"flex gap-3 items-center w-full"}>
|
||||||
<ShieldCheck size={14} className={"shrink-0"} />
|
<ShieldCheck size={14} className={"shrink-0"} />
|
||||||
Bypass Compliance
|
{t("bypassCompliance")}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</FullTooltip>
|
</FullTooltip>
|
||||||
@@ -248,7 +251,7 @@ export default function PeerActionCell() {
|
|||||||
<DropdownMenuItem onClick={handleRevokeBypass}>
|
<DropdownMenuItem onClick={handleRevokeBypass}>
|
||||||
<div className={"flex gap-3 items-center"}>
|
<div className={"flex gap-3 items-center"}>
|
||||||
<ShieldOff size={14} className={"shrink-0"} />
|
<ShieldOff size={14} className={"shrink-0"} />
|
||||||
Revoke Bypass
|
{t("revokeBypass")}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
@@ -270,9 +273,7 @@ export default function PeerActionCell() {
|
|||||||
className={"flex gap-2 items-center !text-nb-gray-300 text-xs"}
|
className={"flex gap-2 items-center !text-nb-gray-300 text-xs"}
|
||||||
>
|
>
|
||||||
<IconInfoCircle size={14} />
|
<IconInfoCircle size={14} />
|
||||||
<span>
|
<span>{t("expirationDisabledTooltip")}</span>
|
||||||
Expiration is disabled for all peers added with an setup-key.
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
className={"w-full block"}
|
className={"w-full block"}
|
||||||
@@ -284,8 +285,9 @@ export default function PeerActionCell() {
|
|||||||
>
|
>
|
||||||
<div className={"flex gap-3 items-center w-full"}>
|
<div className={"flex gap-3 items-center w-full"}>
|
||||||
<TimerResetIcon size={14} className={"shrink-0"} />
|
<TimerResetIcon size={14} className={"shrink-0"} />
|
||||||
{peer.login_expiration_enabled ? "Disable" : "Enable"} Session
|
{peer.login_expiration_enabled
|
||||||
Expiration
|
? t("disableLoginExpiration")
|
||||||
|
: t("enableLoginExpiration")}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</FullTooltip>
|
</FullTooltip>
|
||||||
@@ -302,7 +304,7 @@ export default function PeerActionCell() {
|
|||||||
<div className={"flex gap-3 items-center w-full"}>
|
<div className={"flex gap-3 items-center w-full"}>
|
||||||
<TerminalSquare size={14} className={"shrink-0"} />
|
<TerminalSquare size={14} className={"shrink-0"} />
|
||||||
<div className={"flex justify-between items-center w-full"}>
|
<div className={"flex justify-between items-center w-full"}>
|
||||||
{peer.ssh_enabled ? "Disable" : "Enable"} SSH Access
|
{peer.ssh_enabled ? t("disableSSH") : t("enableSSH")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -319,7 +321,7 @@ export default function PeerActionCell() {
|
|||||||
>
|
>
|
||||||
<div className={"flex gap-3 items-center"}>
|
<div className={"flex gap-3 items-center"}>
|
||||||
<Trash2 size={14} className={"shrink-0"} />
|
<Trash2 size={14} className={"shrink-0"} />
|
||||||
Delete
|
{tCommon("delete")}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import { notify } from "@components/Notification";
|
import { notify } from "@components/Notification";
|
||||||
import { useApiCall } from "@utils/api";
|
import { useApiCall } from "@utils/api";
|
||||||
import { MoreVertical, Trash2, Undo2Icon } from "lucide-react";
|
import { MoreVertical, Trash2, Undo2Icon } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useSWRConfig } from "swr";
|
import { useSWRConfig } from "swr";
|
||||||
import { useDialog } from "@/contexts/DialogProvider";
|
import { useDialog } from "@/contexts/DialogProvider";
|
||||||
@@ -19,6 +20,8 @@ type Props = {
|
|||||||
setupKey: SetupKey;
|
setupKey: SetupKey;
|
||||||
};
|
};
|
||||||
export default function SetupKeyActionCell({ setupKey }: Readonly<Props>) {
|
export default function SetupKeyActionCell({ setupKey }: Readonly<Props>) {
|
||||||
|
const t = useTranslations("setupKeys");
|
||||||
|
const tCommon = useTranslations("common");
|
||||||
const { confirm } = useDialog();
|
const { confirm } = useDialog();
|
||||||
const request = useApiCall<SetupKey>("/setup-keys/" + setupKey.id);
|
const request = useApiCall<SetupKey>("/setup-keys/" + setupKey.id);
|
||||||
const { mutate } = useSWRConfig();
|
const { mutate } = useSWRConfig();
|
||||||
@@ -28,23 +31,24 @@ export default function SetupKeyActionCell({ setupKey }: Readonly<Props>) {
|
|||||||
!setupKey.revoked && setupKey.valid && permission.setup_keys.update;
|
!setupKey.revoked && setupKey.valid && permission.setup_keys.update;
|
||||||
const canDelete = permission.setup_keys.delete;
|
const canDelete = permission.setup_keys.delete;
|
||||||
|
|
||||||
|
const keyName = setupKey?.name || t("key");
|
||||||
|
|
||||||
const handleRevoke = async () => {
|
const handleRevoke = async () => {
|
||||||
const choice = await confirm({
|
const choice = await confirm({
|
||||||
title: `Revoke '${setupKey?.name || "Setup Key"}'?`,
|
title: t("revokeConfirmTitle", { name: keyName }),
|
||||||
description:
|
description: t("revokeConfirmDescription"),
|
||||||
"Are you sure you want to revoke the setup key? This action cannot be undone.",
|
confirmText: t("revoke"),
|
||||||
confirmText: "Revoke",
|
cancelText: tCommon("cancel"),
|
||||||
cancelText: "Cancel",
|
|
||||||
type: "danger",
|
type: "danger",
|
||||||
});
|
});
|
||||||
if (!choice) return;
|
if (!choice) return;
|
||||||
|
|
||||||
notify({
|
notify({
|
||||||
title: setupKey?.name || "Setup Key",
|
title: keyName,
|
||||||
description: "Setup key was successfully revoked",
|
description: t("revokeSuccessDescription"),
|
||||||
promise: request
|
promise: request
|
||||||
.put({
|
.put({
|
||||||
name: setupKey?.name || "Setup Key",
|
name: keyName,
|
||||||
type: setupKey.type,
|
type: setupKey.type,
|
||||||
expires_in: setupKey.expires_in,
|
expires_in: setupKey.expires_in,
|
||||||
revoked: true,
|
revoked: true,
|
||||||
@@ -57,29 +61,28 @@ export default function SetupKeyActionCell({ setupKey }: Readonly<Props>) {
|
|||||||
mutate("/setup-keys");
|
mutate("/setup-keys");
|
||||||
mutate("/groups");
|
mutate("/groups");
|
||||||
}),
|
}),
|
||||||
loadingMessage: "Revoking the setup key...",
|
loadingMessage: t("revokeLoading"),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
const choice = await confirm({
|
const choice = await confirm({
|
||||||
title: `Delete '${setupKey?.name || "Setup Key"}'?`,
|
title: t("deleteConfirmTitle", { name: keyName }),
|
||||||
description:
|
description: t("deleteConfirmDescription"),
|
||||||
"Are you sure you want to delete the setup key? This action cannot be undone.",
|
confirmText: tCommon("delete"),
|
||||||
confirmText: "Delete",
|
cancelText: tCommon("cancel"),
|
||||||
cancelText: "Cancel",
|
|
||||||
type: "danger",
|
type: "danger",
|
||||||
});
|
});
|
||||||
if (!choice) return;
|
if (!choice) return;
|
||||||
|
|
||||||
notify({
|
notify({
|
||||||
title: setupKey?.name || "Setup Key",
|
title: keyName,
|
||||||
description: "Setup key was successfully deleted",
|
description: t("deleteSuccessDescription"),
|
||||||
promise: request.del().then(() => {
|
promise: request.del().then(() => {
|
||||||
mutate("/setup-keys");
|
mutate("/setup-keys");
|
||||||
mutate("/groups");
|
mutate("/groups");
|
||||||
}),
|
}),
|
||||||
loadingMessage: "Deleting the setup key...",
|
loadingMessage: t("deleteLoading"),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,7 +99,7 @@ export default function SetupKeyActionCell({ setupKey }: Readonly<Props>) {
|
|||||||
<Button
|
<Button
|
||||||
variant={"secondary"}
|
variant={"secondary"}
|
||||||
className={"!px-3"}
|
className={"!px-3"}
|
||||||
aria-label={"Open actions menu"}
|
aria-label={t("openActionsMenu")}
|
||||||
data-testid={"setup-key-actions"}
|
data-testid={"setup-key-actions"}
|
||||||
>
|
>
|
||||||
<MoreVertical size={16} className={"shrink-0"} />
|
<MoreVertical size={16} className={"shrink-0"} />
|
||||||
@@ -111,7 +114,7 @@ export default function SetupKeyActionCell({ setupKey }: Readonly<Props>) {
|
|||||||
>
|
>
|
||||||
<div className={"flex gap-3 items-center"}>
|
<div className={"flex gap-3 items-center"}>
|
||||||
<Undo2Icon size={14} className={"shrink-0"} />
|
<Undo2Icon size={14} className={"shrink-0"} />
|
||||||
Revoke
|
{t("revoke")}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
@@ -123,7 +126,7 @@ export default function SetupKeyActionCell({ setupKey }: Readonly<Props>) {
|
|||||||
>
|
>
|
||||||
<div className={"flex gap-3 items-center"}>
|
<div className={"flex gap-3 items-center"}>
|
||||||
<Trash2 size={14} className={"shrink-0"} />
|
<Trash2 size={14} className={"shrink-0"} />
|
||||||
Delete
|
{tCommon("delete")}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { notify } from "@components/Notification";
|
import { notify } from "@components/Notification";
|
||||||
import { useApiCall } from "@utils/api";
|
import { useApiCall } from "@utils/api";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useSWRConfig } from "swr";
|
import { useSWRConfig } from "swr";
|
||||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||||
@@ -11,6 +12,7 @@ type Props = {
|
|||||||
setupKey: SetupKey;
|
setupKey: SetupKey;
|
||||||
};
|
};
|
||||||
export default function SetupKeyGroupsCell({ setupKey }: Readonly<Props>) {
|
export default function SetupKeyGroupsCell({ setupKey }: Readonly<Props>) {
|
||||||
|
const t = useTranslations("setupKeys");
|
||||||
const [modal, setModal] = useState(false);
|
const [modal, setModal] = useState(false);
|
||||||
const { permission } = usePermissions();
|
const { permission } = usePermissions();
|
||||||
const request = useApiCall<SetupKey>("/setup-keys/" + setupKey.id);
|
const request = useApiCall<SetupKey>("/setup-keys/" + setupKey.id);
|
||||||
@@ -19,11 +21,11 @@ export default function SetupKeyGroupsCell({ setupKey }: Readonly<Props>) {
|
|||||||
const groups = await Promise.all(promises);
|
const groups = await Promise.all(promises);
|
||||||
|
|
||||||
notify({
|
notify({
|
||||||
title: setupKey?.name || "Setup Key",
|
title: setupKey?.name || t("key"),
|
||||||
description: "Groups of the setup key were successfully saved",
|
description: t("groupsSavedDescription"),
|
||||||
promise: request
|
promise: request
|
||||||
.put({
|
.put({
|
||||||
name: setupKey?.name || "Setup Key",
|
name: setupKey?.name || t("key"),
|
||||||
type: setupKey.type,
|
type: setupKey.type,
|
||||||
expires_in: setupKey.expires_in,
|
expires_in: setupKey.expires_in,
|
||||||
revoked: setupKey.revoked,
|
revoked: setupKey.revoked,
|
||||||
@@ -37,17 +39,15 @@ export default function SetupKeyGroupsCell({ setupKey }: Readonly<Props>) {
|
|||||||
mutate("/setup-keys");
|
mutate("/setup-keys");
|
||||||
mutate("/groups");
|
mutate("/groups");
|
||||||
}),
|
}),
|
||||||
loadingMessage: "Saving the groups of the setup key...",
|
loadingMessage: t("groupsSaving"),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
permission.groups.read && (
|
permission.groups.read && (
|
||||||
<GroupsRow
|
<GroupsRow
|
||||||
label={"Auto-assigned Groups"}
|
label={t("autoAssignedGroups")}
|
||||||
description={
|
description={t("autoAssignedGroupsDescription")}
|
||||||
"These groups will be automatically assigned to peers enrolled with this key"
|
|
||||||
}
|
|
||||||
groups={setupKey.auto_groups || []}
|
groups={setupKey.auto_groups || []}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
hideAllGroup={true}
|
hideAllGroup={true}
|
||||||
|
|||||||
Reference in New Issue
Block a user