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