diff --git a/src/api/admin-agents.ts b/src/api/admin-agents.ts index 72aede4..e6b845b 100644 --- a/src/api/admin-agents.ts +++ b/src/api/admin-agents.ts @@ -94,6 +94,14 @@ export async function putAgentAdminUserRoles( ); } +export async function deleteAgentAdminUser( + adminUserId: number, +): Promise<{ deleted: boolean; id: number }> { + return adminRequest.delete<{ deleted: boolean; id: number }>( + `${A}/agent-admin-users/${adminUserId}`, + ); +} + export async function getAgentDelegationGrants( agentNodeId: number, ): Promise { diff --git a/src/components/admin/admin-permission-package-selector.tsx b/src/components/admin/admin-permission-package-selector.tsx index ccbc86f..2874854 100644 --- a/src/components/admin/admin-permission-package-selector.tsx +++ b/src/components/admin/admin-permission-package-selector.tsx @@ -37,12 +37,18 @@ type RenderGroup = { const PACKAGE_LEVEL_ORDER: Record = { view: 10, + node_view: 10, + role_view: 11, + user_view: 12, review: 20, export: 20, manage: 30, + node_manage: 30, + role_manage: 31, + user_manage: 32, config: 30, control: 30, - reopen: 30, + reopen: 32, special: 40, }; diff --git a/src/i18n/locales/en/adminUsers.json b/src/i18n/locales/en/adminUsers.json index b0e732e..6c92edd 100644 --- a/src/i18n/locales/en/adminUsers.json +++ b/src/i18n/locales/en/adminUsers.json @@ -73,6 +73,12 @@ }, "permissionLevels": { "view": "View", + "node_view": "Nodes · View", + "node_manage": "Nodes · Manage", + "role_view": "Roles · View", + "role_manage": "Roles · Manage", + "user_view": "Accounts · View", + "user_manage": "Accounts · Manage", "manage": "Manage", "review": "Review", "export": "Export", diff --git a/src/i18n/locales/en/agents.json b/src/i18n/locales/en/agents.json index dfb5258..f742409 100644 --- a/src/i18n/locales/en/agents.json +++ b/src/i18n/locales/en/agents.json @@ -9,6 +9,13 @@ "editNode": "Edit node", "deleteNode": "Delete node", "deleteNodeConfirm": "This action cannot be undone. Make sure the node has no children, users, or roles.", + "deleteNodeBlockedHint": "Remove child agents, roles, and accounts before deleting this node", + "deleteNodeBlockedPrefix": "Cannot delete yet: ", + "deleteBlocked": { + "children": "{{count}} child agent(s) remain", + "roles": "{{count}} role(s) must be removed in the Roles tab first", + "users": "{{count}} bound account(s) remain" + }, "code": "Code", "name": "Name", "depth": "Depth", @@ -48,6 +55,7 @@ "deleteSuccess": "Deleted role {{name}}", "permissionSaveSuccess": "Permissions updated", "readOnlyTemplate": "Read-only template", + "inUse": "{{count}} in use", "permissionSubsetHint": "Only permissions you hold can be assigned" }, "users": { @@ -57,6 +65,8 @@ "password": "Password", "roles": "Roles", "createSuccess": "Created account {{name}}", - "roleSaveSuccess": "Roles updated for {{name}}" + "roleSaveSuccess": "Roles updated for {{name}}", + "deleteConfirm": "This admin will no longer be able to sign in. This cannot be undone.", + "deleteSuccess": "Deleted account {{name}}" } } diff --git a/src/i18n/locales/ne/adminUsers.json b/src/i18n/locales/ne/adminUsers.json index dcbbf44..e036dd2 100644 --- a/src/i18n/locales/ne/adminUsers.json +++ b/src/i18n/locales/ne/adminUsers.json @@ -73,6 +73,12 @@ }, "permissionLevels": { "view": "हेर्नुहोस्", + "node_view": "नोड · हेर्नुहोस्", + "node_manage": "नोड · व्यवस्थापन", + "role_view": "भूमिका · हेर्नुहोस्", + "role_manage": "भूमिका · व्यवस्थापन", + "user_view": "खाता · हेर्नुहोस्", + "user_manage": "खाता · व्यवस्थापन", "manage": "व्यवस्थापन", "review": "समीक्षा", "export": "निर्यात", diff --git a/src/i18n/locales/ne/agents.json b/src/i18n/locales/ne/agents.json index 708d6f8..0eb0d8e 100644 --- a/src/i18n/locales/ne/agents.json +++ b/src/i18n/locales/ne/agents.json @@ -9,6 +9,13 @@ "editNode": "Edit node", "deleteNode": "Delete node", "deleteNodeConfirm": "This action cannot be undone. Make sure the node has no children, users, or roles.", + "deleteNodeBlockedHint": "पहिले चाइल्ड एजेन्ट, भूमिका र खाता हटाउनुहोस्", + "deleteNodeBlockedPrefix": "अहिले मेटाउन मिल्दैन: ", + "deleteBlocked": { + "children": "{{count}} वटा चाइल्ड एजेन्ट बाँकी", + "roles": "{{count}} वटा भूमिका पहिले हटाउनुहोस्", + "users": "{{count}} वटा खाता बाँकी" + }, "code": "Code", "name": "Name", "depth": "Depth", @@ -48,6 +55,7 @@ "deleteSuccess": "Deleted role {{name}}", "permissionSaveSuccess": "Permissions updated", "readOnlyTemplate": "Read-only template", + "inUse": "{{count}} प्रयोगमा", "permissionSubsetHint": "Only permissions you hold can be assigned" }, "users": { @@ -57,6 +65,8 @@ "password": "Password", "roles": "Roles", "createSuccess": "Created account {{name}}", - "roleSaveSuccess": "Roles updated for {{name}}" + "roleSaveSuccess": "Roles updated for {{name}}", + "deleteConfirm": "यो खाता अब लगइन गर्न सक्दैन।", + "deleteSuccess": "खाता {{name}} मेटियो" } } diff --git a/src/i18n/locales/zh/adminUsers.json b/src/i18n/locales/zh/adminUsers.json index fd3952b..f2d6548 100644 --- a/src/i18n/locales/zh/adminUsers.json +++ b/src/i18n/locales/zh/adminUsers.json @@ -73,6 +73,12 @@ }, "permissionLevels": { "view": "查看", + "node_view": "节点·查看", + "node_manage": "节点·管理", + "role_view": "角色·查看", + "role_manage": "角色·管理", + "user_view": "账号·查看", + "user_manage": "账号·管理", "manage": "管理", "review": "审核", "export": "导出", diff --git a/src/i18n/locales/zh/agents.json b/src/i18n/locales/zh/agents.json index 7c414a2..6d91d13 100644 --- a/src/i18n/locales/zh/agents.json +++ b/src/i18n/locales/zh/agents.json @@ -9,6 +9,13 @@ "editNode": "编辑节点", "deleteNode": "删除节点", "deleteNodeConfirm": "删除后不可恢复,请确认该节点无下级、无账号、无角色绑定。", + "deleteNodeBlockedHint": "请先删除下级代理、角色与账号后再删除本节点", + "deleteNodeBlockedPrefix": "暂不可删除:", + "deleteBlocked": { + "children": "仍有 {{count}} 个下级代理", + "roles": "仍有 {{count}} 个角色需先在「角色」里删除", + "users": "仍有 {{count}} 个账号需先处理" + }, "code": "编码", "name": "名称", "depth": "层级", @@ -48,6 +55,7 @@ "deleteSuccess": "已删除角色 {{name}}", "permissionSaveSuccess": "权限已更新", "readOnlyTemplate": "只读模板", + "inUse": "{{count}} 人使用中", "permissionSubsetHint": "只能分配您当前拥有的权限", "selectedCount": "已选 {{selected}} / {{total}} 项", "groupSelectedCount": "已选 {{selected}} / {{total}}", @@ -61,6 +69,8 @@ "password": "密码", "roles": "角色", "createSuccess": "已创建账号 {{name}}", - "roleSaveSuccess": "已更新 {{name}} 的角色" + "roleSaveSuccess": "已更新 {{name}} 的角色", + "deleteConfirm": "删除后该管理员将无法登录,且不可恢复。", + "deleteSuccess": "已删除账号 {{name}}" } } diff --git a/src/lib/admin-permission-packages.ts b/src/lib/admin-permission-packages.ts index 8286051..a4e4651 100644 --- a/src/lib/admin-permission-packages.ts +++ b/src/lib/admin-permission-packages.ts @@ -15,16 +15,12 @@ export const ADMIN_PERMISSION_PACKAGES: Record { key: "manage", label: "管理", slugs: ["prd.admin_role.manage"] }, ], agents: [ - { - key: "view", - label: "查看", - slugs: ["prd.agent.view", "prd.agent.role.view", "prd.agent.user.view"], - }, - { - key: "manage", - label: "管理", - slugs: ["prd.agent.manage", "prd.agent.role.manage", "prd.agent.user.manage"], - }, + { key: "node_view", label: "节点·查看", slugs: ["prd.agent.view"] }, + { key: "node_manage", label: "节点·管理", slugs: ["prd.agent.manage"] }, + { key: "role_view", label: "角色·查看", slugs: ["prd.agent.role.view"] }, + { key: "role_manage", label: "角色·管理", slugs: ["prd.agent.role.manage"] }, + { key: "user_view", label: "账号·查看", slugs: ["prd.agent.user.view"] }, + { key: "user_manage", label: "账号·管理", slugs: ["prd.agent.user.manage"] }, ], players: [ { diff --git a/src/modules/admin-roles/admin-roles-console.tsx b/src/modules/admin-roles/admin-roles-console.tsx index ae51029..0cf28b4 100644 --- a/src/modules/admin-roles/admin-roles-console.tsx +++ b/src/modules/admin-roles/admin-roles-console.tsx @@ -55,9 +55,8 @@ function permissionGroupLabel(key: string, fallback: string, t: (key: string) => return translated === `permissionGroups.${key}` ? fallback : translated; } -function permissionPackageLabel(key: string, fallback: string, t: (key: string) => string): string { - const translated = t(`permissionLevels.${key}`); - return translated === `permissionLevels.${key}` ? fallback : translated; +function permissionPackageLabel(key: string, fallback: string, t: (key: string, options?: { defaultValue?: string }) => string): string { + return t(`permissionLevels.${key}`, { defaultValue: fallback }); } export function AdminRolesConsole(): React.ReactElement { diff --git a/src/modules/agents/agents-console.tsx b/src/modules/agents/agents-console.tsx index f585e72..42cfa5c 100644 --- a/src/modules/agents/agents-console.tsx +++ b/src/modules/agents/agents-console.tsx @@ -9,6 +9,8 @@ import { useConfirmAction } from "@/hooks/use-confirm-action"; import { toast } from "sonner"; import { + deleteAgentAdminUser, + deleteAgentNode, deleteAgentRole, getAgentNodeAdminUsers, getAgentNodeRoles, @@ -81,10 +83,9 @@ function permissionGroupLabel(key: string, fallback: string, t: (key: string, op function permissionPackageLabel( key: string, fallback: string, - t: (key: string, options?: Record) => string, + t: (key: string, options?: { defaultValue?: string }) => string, ): string { - const translated = t(`adminUsers:permissionLevels.${key}`); - return translated === `adminUsers:permissionLevels.${key}` ? fallback : translated; + return t(`adminUsers:permissionLevels.${key}`, { defaultValue: fallback }); } function flattenTree(nodes: AgentNodeRow[]): AgentNodeRow[] { @@ -269,6 +270,43 @@ export function AgentsConsole(): React.ReactElement { selected !== null && !selected.is_root && (isSuperAdmin || profile?.agent?.id === selected.parent_id); + const blockingCustomRoleCount = useMemo( + () => roles.filter((role) => !role.is_read_only_template).length, + [roles], + ); + const deleteBlockReasons = useMemo(() => { + if (!selected || selected.is_root) { + return []; + } + const reasons: string[] = []; + if (selectedChildrenCount > 0) { + reasons.push( + t("deleteBlocked.children", { + count: selectedChildrenCount, + defaultValue: "仍有 {{count}} 个下级代理", + }), + ); + } + if (blockingCustomRoleCount > 0) { + reasons.push( + t("deleteBlocked.roles", { + count: blockingCustomRoleCount, + defaultValue: "仍有 {{count}} 个可编辑角色需先删除", + }), + ); + } + if (users.length > 0) { + reasons.push( + t("deleteBlocked.users", { + count: users.length, + defaultValue: "仍有 {{count}} 个绑定账号", + }), + ); + } + return reasons; + }, [blockingCustomRoleCount, selected, selectedChildrenCount, t, users.length]); + const canDeleteSelectedNode = + canManageNode && selected !== null && !selected.is_root && deleteBlockReasons.length === 0; const defaultDetailTab = canViewRoles ? "roles" : canViewUsers ? "users" : canManageDelegation ? "delegation" : "roles"; const assignablePermissionSlugs = useMemo(() => { @@ -332,13 +370,15 @@ export function AgentsConsole(): React.ReactElement { }, [selectedId, tRef]); const loadDetail = useCallback(async (nodeId: number) => { - if (canViewRoles) { + const needRoleRows = canViewRoles || canManageNode; + const needUserRows = canViewUsers || canManageNode; + if (needRoleRows) { const roleData = await getAgentNodeRoles(nodeId); setRoles(roleData.items); } else { setRoles([]); } - if (canViewUsers) { + if (needUserRows) { const userData = await getAgentNodeAdminUsers(nodeId); setUsers(userData.items); } else { @@ -474,7 +514,9 @@ export function AgentsConsole(): React.ReactElement { } setPermSaving(true); try { - await putAgentRolePermissions(permRoleId, draftPerms); + const result = await putAgentRolePermissions(permRoleId, draftPerms); + setDraftPerms([...result.permission_slugs].sort()); + setRoles((prev) => prev.map((role) => (role.id === result.id ? result : role))); toast.success(t("roles.permissionSaveSuccess")); setPermDialogOpen(false); if (selectedId !== null) { @@ -672,6 +714,46 @@ export function AgentsConsole(): React.ReactElement { {t("editNode")} ) : null} + {canManageNode && !selected.is_root ? ( + + ) : null} {canManageNode ? ( ) : null} + {canManageNode && !selected.is_root && deleteBlockReasons.length > 0 ? ( +

+ {t("deleteNodeBlockedPrefix", { defaultValue: "暂不可删除:" })} + {deleteBlockReasons.join(";")} +

+ ) : null} {canViewRoles ? ( @@ -717,41 +805,52 @@ export function AgentsConsole(): React.ReactElement { {role.user_count} {canManageRoles && !role.is_read_only_template ? ( - { - setPermRoleId(role.id); - setDraftPerms([...role.permission_slugs]); - setPermDialogOpen(true); +
+ {(role.user_count ?? 0) > 0 ? ( + + {t("roles.inUse", { + count: role.user_count ?? 0, + defaultValue: "{{count}} 人使用中", + })} + + ) : null} + { + setPermRoleId(role.id); + setDraftPerms([...role.permission_slugs]); + setPermDialogOpen(true); + }, }, - }, - { - key: "delete", - label: t("common:actions.delete", { defaultValue: "Delete" }), - icon: Trash2, - destructive: true, - onClick: () => { - requestConfirm({ - title: role.name, - description: t("common:confirm.deleteDescription", { - defaultValue: "This cannot be undone.", - }), - onConfirm: async () => { - await deleteAgentRole(role.id); - toast.success(t("roles.deleteSuccess", { name: role.name })); - if (selectedId !== null) { - await loadDetail(selectedId); - } - }, - }); + { + key: "delete", + label: t("common:actions.delete", { defaultValue: "Delete" }), + icon: Trash2, + destructive: true, + disabled: (role.user_count ?? 0) > 0, + onClick: () => { + requestConfirm({ + title: role.name, + description: t("common:confirm.deleteDescription", { + defaultValue: "This cannot be undone.", + }), + onConfirm: async () => { + await deleteAgentRole(role.id); + toast.success(t("roles.deleteSuccess", { name: role.name })); + if (selectedId !== null) { + await loadDetail(selectedId); + } + }, + }); + }, }, - }, - ]} - /> + ]} + /> +
) : role.is_read_only_template ? ( {t("roles.readOnlyTemplate")} @@ -793,6 +892,7 @@ export function AgentsConsole(): React.ReactElement { {t("users.username")} {t("name")} {t("users.roles")} + @@ -801,6 +901,41 @@ export function AgentsConsole(): React.ReactElement { {user.username} {user.nickname} {user.roles.join(", ") || "—"} + + {canManageUsers ? ( + { + requestConfirm({ + title: user.username, + description: t("users.deleteConfirm", { + defaultValue: "删除后该管理员将无法登录,且不可恢复。", + }), + confirmLabel: t("common:actions.delete", { + defaultValue: "Delete", + }), + confirmVariant: "destructive", + onConfirm: async () => { + await deleteAgentAdminUser(user.id); + toast.success( + t("users.deleteSuccess", { name: user.nickname }), + ); + if (selectedId !== null) { + await loadDetail(selectedId); + } + }, + }); + }, + }, + ]} + /> + ) : null} + ))} diff --git a/src/modules/config/config-version-actions.tsx b/src/modules/config/config-version-actions.tsx index 49be041..6702049 100644 --- a/src/modules/config/config-version-actions.tsx +++ b/src/modules/config/config-version-actions.tsx @@ -23,7 +23,7 @@ type ConfigVersionActionsProps = { export function ConfigVersionActions({ isDraft, - canManage = true, + canManage = false, loadingList = false, loadingDetail = false, saving = false,