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

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