Skip to content

Commit bd78074

Browse files
committed
fix(FR-2812): display project-scoped roles in user list Role column
1 parent 82d550b commit bd78074

1 file changed

Lines changed: 139 additions & 40 deletions

File tree

react/src/components/UserManagement.tsx

Lines changed: 139 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
SettingOutlined,
2424
} from '@ant-design/icons';
2525
import { useToggle } from 'ahooks';
26-
import { App, Button, Dropdown, Space, theme } from 'antd';
26+
import { App, Button, Dropdown, Space, Tag, Tooltip, theme } from 'antd';
2727
import {
2828
filterOutEmpty,
2929
filterOutNullAndUndefined,
@@ -43,7 +43,13 @@ import {
4343
useFetchKey,
4444
} from 'backend.ai-ui';
4545
import * as _ from 'lodash-es';
46-
import { BanIcon, EditIcon, PlusIcon, UndoIcon } from 'lucide-react';
46+
import {
47+
BanIcon,
48+
EditIcon,
49+
PlusIcon,
50+
ShieldUser,
51+
UndoIcon,
52+
} from 'lucide-react';
4753
import { parseAsString, parseAsStringLiteral, useQueryStates } from 'nuqs';
4854
import React, { useState, useTransition, useDeferredValue } from 'react';
4955
import { useTranslation } from 'react-i18next';
@@ -122,42 +128,101 @@ const UserManagement: React.FC<UserManagementProps> = () => {
122128

123129
const { supportedFields, exportCSV } = useCSVExport('users');
124130

125-
const { user_nodes } = useLazyLoadQuery<UserManagementQuery>(
126-
graphql`
127-
query UserManagementQuery(
128-
$first: Int
129-
$offset: Int
130-
$filter: String
131-
$order: String
132-
) {
133-
user_nodes(
134-
first: $first
135-
offset: $offset
136-
filter: $filter
137-
order: $order
131+
const { user_nodes, projectAdminAssignments } =
132+
useLazyLoadQuery<UserManagementQuery>(
133+
graphql`
134+
query UserManagementQuery(
135+
$first: Int
136+
$offset: Int
137+
$filter: String
138+
$order: String
138139
) {
139-
count
140-
edges {
141-
node {
142-
id @required(action: THROW)
143-
email @required(action: THROW)
144-
...BAIUserNodesFragment
145-
...PurgeUsersModalFragment
146-
...UpdateUsersModalFragment
140+
user_nodes(
141+
first: $first
142+
offset: $offset
143+
filter: $filter
144+
order: $order
145+
) {
146+
count
147+
edges {
148+
node {
149+
id @required(action: THROW)
150+
email @required(action: THROW)
151+
...BAIUserNodesFragment
152+
...PurgeUsersModalFragment
153+
...UpdateUsersModalFragment
154+
}
155+
}
156+
}
157+
projectAdminAssignments: adminRoleAssignments(
158+
first: 1000
159+
filter: {
160+
permission: { entityType: { equals: PROJECT_ADMIN_PAGE } }
161+
}
162+
) @catch(to: RESULT) {
163+
edges {
164+
node {
165+
user {
166+
basicInfo {
167+
email
168+
}
169+
}
170+
role {
171+
name
172+
scopes(first: 10) {
173+
edges {
174+
node {
175+
scopeType
176+
scope {
177+
... on ProjectV2 {
178+
basicInfo {
179+
name
180+
}
181+
}
182+
}
183+
}
184+
}
185+
}
186+
}
187+
}
147188
}
148189
}
149190
}
191+
`,
192+
deferredQueryVariables,
193+
{
194+
fetchKey: deferredFetchKey,
195+
fetchPolicy:
196+
deferredFetchKey === INITIAL_FETCH_KEY
197+
? 'store-and-network'
198+
: 'network-only',
199+
},
200+
);
201+
202+
// Build a map from user email to list of project names where the user
203+
// holds a project-admin role. Used by the role column to display
204+
// project-scoped roles alongside the global role.
205+
const projectAdminByEmail = (() => {
206+
const map = new Map<string, string[]>();
207+
if (projectAdminAssignments?.ok !== true) return map;
208+
for (const edge of projectAdminAssignments.value?.edges ?? []) {
209+
const email = edge?.node?.user?.basicInfo?.email;
210+
if (!email) continue;
211+
const scopes = edge?.node?.role?.scopes?.edges ?? [];
212+
for (const scopeEdge of scopes) {
213+
const scope = scopeEdge?.node;
214+
if (scope?.scopeType === 'PROJECT') {
215+
const projectName = scope?.scope?.basicInfo?.name ?? scope?.scopeType;
216+
const existing = map.get(email) ?? [];
217+
if (!existing.includes(projectName)) {
218+
existing.push(projectName);
219+
}
220+
map.set(email, existing);
221+
}
150222
}
151-
`,
152-
deferredQueryVariables,
153-
{
154-
fetchKey: deferredFetchKey,
155-
fetchPolicy:
156-
deferredFetchKey === INITIAL_FETCH_KEY
157-
? 'store-and-network'
158-
: 'network-only',
159-
},
160-
);
223+
}
224+
return map;
225+
})();
161226

162227
const [commitModifyUser] = useMutation<UserManagementModifyMutation>(graphql`
163228
mutation UserManagementModifyMutation(
@@ -438,13 +503,47 @@ const UserManagement: React.FC<UserManagementProps> = () => {
438503
</BAIFlex>
439504
<BAIUserNodes
440505
usersFrgmt={filterOutNullAndUndefined(_.map(user_nodes?.edges, 'node'))}
441-
customizeColumns={(baseColumns) => [
442-
{
443-
...baseColumns[0],
444-
render: renderEmailWithActions,
445-
},
446-
...baseColumns.slice(1),
447-
]}
506+
customizeColumns={(baseColumns) => {
507+
return baseColumns.map((col) => {
508+
if (col.key === 'email') {
509+
return { ...col, render: renderEmailWithActions };
510+
}
511+
if (col.key === 'role') {
512+
return {
513+
...col,
514+
render: (__: unknown, record: UserNodeInList) => {
515+
const projectNames = projectAdminByEmail.get(record.email);
516+
return (
517+
<BAIFlex gap="xs" align="center" wrap="wrap">
518+
<span>{record.role}</span>
519+
{projectNames && projectNames.length > 0 && (
520+
<Tooltip title={projectNames.join(', ')}>
521+
<Tag
522+
icon={
523+
<ShieldUser
524+
style={{
525+
width: 12,
526+
height: 12,
527+
verticalAlign: '-0.1em',
528+
marginInlineEnd: 4,
529+
}}
530+
/>
531+
}
532+
color="geekblue"
533+
style={{ marginInlineEnd: 0 }}
534+
>
535+
{t('projectSelect.ProjectAdminBadge')}
536+
</Tag>
537+
</Tooltip>
538+
)}
539+
</BAIFlex>
540+
);
541+
},
542+
};
543+
}
544+
return col;
545+
});
546+
}}
448547
scroll={{ x: 'max-content' }}
449548
pagination={{
450549
pageSize: tablePaginationOption.pageSize,

0 commit comments

Comments
 (0)