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", "@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",

View File

@@ -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",

View File

@@ -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

View File

@@ -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"

View File

@@ -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={

View File

@@ -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",

View File

@@ -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`;
} }

View File

@@ -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"

View File

@@ -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&apos;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>

View File

@@ -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>

View File

@@ -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}