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:
2026-06-24 07:44:15 +00:00
parent 46d20e5877
commit 3bb1a61c3f
12 changed files with 1359 additions and 312 deletions

136
package-lock.json generated
View File

@@ -32,7 +32,7 @@
"@tanstack/react-table": "^8.10.7",
"@types/crypto-js": "^4.2.2",
"@types/d3": "^7.4.3",
"@types/lodash": "^4.14.200",
"@types/lodash": "4.17.24",
"@types/node": "20.10.6",
"@types/react": "^19",
"@types/react-dom": "^19",
@@ -47,6 +47,7 @@
"classnames": "^2.5.1",
"clsx": "^2.0.0",
"cmdk": "^1.1.1",
"cross-env": "^7.0.3",
"crypto-js": "^4.2.0",
"d3": "^7.9.0",
"date-fns": "^2.30.0",
@@ -55,16 +56,18 @@
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"framer-motion": "^12.29.2",
"ip-address": "^10.1.0",
"ip-address": "^10.2.0",
"ip-cidr": "^3.1.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.23",
"js-cookie": "^3.0.7",
"lodash": "4.18.1",
"lucide-react": "^0.566.0",
"next": "16.1.7",
"next-intl": "^4.13.0",
"next-themes": "^0.2.1",
"punycode": "^2.3.1",
"react": "^19.2.4",
"react-chartjs-2": "^5.3.0",
"react-confetti-explosion": "^3.0.3",
"react-day-picker": "^9.13.0",
"react-dom": "^19.2.4",
"react-ga4": "^2.1.0",
@@ -84,6 +87,7 @@
},
"devDependencies": {
"@faker-js/faker": "^9.5.1",
"@playwright/test": "^1.52.0",
"@types/chroma-js": "^3.1.1",
"@types/js-cookie": "^3.0.6",
"eslint": "^9.39.1",
@@ -1738,6 +1742,22 @@
"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": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
@@ -3562,9 +3582,9 @@
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==",
"version": "4.17.24",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
"integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
"license": "MIT"
},
"node_modules/@types/node": {
@@ -4850,6 +4870,24 @@
"dev": true,
"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": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -6760,9 +6798,9 @@
}
},
"node_modules/ip-address": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
"license": "MIT",
"engines": {
"node": ">= 12"
@@ -7258,13 +7296,10 @@
}
},
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"license": "MIT",
"engines": {
"node": ">=14"
}
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz",
"integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==",
"license": "MIT"
},
"node_modules/js-tokens": {
"version": "4.0.0",
@@ -8084,6 +8119,53 @@
"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": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz",
@@ -8330,6 +8412,26 @@
"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": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.13.0.tgz",

View File

@@ -472,6 +472,25 @@ export default {
"Access other networks without installing NetBird on every resource.",
remoteJobsDesc:
"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: {
title: "Policies",
@@ -1552,6 +1571,25 @@ export default {
groups: "Groups",
usage: "Usage",
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: {
title: "Activity",
@@ -1569,6 +1607,281 @@ export default {
ipAddress: "IP Address",
details: "Details",
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: {
title: "Control Center",

View File

@@ -456,6 +456,24 @@ export default {
networkRoutesDesc: "无需在每个资源上安装 NetBird 即可访问其他网络。",
remoteJobsDesc:
"远程触发此节点上的操作,如调试包或其他任务,无需 CLI 访问。",
revoke: "撤销",
bypassCompliance: "绕过合规检查",
bypassComplianceConfirmTitle: "绕过节点 '{name}' 的合规检查?",
bypassComplianceConfirmDescription:
"此操作将覆盖合规检查,允许此节点进行连接。当设备恢复合规时,绕过将自动移除。",
bypassComplianceSuccess: "已为 {name} 绕过合规检查",
bypassComplianceSuccessDescription: "此节点现在可以连接到其他节点。",
bypassComplianceLoading: "正在绕过合规检查...",
revokeBypass: "撤销绕过",
revokeBypassConfirmTitle: "撤销节点 '{name}' 的合规绕过?",
revokeBypassConfirmDescription:
"此节点将接受正常的合规验证。如果仍然不合规,它将失去网络访问权限。",
revokeBypassSuccess: "合规绕过已撤销",
revokeBypassSuccessDescription:
"节点 {name} 现在接受正常的合规验证。",
revokeBypassLoading: "正在撤销合规绕过...",
bypassTooltip:
"绕过 {integrationName} 合规检查并允许此节点连接。当设备恢复合规时,绕过将自动移除。",
},
policies: {
title: "策略",
@@ -1459,6 +1477,25 @@ export default {
groups: "组",
usage: "使用情况",
lastUsedOn: "上次使用于",
// SetupKeyActionCell
revoke: "撤销",
openActionsMenu: "打开操作菜单",
revokeConfirmTitle: "撤销安装密钥「{name}」?",
revokeConfirmDescription:
"确定要撤销此安装密钥吗?此操作无法撤销。",
revokeSuccessDescription: "安装密钥已成功撤销",
revokeLoading: "正在撤销安装密钥...",
deleteConfirmTitle: "删除安装密钥「{name}」?",
deleteConfirmDescription:
"确定要删除此安装密钥吗?此操作无法撤销。",
deleteSuccessDescription: "安装密钥已成功删除",
deleteLoading: "正在删除安装密钥...",
// SetupKeyGroupsCell
autoAssignedGroups: "自动分配的组",
autoAssignedGroupsDescription:
"使用此密钥注册的节点将自动分配这些组",
groupsSavedDescription: "安装密钥的组已成功保存",
groupsSaving: "正在保存安装密钥的组...",
},
activity: {
title: "活动",
@@ -1476,6 +1513,281 @@ export default {
ipAddress: "IP 地址",
details: "详情",
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: {
title: "控制中心",

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ import TextWithTooltip from "@components/ui/TextWithTooltip";
import { cn, generateColorFromUser } from "@utils/helpers";
import dayjs from "dayjs";
import { AlertCircle, ArrowUpRight, Cog, PlusIcon, XIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import React, { useMemo } from "react";
import { useUsers } from "@/contexts/UsersProvider";
import { ActivityEvent } from "@/interfaces/ActivityEvent";
@@ -23,6 +24,7 @@ const ActionIcons: Record<ActionColor, React.ReactNode> = {
export const ActivityEntryRow = ({ event }: { event: ActivityEvent }) => {
const { users } = useUsers();
const t = useTranslations("activity");
const getActivityUser = () => {
let user;
@@ -95,7 +97,7 @@ export const ActivityEntryRow = ({ event }: { event: ActivityEvent }) => {
<span className={"text-sm text-nb-gray-200"}>
<TextWithTooltip
text={user?.name || user?.id || "System"}
text={user?.name || user?.id || t("system")}
maxChars={20}
/>
</span>
@@ -105,7 +107,7 @@ export const ActivityEntryRow = ({ event }: { event: ActivityEvent }) => {
{isExternal && (
<span className={"flex items-center"}>
<SmallBadge
text={"External"}
text={t("external")}
variant={"sky"}
className={
"text-[10px] py-[0.2rem] px-1.5 rounded-full leading-none -top-0"

View File

@@ -7,6 +7,7 @@ import { cn } from "@utils/helpers";
import { Command, CommandGroup, CommandInput, CommandList } from "cmdk";
import { trim, uniqBy } from "lodash";
import { ChevronsUpDown, Layers, SearchIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import * as React from "react";
import { useMemo, useState } from "react";
import { useElementSize } from "@/hooks/useElementSize";
@@ -27,6 +28,7 @@ export function ActivityEventCodeSelector({
popoverWidth = 400,
events,
}: MultiSelectProps) {
const t = useTranslations("activity");
const searchRef = React.useRef<HTMLInputElement>(null);
const [inputRef, { width }] = useElementSize<HTMLButtonElement>();
const [search, setSearch] = useState("");
@@ -50,7 +52,7 @@ export function ActivityEventCodeSelector({
activity_code: event.activity_code,
activity: event.activity,
group: event.activity_code.startsWith("service.user")
? "Service User"
? t("serviceUser")
: event.activity_code.split(".")[0],
};
});
@@ -81,9 +83,9 @@ export function ActivityEventCodeSelector({
<Layers size={16} className={"shrink-0"} />
<div className={"w-full flex justify-between"}>
{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"}>
<ChevronsUpDown size={18} className={"shrink-0"} />
@@ -122,7 +124,7 @@ export function ActivityEventCodeSelector({
ref={searchRef}
value={search}
onValueChange={setSearch}
placeholder={"Search event..."}
placeholder={t("searchEvent")}
/>
<div
className={

View File

@@ -144,7 +144,7 @@ export default function ActivityTable({
events={events ?? []}
/>
),
formatChip: (v) => formatActivityTypeChip(v as string[] | undefined),
formatChip: (v) => formatActivityTypeChip(v as string[] | undefined, t),
},
{
id: "initiator_email",

View File

@@ -7,6 +7,7 @@ import { cn } from "@utils/helpers";
import { Command, CommandGroup, CommandInput, CommandList } from "cmdk";
import { trim, uniqBy } from "lodash";
import { SearchIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import * as React from "react";
import { useMemo, useRef } from "react";
import { ActivityEvent } from "@/interfaces/ActivityEvent";
@@ -34,6 +35,7 @@ export function ActivityTypePicker({
onChange,
events,
}: Readonly<Props>) {
const t = useTranslations("activity");
const searchRef = useRef<HTMLInputElement>(null);
const selected = value ?? [];
@@ -43,7 +45,7 @@ export function ActivityTypePicker({
activity_code: event.activity_code,
activity: event.activity,
group: event.activity_code.startsWith("service.user")
? "Service User"
? t("serviceUser")
: event.activity_code.split(".")[0],
}));
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",
)}
ref={searchRef}
placeholder={"Search event..."}
placeholder={t("searchEvent")}
/>
<div
className={
@@ -150,8 +152,9 @@ export function ActivityTypePicker({
export function formatActivityTypeChip(
value: string[] | undefined,
t?: (key: string, params?: Record<string, any>) => string,
): string | null {
if (!value || value.length === 0) return null;
if (value.length === 1) return value[0];
return `${value.length} types`;
return t ? t("typeCount", { count: value.length }) : `${value.length} types`;
}

View File

@@ -9,6 +9,7 @@ import { useSearch } from "@hooks/useSearch";
import { generateColorFromString } from "@utils/helpers";
import { sortBy, uniqBy } from "lodash";
import { ChevronsUpDown, Cog, UserCircle2 } from "lucide-react";
import { useTranslations } from "next-intl";
import * as React from "react";
import { useMemo, useState } from "react";
import { useElementSize } from "@/hooks/useElementSize";
@@ -48,11 +49,12 @@ export function UsersDropdownSelector({
popoverWidth = 250,
options,
}: Readonly<Props>) {
const t = useTranslations("activity");
const [filteredItems, search, setSearch] = useSearch(
options.concat({
id: "all-users",
name: "All Users",
email: "Include all users",
name: t("allUsers"),
email: t("includeAllUsers"),
}),
searchPredicate,
{ filter: true, debounce: 150 },
@@ -107,7 +109,7 @@ export function UsersDropdownSelector({
{!selectedUser ? (
<React.Fragment>
<UserCircle2 size={16} />
All Users
{t("allUsers")}
</React.Fragment>
) : (
<React.Fragment>
@@ -136,7 +138,7 @@ export function UsersDropdownSelector({
<TextWithTooltip
text={
selectedUser?.email === "NetBird"
? "System"
? t("system")
: selectedUser?.name
}
maxChars={20}
@@ -165,14 +167,14 @@ export function UsersDropdownSelector({
<DropdownInput
value={search}
onChange={setSearch}
placeholder={"Search user..."}
placeholder={t("searchUser")}
hideEnterIcon={true}
/>
{options.length == 0 && !search && (
<div className={"max-w-xs mx-auto"}>
<DropdownInfoText>
{"No users available to select."}
{t("noUsersAvailable")}
</DropdownInfoText>
</div>
)}
@@ -180,7 +182,7 @@ export function UsersDropdownSelector({
{filteredItems.length == 0 && search != "" && (
<div className={"px-10"}>
<DropdownInfoText>
There are no users matching your search.
{t("noUsersMatching")}
</DropdownInfoText>
</div>
)}
@@ -227,7 +229,7 @@ export function UsersDropdownSelector({
>
<TextWithTooltip
text={
isSystemUser ? "System" : user?.name || user?.id
isSystemUser ? t("system") : user?.name || user?.id
}
maxChars={20}
/>
@@ -246,7 +248,7 @@ export function UsersDropdownSelector({
{user.external && (
<span className={"flex items-center ml-auto relative"}>
<SmallBadge
text={"External"}
text={t("external")}
variant={"sky"}
className={
"text-[8.5px] py-[0.15rem] px-[.32rem] leading-none rounded-full -top-0"

View File

@@ -1,3 +1,5 @@
"use client";
import Button from "@components/Button";
import {
DropdownMenu,
@@ -7,6 +9,7 @@ import {
DropdownMenuTrigger,
} from "@components/DropdownMenu";
import FullTooltip from "@components/FullTooltip";
import InlineLink from "@components/InlineLink";
import { notify } from "@components/Notification";
import { getOperatingSystem } from "@hooks/useOperatingSystem";
import { IconInfoCircle } from "@tabler/icons-react";
@@ -21,19 +24,19 @@ import {
TimerResetIcon,
Trash2,
} from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import React, { useMemo } from "react";
import { useSWRConfig } from "swr";
import { useBypass, useBypassedPeers } from "@/cloud/edr/useBypass";
import { usePeer } from "@/contexts/PeerProvider";
import { usePermissions } from "@/contexts/PermissionsProvider";
import { useDialog } from "@/contexts/DialogProvider";
import { OperatingSystem } from "@/interfaces/OperatingSystem";
import { ExitNodeDropdownButton } from "@/modules/exit-node/ExitNodeDropdownButton";
import { useIntegrations } from "@/modules/integrations/edr/useIntegrations";
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
import InlineLink from "@components/InlineLink";
import { useDialog } from "@/contexts/DialogProvider";
export default function PeerActionCell() {
const { peer, deletePeer, update, toggleSSH, setSSHInstructionsModal } =
@@ -42,6 +45,8 @@ export default function PeerActionCell() {
const { mutate } = useSWRConfig();
const { permission } = usePermissions();
const { confirm } = useDialog();
const t = useTranslations("peers");
const tCommon = useTranslations("common");
// Approval / EDR-bypass state. We pull this directly so the action
// menu can offer Approve / Bypass / Revoke without the inline badges
@@ -55,16 +60,16 @@ export default function PeerActionCell() {
const approvePeer = async () => {
const choice = await confirm({
title: `Approve peer '${peer.name}'?`,
description: "Are you sure you want to approve this peer?",
confirmText: "Approve",
cancelText: "Cancel",
title: t("confirmApprove", { name: peer.name }),
description: t("confirmApproveDescription"),
confirmText: t("approve"),
cancelText: tCommon("cancel"),
type: "default",
});
if (!choice) return;
notify({
title: `Peer ${peer.name} approved`,
description: `This peer was approved and can now connect to other peers.`,
title: t("approveSuccess", { name: peer.name }),
description: t("approveSuccessDescription"),
promise: update({
name: peer.name,
ssh: peer.ssh_enabled,
@@ -74,45 +79,41 @@ export default function PeerActionCell() {
mutate("/peers");
mutate("/groups");
}),
loadingMessage: "Approving peer...",
loadingMessage: t("approveLoading"),
});
};
const handleBypassCompliance = async () => {
const choice = await confirm({
title: `Bypass compliance for '${peer.name}'?`,
description:
"This will override the compliance check and allow this peer to connect. " +
"The bypass will be automatically removed if the device becomes compliant.",
confirmText: "Bypass Compliance",
cancelText: "Cancel",
title: t("bypassComplianceConfirmTitle", { name: peer.name }),
description: t("bypassComplianceConfirmDescription"),
confirmText: t("bypassCompliance"),
cancelText: tCommon("cancel"),
type: "warning",
});
if (!choice || !peer.id) return;
notify({
title: `Compliance bypassed for ${peer.name}`,
description: `This peer can now connect to other peers.`,
title: t("bypassComplianceSuccess", { name: peer.name }),
description: t("bypassComplianceSuccessDescription"),
promise: bypassCompliance(peer.id),
loadingMessage: "Bypassing compliance...",
loadingMessage: t("bypassComplianceLoading"),
});
};
const handleRevokeBypass = async () => {
const choice = await confirm({
title: `Revoke compliance bypass for '${peer.name}'?`,
description:
"This peer will be subject to normal compliance validation. " +
"If still non-compliant, it will lose network access.",
confirmText: "Revoke",
cancelText: "Cancel",
title: t("revokeBypassConfirmTitle", { name: peer.name }),
description: t("revokeBypassConfirmDescription"),
confirmText: t("revoke"),
cancelText: tCommon("cancel"),
type: "warning",
});
if (!choice || !peer.id) return;
notify({
title: `Compliance bypass revoked`,
description: `Peer ${peer.name} is now subject to normal compliance validation.`,
title: t("revokeBypassSuccess"),
description: t("revokeBypassSuccessDescription", { name: peer.name }),
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 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;
notify({
title: `Session expiration is ${text}`,
description: `Session expiration for peer ${peer.name} was successfully ${text}.`,
title: t("loginExpirationUpdated", { state }),
description: t("loginExpirationUpdateDescription", {
name: peer.name,
state,
}),
promise: update({
loginExpiration: !peer.login_expiration_enabled,
inactivityExpiration: disableLoginExpiration
@@ -157,31 +163,28 @@ export default function PeerActionCell() {
mutate("/peers");
mutate("/groups");
}),
loadingMessage: "Updating session expiration...",
loadingMessage: t("loginExpirationUpdating"),
});
};
const disableDashboardSSH = async () => {
const choice = await confirm({
title: `Disable SSH Access?`,
title: t("disableSSHConfirmation"),
description: (
<div>
Starting from NetBird v0.61.0, once SSH access is disabled, you cannot
re-enable it again from the dashboard. You&apos;ll need to create an
explicit access control policy and update your NetBird client to
restore SSH functionality.{" "}
{t("disableSSHDescription")}{" "}
<InlineLink
href={"https://docs.netbird.io/manage/peers/ssh"}
target={"_blank"}
onClick={(e) => e.stopPropagation()}
>
Learn more
{tCommon("learnMore")}
<ExternalLinkIcon size={12} />
</InlineLink>
</div>
),
confirmText: "Disable",
cancelText: "Cancel",
confirmText: tCommon("disable"),
cancelText: tCommon("cancel"),
type: "warning",
maxWidthClass: "max-w-xl",
});
@@ -210,7 +213,7 @@ export default function PeerActionCell() {
>
<div className={"flex gap-3 items-center"}>
<MonitorIcon size={14} className={"shrink-0"} />
View Details
{t("viewDetails")}
</div>
</DropdownMenuItem>
@@ -221,7 +224,7 @@ export default function PeerActionCell() {
<DropdownMenuItem onClick={approvePeer}>
<div className={"flex gap-3 items-center"}>
<CheckCircle2 size={14} className={"shrink-0"} />
Approve
{t("approve")}
</div>
</DropdownMenuItem>
)}
@@ -230,16 +233,16 @@ export default function PeerActionCell() {
className={"w-full block"}
content={
<div className={"text-xs max-w-xs"}>
Bypass {activeIntegrationName} compliance check and
allow this peer to connect. The bypass is automatically
removed when the device becomes compliant.
{t("bypassTooltip", {
integrationName: activeIntegrationName,
})}
</div>
}
>
<DropdownMenuItem onClick={handleBypassCompliance}>
<div className={"flex gap-3 items-center w-full"}>
<ShieldCheck size={14} className={"shrink-0"} />
Bypass Compliance
{t("bypassCompliance")}
</div>
</DropdownMenuItem>
</FullTooltip>
@@ -248,7 +251,7 @@ export default function PeerActionCell() {
<DropdownMenuItem onClick={handleRevokeBypass}>
<div className={"flex gap-3 items-center"}>
<ShieldOff size={14} className={"shrink-0"} />
Revoke Bypass
{t("revokeBypass")}
</div>
</DropdownMenuItem>
)}
@@ -270,9 +273,7 @@ export default function PeerActionCell() {
className={"flex gap-2 items-center !text-nb-gray-300 text-xs"}
>
<IconInfoCircle size={14} />
<span>
Expiration is disabled for all peers added with an setup-key.
</span>
<span>{t("expirationDisabledTooltip")}</span>
</div>
}
className={"w-full block"}
@@ -284,8 +285,9 @@ export default function PeerActionCell() {
>
<div className={"flex gap-3 items-center w-full"}>
<TimerResetIcon size={14} className={"shrink-0"} />
{peer.login_expiration_enabled ? "Disable" : "Enable"} Session
Expiration
{peer.login_expiration_enabled
? t("disableLoginExpiration")
: t("enableLoginExpiration")}
</div>
</DropdownMenuItem>
</FullTooltip>
@@ -302,7 +304,7 @@ export default function PeerActionCell() {
<div className={"flex gap-3 items-center w-full"}>
<TerminalSquare size={14} className={"shrink-0"} />
<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>
</DropdownMenuItem>
@@ -319,7 +321,7 @@ export default function PeerActionCell() {
>
<div className={"flex gap-3 items-center"}>
<Trash2 size={14} className={"shrink-0"} />
Delete
{tCommon("delete")}
</div>
</DropdownMenuItem>
</DropdownMenuContent>

View File

@@ -9,6 +9,7 @@ import {
import { notify } from "@components/Notification";
import { useApiCall } from "@utils/api";
import { MoreVertical, Trash2, Undo2Icon } from "lucide-react";
import { useTranslations } from "next-intl";
import * as React from "react";
import { useSWRConfig } from "swr";
import { useDialog } from "@/contexts/DialogProvider";
@@ -19,6 +20,8 @@ type Props = {
setupKey: SetupKey;
};
export default function SetupKeyActionCell({ setupKey }: Readonly<Props>) {
const t = useTranslations("setupKeys");
const tCommon = useTranslations("common");
const { confirm } = useDialog();
const request = useApiCall<SetupKey>("/setup-keys/" + setupKey.id);
const { mutate } = useSWRConfig();
@@ -28,23 +31,24 @@ export default function SetupKeyActionCell({ setupKey }: Readonly<Props>) {
!setupKey.revoked && setupKey.valid && permission.setup_keys.update;
const canDelete = permission.setup_keys.delete;
const keyName = setupKey?.name || t("key");
const handleRevoke = async () => {
const choice = await confirm({
title: `Revoke '${setupKey?.name || "Setup Key"}'?`,
description:
"Are you sure you want to revoke the setup key? This action cannot be undone.",
confirmText: "Revoke",
cancelText: "Cancel",
title: t("revokeConfirmTitle", { name: keyName }),
description: t("revokeConfirmDescription"),
confirmText: t("revoke"),
cancelText: tCommon("cancel"),
type: "danger",
});
if (!choice) return;
notify({
title: setupKey?.name || "Setup Key",
description: "Setup key was successfully revoked",
title: keyName,
description: t("revokeSuccessDescription"),
promise: request
.put({
name: setupKey?.name || "Setup Key",
name: keyName,
type: setupKey.type,
expires_in: setupKey.expires_in,
revoked: true,
@@ -57,29 +61,28 @@ export default function SetupKeyActionCell({ setupKey }: Readonly<Props>) {
mutate("/setup-keys");
mutate("/groups");
}),
loadingMessage: "Revoking the setup key...",
loadingMessage: t("revokeLoading"),
});
};
const handleDelete = async () => {
const choice = await confirm({
title: `Delete '${setupKey?.name || "Setup Key"}'?`,
description:
"Are you sure you want to delete the setup key? This action cannot be undone.",
confirmText: "Delete",
cancelText: "Cancel",
title: t("deleteConfirmTitle", { name: keyName }),
description: t("deleteConfirmDescription"),
confirmText: tCommon("delete"),
cancelText: tCommon("cancel"),
type: "danger",
});
if (!choice) return;
notify({
title: setupKey?.name || "Setup Key",
description: "Setup key was successfully deleted",
title: keyName,
description: t("deleteSuccessDescription"),
promise: request.del().then(() => {
mutate("/setup-keys");
mutate("/groups");
}),
loadingMessage: "Deleting the setup key...",
loadingMessage: t("deleteLoading"),
});
};
@@ -96,7 +99,7 @@ export default function SetupKeyActionCell({ setupKey }: Readonly<Props>) {
<Button
variant={"secondary"}
className={"!px-3"}
aria-label={"Open actions menu"}
aria-label={t("openActionsMenu")}
data-testid={"setup-key-actions"}
>
<MoreVertical size={16} className={"shrink-0"} />
@@ -111,7 +114,7 @@ export default function SetupKeyActionCell({ setupKey }: Readonly<Props>) {
>
<div className={"flex gap-3 items-center"}>
<Undo2Icon size={14} className={"shrink-0"} />
Revoke
{t("revoke")}
</div>
</DropdownMenuItem>
<DropdownMenuSeparator />
@@ -123,7 +126,7 @@ export default function SetupKeyActionCell({ setupKey }: Readonly<Props>) {
>
<div className={"flex gap-3 items-center"}>
<Trash2 size={14} className={"shrink-0"} />
Delete
{tCommon("delete")}
</div>
</DropdownMenuItem>
</DropdownMenuContent>

View File

@@ -1,5 +1,6 @@
import { notify } from "@components/Notification";
import { useApiCall } from "@utils/api";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { useSWRConfig } from "swr";
import { usePermissions } from "@/contexts/PermissionsProvider";
@@ -11,6 +12,7 @@ type Props = {
setupKey: SetupKey;
};
export default function SetupKeyGroupsCell({ setupKey }: Readonly<Props>) {
const t = useTranslations("setupKeys");
const [modal, setModal] = useState(false);
const { permission } = usePermissions();
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);
notify({
title: setupKey?.name || "Setup Key",
description: "Groups of the setup key were successfully saved",
title: setupKey?.name || t("key"),
description: t("groupsSavedDescription"),
promise: request
.put({
name: setupKey?.name || "Setup Key",
name: setupKey?.name || t("key"),
type: setupKey.type,
expires_in: setupKey.expires_in,
revoked: setupKey.revoked,
@@ -37,17 +39,15 @@ export default function SetupKeyGroupsCell({ setupKey }: Readonly<Props>) {
mutate("/setup-keys");
mutate("/groups");
}),
loadingMessage: "Saving the groups of the setup key...",
loadingMessage: t("groupsSaving"),
});
};
return (
permission.groups.read && (
<GroupsRow
label={"Auto-assigned Groups"}
description={
"These groups will be automatically assigned to peers enrolled with this key"
}
label={t("autoAssignedGroups")}
description={t("autoAssignedGroupsDescription")}
groups={setupKey.auto_groups || []}
onSave={handleSave}
hideAllGroup={true}