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:
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,12 @@
|
||||
},
|
||||
"permissionLevels": {
|
||||
"view": "हेर्नुहोस्",
|
||||
"node_view": "नोड · हेर्नुहोस्",
|
||||
"node_manage": "नोड · व्यवस्थापन",
|
||||
"role_view": "भूमिका · हेर्नुहोस्",
|
||||
"role_manage": "भूमिका · व्यवस्थापन",
|
||||
"user_view": "खाता · हेर्नुहोस्",
|
||||
"user_manage": "खाता · व्यवस्थापन",
|
||||
"manage": "व्यवस्थापन",
|
||||
"review": "समीक्षा",
|
||||
"export": "निर्यात",
|
||||
|
||||
@@ -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}} मेटियो"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,12 @@
|
||||
},
|
||||
"permissionLevels": {
|
||||
"view": "查看",
|
||||
"node_view": "节点·查看",
|
||||
"node_manage": "节点·管理",
|
||||
"role_view": "角色·查看",
|
||||
"role_manage": "角色·管理",
|
||||
"user_view": "账号·查看",
|
||||
"user_manage": "账号·管理",
|
||||
"manage": "管理",
|
||||
"review": "审核",
|
||||
"export": "导出",
|
||||
|
||||
@@ -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}}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -23,7 +23,7 @@ type ConfigVersionActionsProps = {
|
||||
|
||||
export function ConfigVersionActions({
|
||||
isDraft,
|
||||
canManage = true,
|
||||
canManage = false,
|
||||
loadingList = false,
|
||||
loadingDetail = false,
|
||||
saving = false,
|
||||
|
||||
Reference in New Issue
Block a user