@@ -23,7 +23,7 @@ import {
2323 SettingOutlined ,
2424} from '@ant-design/icons' ;
2525import { useToggle } from 'ahooks' ;
26- import { App , Button , Dropdown , Space , theme } from 'antd' ;
26+ import { App , Button , Dropdown , Space , Tag , Tooltip , theme } from 'antd' ;
2727import {
2828 filterOutEmpty ,
2929 filterOutNullAndUndefined ,
@@ -43,7 +43,13 @@ import {
4343 useFetchKey ,
4444} from 'backend.ai-ui' ;
4545import * 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' ;
4753import { parseAsString , parseAsStringLiteral , useQueryStates } from 'nuqs' ;
4854import React , { useState , useTransition , useDeferredValue } from 'react' ;
4955import { 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