feat(admin, i18n): implement user deletion functionality and enhance permission management translations

Added a new API function to delete admin users, improving user management capabilities. Updated the admin permission package selector to include new permission levels for nodes, roles, and users. Enhanced multi-language support by adding translations for these new permission levels in English, Nepali, and Chinese. Additionally, improved the agents console to handle deletion confirmations and display relevant block reasons, ensuring a smoother user experience.
This commit is contained in:
2026-06-03 10:56:44 +08:00
parent ce27a3ec8a
commit bbb6f28459
12 changed files with 249 additions and 57 deletions

View File

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

View File

@@ -37,12 +37,18 @@ type RenderGroup = {
const PACKAGE_LEVEL_ORDER: Record<string, number> = {
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,
};

View File

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

View File

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

View File

@@ -73,6 +73,12 @@
},
"permissionLevels": {
"view": "हेर्नुहोस्",
"node_view": "नोड · हेर्नुहोस्",
"node_manage": "नोड · व्यवस्थापन",
"role_view": "भूमिका · हेर्नुहोस्",
"role_manage": "भूमिका · व्यवस्थापन",
"user_view": "खाता · हेर्नुहोस्",
"user_manage": "खाता · व्यवस्थापन",
"manage": "व्यवस्थापन",
"review": "समीक्षा",
"export": "निर्यात",

View File

@@ -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}} मेटियो"
}
}

View File

@@ -73,6 +73,12 @@
},
"permissionLevels": {
"view": "查看",
"node_view": "节点·查看",
"node_manage": "节点·管理",
"role_view": "角色·查看",
"role_manage": "角色·管理",
"user_view": "账号·查看",
"user_manage": "账号·管理",
"manage": "管理",
"review": "审核",
"export": "导出",

View File

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

View File

@@ -15,16 +15,12 @@ export const ADMIN_PERMISSION_PACKAGES: Record<string, AdminPermissionPackage[]>
{ 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: [
{

View File

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

View File

@@ -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, unknown>) => 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")}
</Button>
) : null}
{canManageNode && !selected.is_root ? (
<Button
type="button"
size="sm"
variant="outline"
className="text-destructive hover:text-destructive"
disabled={!canDeleteSelectedNode}
title={
canDeleteSelectedNode
? undefined
: deleteBlockReasons.join("") ||
t("deleteNodeBlockedHint", {
defaultValue: "请先删除下级代理、角色与账号后再删除本节点",
})
}
onClick={() => {
if (!canDeleteSelectedNode) {
return;
}
requestConfirm({
title: t("deleteNode"),
description: t("deleteNodeConfirm"),
confirmLabel: t("deleteNode"),
confirmVariant: "destructive",
onConfirm: async () => {
const deletedId = selected.id;
const deletedName = selected.name;
const parentId = selected.parent_id;
await deleteAgentNode(deletedId);
toast.success(t("deleteSuccess", { name: deletedName }));
setSelectedId(parentId);
await loadTree(adminSiteId);
},
});
}}
>
<Trash2 className="mr-1 size-3.5" />
{t("deleteNode")}
</Button>
) : null}
{canManageNode ? (
<Button type="button" size="sm" onClick={openCreateChild}>
<Plus className="mr-1 size-3.5" />
@@ -679,6 +761,12 @@ export function AgentsConsole(): React.ReactElement {
</Button>
) : null}
</div>
{canManageNode && !selected.is_root && deleteBlockReasons.length > 0 ? (
<p className="mb-4 text-xs text-muted-foreground">
{t("deleteNodeBlockedPrefix", { defaultValue: "暂不可删除:" })}
{deleteBlockReasons.join("")}
</p>
) : null}
{canViewRoles ? (
<TabsContent value="roles">
@@ -717,41 +805,52 @@ export function AgentsConsole(): React.ReactElement {
<TableCell>{role.user_count}</TableCell>
<TableCell className="sticky right-0 z-10 bg-card text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">
{canManageRoles && !role.is_read_only_template ? (
<AdminRowActionsMenu
actions={[
{
key: "permissions",
label: t("roles.permissions"),
icon: KeyRound,
onClick: () => {
setPermRoleId(role.id);
setDraftPerms([...role.permission_slugs]);
setPermDialogOpen(true);
<div className="flex flex-col items-center gap-1">
{(role.user_count ?? 0) > 0 ? (
<span className="text-xs text-muted-foreground">
{t("roles.inUse", {
count: role.user_count ?? 0,
defaultValue: "{{count}} 人使用中",
})}
</span>
) : null}
<AdminRowActionsMenu
actions={[
{
key: "permissions",
label: t("roles.permissions"),
icon: KeyRound,
onClick: () => {
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);
}
},
});
},
},
},
]}
/>
]}
/>
</div>
) : role.is_read_only_template ? (
<span className="text-xs text-muted-foreground">
{t("roles.readOnlyTemplate")}
@@ -793,6 +892,7 @@ export function AgentsConsole(): React.ReactElement {
<TableHead>{t("users.username")}</TableHead>
<TableHead>{t("name")}</TableHead>
<TableHead>{t("users.roles")}</TableHead>
<TableHead className="sticky right-0 z-20 bg-muted w-[80px] shadow-[-1px_0_0_rgba(203,213,225,0.7)]" />
</TableRow>
</TableHeader>
<TableBody>
@@ -801,6 +901,41 @@ export function AgentsConsole(): React.ReactElement {
<TableCell>{user.username}</TableCell>
<TableCell>{user.nickname}</TableCell>
<TableCell className="text-xs">{user.roles.join(", ") || "—"}</TableCell>
<TableCell className="sticky right-0 z-10 bg-card text-center shadow-[-1px_0_0_rgba(203,213,225,0.7)]">
{canManageUsers ? (
<AdminRowActionsMenu
actions={[
{
key: "delete",
label: t("common:actions.delete", { defaultValue: "Delete" }),
icon: Trash2,
destructive: true,
onClick: () => {
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}
</TableCell>
</TableRow>
))}
</TableBody>

View File

@@ -23,7 +23,7 @@ type ConfigVersionActionsProps = {
export function ConfigVersionActions({
isDraft,
canManage = true,
canManage = false,
loadingList = false,
loadingDetail = false,
saving = false,