diff --git a/artifacts/api-server/src/routes/roles.ts b/artifacts/api-server/src/routes/roles.ts index 4992cdc1..da1a354f 100644 --- a/artifacts/api-server/src/routes/roles.ts +++ b/artifacts/api-server/src/routes/roles.ts @@ -54,7 +54,41 @@ function serializeRole(r: typeof rolesTable.$inferSelect) { router.get("/roles", requireAdmin, async (_req, res): Promise => { const rows = await db.select().from(rolesTable).orderBy(rolesTable.name); - res.json(rows.map(serializeRole)); + if (rows.length === 0) { + res.json([]); + return; + } + // Compute dependency counts so the admin list can show the same inline + // "5 users · 2 groups" hint as the Apps/Services/Users/Groups panels + // (mirrors the GET /apps and GET /groups behavior). The single-role + // /roles/:id/usage endpoint remains the source of truth for the delete + // dialog and edit dialog. + const roleIds = rows.map((r) => r.id); + const userCountRows = await db + .select({ + roleId: userRolesTable.roleId, + count: sql`count(*)::int`, + }) + .from(userRolesTable) + .where(inArray(userRolesTable.roleId, roleIds)) + .groupBy(userRolesTable.roleId); + const groupCountRows = await db + .select({ + roleId: groupRolesTable.roleId, + count: sql`count(*)::int`, + }) + .from(groupRolesTable) + .where(inArray(groupRolesTable.roleId, roleIds)) + .groupBy(groupRolesTable.roleId); + const userMap = new Map(userCountRows.map((r) => [r.roleId, r.count])); + const groupMap = new Map(groupCountRows.map((r) => [r.roleId, r.count])); + res.json( + rows.map((r) => ({ + ...serializeRole(r), + userCount: userMap.get(r.id) ?? 0, + groupCount: groupMap.get(r.id) ?? 0, + })), + ); }); router.get("/permissions", requireAdmin, async (_req, res): Promise => { diff --git a/artifacts/tx-os/src/pages/admin.tsx b/artifacts/tx-os/src/pages/admin.tsx index 256d5be5..3753f3b2 100644 --- a/artifacts/tx-os/src/pages/admin.tsx +++ b/artifacts/tx-os/src/pages/admin.tsx @@ -5246,6 +5246,27 @@ function RolesPanel() { {r.descriptionAr}

)} + {(() => { + const userCount = r.userCount ?? 0; + const groupCount = r.groupCount ?? 0; + const parts: string[] = []; + if (userCount > 0) parts.push(t("admin.roles.usersCount", { count: userCount })); + if (groupCount > 0) parts.push(t("admin.roles.groupsCount", { count: groupCount })); + if (parts.length === 0) return null; + return ( +
+ {parts.map((p, i) => ( + + {i > 0 && } + {p} + + ))} +
+ ); + })()}