2023-09-27 13:55:57 +08:00
import React , { useEffect , useMemo , useState } from 'react' ;
2023-10-17 19:06:28 +08:00
import { OrgRole } from '@grafana/data' ;
2023-09-27 13:55:57 +08:00
import { selectors as e2eSelectors } from '@grafana/e2e-selectors' ;
import {
2023-11-02 18:22:57 +08:00
Avatar ,
Box ,
2023-09-27 13:55:57 +08:00
Button ,
CellProps ,
Column ,
2023-11-02 18:22:57 +08:00
ConfirmModal ,
2023-09-29 17:48:36 +08:00
FetchDataFunc ,
2023-11-02 18:22:57 +08:00
Icon ,
InteractiveTable ,
2023-09-27 13:55:57 +08:00
Pagination ,
2023-11-02 18:22:57 +08:00
Stack ,
Tag ,
Tooltip ,
2023-09-27 13:55:57 +08:00
} from '@grafana/ui' ;
import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker' ;
import { fetchRoleOptions } from 'app/core/components/RolePicker/api' ;
import { TagBadge } from 'app/core/components/TagFilter/TagBadge' ;
import config from 'app/core/config' ;
import { contextSrv } from 'app/core/core' ;
import { AccessControlAction , OrgUser , Role } from 'app/types' ;
import { OrgRolePicker } from '../OrgRolePicker' ;
type Cell < T extends keyof OrgUser = keyof OrgUser > = CellProps < OrgUser , OrgUser [ T ] > ;
const disabledRoleMessage = ` This user's role is not editable because it is synchronized from your auth provider.
2023-10-18 22:14:07 +08:00
Refer to the Grafana authentication docs for details . ` ;
2023-09-27 13:55:57 +08:00
const getBasicRoleDisabled = ( user : OrgUser ) = > {
let basicRoleDisabled = ! contextSrv . hasPermissionInMetadata ( AccessControlAction . OrgUsersWrite , user ) ;
let authLabel = Array . isArray ( user . authLabels ) && user . authLabels . length > 0 ? user . authLabels [ 0 ] : '' ;
// A GCom specific feature toggle for role locking has been introduced, as the previous implementation had a bug with locking down external users synced through GCom (https://github.com/grafana/grafana/pull/72044)
// Remove this conditional once FlagGcomOnlyExternalOrgRoleSync feature toggle has been removed
if ( authLabel !== 'grafana.com' || config . featureToggles . gcomOnlyExternalOrgRoleSync ) {
const isUserSynced = user ? . isExternallySynced ;
basicRoleDisabled = isUserSynced || basicRoleDisabled ;
}
return basicRoleDisabled ;
} ;
const selectors = e2eSelectors . pages . UserListPage . UsersListPage ;
export interface Props {
users : OrgUser [ ] ;
orgId? : number ;
onRoleChange : ( role : OrgRole , user : OrgUser ) = > void ;
onRemoveUser : ( user : OrgUser ) = > void ;
2023-09-29 17:48:36 +08:00
fetchData? : FetchDataFunc < OrgUser > ;
2023-09-27 13:55:57 +08:00
changePage : ( page : number ) = > void ;
page : number ;
totalPages : number ;
2023-11-01 18:57:02 +08:00
rolesLoading? : boolean ;
2023-09-27 13:55:57 +08:00
}
2023-09-29 17:48:36 +08:00
export const OrgUsersTable = ( {
users ,
orgId ,
onRoleChange ,
onRemoveUser ,
fetchData ,
changePage ,
page ,
totalPages ,
2023-11-01 18:57:02 +08:00
rolesLoading ,
2023-09-29 17:48:36 +08:00
} : Props ) = > {
2023-09-27 13:55:57 +08:00
const [ userToRemove , setUserToRemove ] = useState < OrgUser | null > ( null ) ;
const [ roleOptions , setRoleOptions ] = useState < Role [ ] > ( [ ] ) ;
useEffect ( ( ) = > {
async function fetchOptions() {
try {
if ( contextSrv . hasPermission ( AccessControlAction . ActionRolesList ) ) {
let options = await fetchRoleOptions ( orgId ) ;
setRoleOptions ( options ) ;
}
} catch ( e ) {
console . error ( 'Error loading options' ) ;
}
}
if ( contextSrv . licensedAccessControlEnabled ( ) ) {
fetchOptions ( ) ;
}
} , [ orgId ] ) ;
const columns : Array < Column < OrgUser > > = useMemo (
( ) = > [
{
id : 'avatarUrl' ,
header : '' ,
2023-10-16 18:59:54 +08:00
cell : ( { cell : { value } } : Cell < 'avatarUrl' > ) = > value && < Avatar src = { value } alt = "User avatar" / > ,
2023-09-27 13:55:57 +08:00
} ,
{
id : 'login' ,
header : 'Login' ,
cell : ( { cell : { value } } : Cell < 'login' > ) = > < div > { value } < / div > ,
2023-09-29 17:48:36 +08:00
sortType : 'string' ,
2023-09-27 13:55:57 +08:00
} ,
{
id : 'email' ,
header : 'Email' ,
cell : ( { cell : { value } } : Cell < 'email' > ) = > value ,
2023-09-29 17:48:36 +08:00
sortType : 'string' ,
2023-09-27 13:55:57 +08:00
} ,
{
id : 'name' ,
header : 'Name' ,
cell : ( { cell : { value } } : Cell < 'name' > ) = > value ,
2023-09-29 17:48:36 +08:00
sortType : 'string' ,
2023-09-27 13:55:57 +08:00
} ,
{
id : 'lastSeenAtAge' ,
header : 'Last active' ,
cell : ( { cell : { value } } : Cell < 'lastSeenAtAge' > ) = > value ,
2023-09-29 17:48:36 +08:00
sortType : ( a , b ) = > new Date ( a . original . lastSeenAt ) . getTime ( ) - new Date ( b . original . lastSeenAt ) . getTime ( ) ,
2023-09-27 13:55:57 +08:00
} ,
{
id : 'role' ,
header : 'Role' ,
cell : ( { cell : { value } , row : { original } } : Cell < 'role' > ) = > {
const basicRoleDisabled = getBasicRoleDisabled ( original ) ;
return contextSrv . licensedAccessControlEnabled ( ) ? (
< UserRolePicker
userId = { original . userId }
2023-11-01 18:57:02 +08:00
roles = { original . roles || [ ] }
isLoading = { rolesLoading }
2023-09-27 13:55:57 +08:00
orgId = { orgId }
roleOptions = { roleOptions }
basicRole = { value }
onBasicRoleChange = { ( newRole ) = > onRoleChange ( newRole , original ) }
basicRoleDisabled = { basicRoleDisabled }
basicRoleDisabledMessage = { disabledRoleMessage }
2023-10-25 19:03:12 +08:00
width = { 40 }
2023-09-27 13:55:57 +08:00
/ >
) : (
< OrgRolePicker
aria - label = "Role"
value = { value }
disabled = { basicRoleDisabled }
onChange = { ( newRole ) = > onRoleChange ( newRole , original ) }
/ >
) ;
} ,
} ,
{
id : 'info' ,
header : '' ,
2023-10-17 19:06:28 +08:00
cell : ( { row : { original } } : Cell ) = > {
const basicRoleDisabled = getBasicRoleDisabled ( original ) ;
return (
basicRoleDisabled && (
< Box display = { 'flex' } alignItems = { 'center' } marginLeft = { 1 } >
2023-10-18 22:14:07 +08:00
< Tooltip
interactive = { true }
content = {
< div >
This user & apos ; s role is not editable because it is synchronized from your auth provider . Refer to
the & nbsp ;
< a
href = {
'https://grafana.com/docs/grafana/latest/administration/user-management/manage-org-users/#change-a-users-organization-permissions'
}
rel = "noreferrer"
target = "_blank"
>
Grafana authentication docs
< / a >
& nbsp ; for details .
< / div >
}
>
2023-10-17 19:06:28 +08:00
< Icon name = "question-circle" / >
< / Tooltip >
< / Box >
)
) ;
} ,
2023-09-27 13:55:57 +08:00
} ,
{
id : 'authLabels' ,
header : 'Origin' ,
cell : ( { cell : { value } } : Cell < 'authLabels' > ) = > (
< > { Array . isArray ( value ) && value . length > 0 && < TagBadge label = { value [ 0 ] } removeIcon = { false } count = { 0 } / > } < / >
) ,
} ,
{
id : 'isDisabled' ,
header : '' ,
cell : ( { cell : { value } } : Cell < 'isDisabled' > ) = > < > { value && < Tag colorIndex = { 9 } name = { 'Disabled' } / > } < / > ,
} ,
{
id : 'delete' ,
header : '' ,
cell : ( { row : { original } } : Cell ) = > {
return (
contextSrv . hasPermissionInMetadata ( AccessControlAction . OrgUsersRemove , original ) && (
< Button
size = "sm"
variant = "destructive"
onClick = { ( ) = > {
setUserToRemove ( original ) ;
} }
icon = "times"
aria - label = { ` Delete user ${ original . name } ` }
/ >
)
) ;
} ,
} ,
] ,
2023-11-01 18:57:02 +08:00
[ rolesLoading , orgId , roleOptions , onRoleChange ]
2023-09-27 13:55:57 +08:00
) ;
return (
2023-10-17 19:06:28 +08:00
< Stack gap = { 2 } data - testid = { selectors . container } >
2023-11-06 22:33:58 +08:00
< InteractiveTable columns = { columns } data = { users } getRowId = { ( user ) = > String ( user . userId ) } fetchData = { fetchData } / >
< Stack justifyContent = "flex-end" >
< Pagination onNavigate = { changePage } currentPage = { page } numberOfPages = { totalPages } hideWhenSinglePage = { true } / >
< / Stack >
2023-09-27 13:55:57 +08:00
{ Boolean ( userToRemove ) && (
< ConfirmModal
body = { ` Are you sure you want to delete user ${ userToRemove ? . login } ? ` }
confirmText = "Delete"
title = "Delete"
onDismiss = { ( ) = > {
setUserToRemove ( null ) ;
} }
isOpen = { true }
onConfirm = { ( ) = > {
if ( ! userToRemove ) {
return ;
}
onRemoveUser ( userToRemove ) ;
setUserToRemove ( null ) ;
} }
/ >
) }
2023-10-17 19:06:28 +08:00
< / Stack >
2023-09-27 13:55:57 +08:00
) ;
} ;