grafana/public/app/features/admin/Users/OrgUsersTable.tsx

260 lines
7.7 KiB
TypeScript
Raw Normal View History

import { css } from '@emotion/css';
import React, { useEffect, useMemo, useState } from 'react';
import { GrafanaTheme2, OrgRole } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import {
Button,
ConfirmModal,
Icon,
Tooltip,
CellProps,
useStyles2,
Tag,
InteractiveTable,
Column,
FetchDataFunc,
Pagination,
HorizontalGroup,
VerticalGroup,
} 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';
import { Avatar } from './Avatar';
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.
Refer to the Grafana authentication docs for details.`;
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;
fetchData?: FetchDataFunc<OrgUser>;
changePage: (page: number) => void;
page: number;
totalPages: number;
}
export const OrgUsersTable = ({
users,
orgId,
onRoleChange,
onRemoveUser,
fetchData,
changePage,
page,
totalPages,
}: Props) => {
const [userToRemove, setUserToRemove] = useState<OrgUser | null>(null);
const [roleOptions, setRoleOptions] = useState<Role[]>([]);
const styles = useStyles2(getStyles);
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: '',
cell: ({ cell: { value } }: Cell<'avatarUrl'>) => <Avatar src={value} alt="User avatar" />,
},
{
id: 'login',
header: 'Login',
cell: ({ cell: { value } }: Cell<'login'>) => <div>{value}</div>,
sortType: 'string',
},
{
id: 'email',
header: 'Email',
cell: ({ cell: { value } }: Cell<'email'>) => value,
sortType: 'string',
},
{
id: 'name',
header: 'Name',
cell: ({ cell: { value } }: Cell<'name'>) => value,
sortType: 'string',
},
{
id: 'lastSeenAtAge',
header: 'Last active',
cell: ({ cell: { value } }: Cell<'lastSeenAtAge'>) => value,
sortType: (a, b) => new Date(a.original.lastSeenAt).getTime() - new Date(b.original.lastSeenAt).getTime(),
},
{
id: 'role',
header: 'Role',
cell: ({ cell: { value }, row: { original } }: Cell<'role'>) => {
const basicRoleDisabled = getBasicRoleDisabled(original);
return contextSrv.licensedAccessControlEnabled() ? (
<UserRolePicker
userId={original.userId}
orgId={orgId}
roleOptions={roleOptions}
basicRole={value}
onBasicRoleChange={(newRole) => onRoleChange(newRole, original)}
basicRoleDisabled={basicRoleDisabled}
basicRoleDisabledMessage={disabledRoleMessage}
/>
) : (
<OrgRolePicker
aria-label="Role"
value={value}
disabled={basicRoleDisabled}
onChange={(newRole) => onRoleChange(newRole, original)}
/>
);
},
},
{
id: 'info',
header: '',
cell: InfoCell,
},
{
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}`}
/>
)
);
},
},
],
[orgId, roleOptions, onRoleChange]
);
return (
<VerticalGroup spacing="md" data-testid={selectors.container}>
<div className={styles.wrapper}>
<InteractiveTable
columns={columns}
data={users}
getRowId={(user) => String(user.userId)}
fetchData={fetchData}
/>
<HorizontalGroup justify="flex-end">
<Pagination onNavigate={changePage} currentPage={page} numberOfPages={totalPages} hideWhenSinglePage={true} />
</HorizontalGroup>
</div>
{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);
}}
/>
)}
</VerticalGroup>
);
};
const InfoCell = ({ row: { original } }: Cell) => {
const styles = useStyles2(getStyles);
const basicRoleDisabled = getBasicRoleDisabled(original);
return (
basicRoleDisabled && (
<div className={styles.row}>
<Tooltip content={disabledRoleMessage}>
<Icon name="question-circle" className={styles.icon} />
</Tooltip>
</div>
)
);
};
const getStyles = (theme: GrafanaTheme2) => ({
row: css({
display: 'flex',
alignItems: 'center',
}),
icon: css({
marginLeft: theme.spacing(1),
}),
// Enable RolePicker overflow
wrapper: css({
display: 'flex',
flexDirection: 'column',
overflowX: 'auto',
overflowY: 'hidden',
minHeight: '100vh',
width: '100%',
'& > div': {
overflowX: 'unset',
marginBottom: theme.spacing(2),
},
}),
});