2024-06-25 19:43:47 +08:00
|
|
|
import { useEffect, useMemo, useState } from 'react';
|
2023-09-27 13:55:57 +08:00
|
|
|
|
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';
|
2024-10-22 22:21:10 +08:00
|
|
|
import { config } from '@grafana/runtime';
|
2023-09-27 13:55:57 +08:00
|
|
|
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,
|
2024-03-12 19:18:33 +08:00
|
|
|
Text,
|
2023-11-02 18:22:57 +08:00
|
|
|
Tooltip,
|
2023-09-27 13:55:57 +08:00
|
|
|
} from '@grafana/ui';
|
|
|
|
import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
|
2024-07-31 16:46:51 +08:00
|
|
|
import { fetchRoleOptions, updateUserRoles } from 'app/core/components/RolePicker/api';
|
2024-10-22 22:21:10 +08:00
|
|
|
import { RolePickerBadges } from 'app/core/components/RolePickerDrawer/RolePickerBadges';
|
2023-09-27 13:55:57 +08:00
|
|
|
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
|
|
|
|
import { contextSrv } from 'app/core/core';
|
2025-03-19 21:49:17 +08:00
|
|
|
import { Trans, t } from 'app/core/internationalization';
|
2023-09-27 13:55:57 +08:00
|
|
|
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) => {
|
2023-11-13 17:56:02 +08:00
|
|
|
const isUserSynced = user?.isExternallySynced;
|
|
|
|
return !contextSrv.hasPermissionInMetadata(AccessControlAction.OrgUsersWrite, user) || isUserSynced;
|
2023-09-27 13:55:57 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
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;
|
2024-07-31 16:46:51 +08:00
|
|
|
onUserRolesChange?: () => void;
|
2023-09-27 13:55:57 +08:00
|
|
|
}
|
|
|
|
|
2023-09-29 17:48:36 +08:00
|
|
|
export const OrgUsersTable = ({
|
|
|
|
users,
|
|
|
|
orgId,
|
|
|
|
onRoleChange,
|
2024-07-31 16:46:51 +08:00
|
|
|
onUserRolesChange,
|
2023-09-29 17:48:36 +08:00
|
|
|
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',
|
2024-03-12 19:18:33 +08:00
|
|
|
cell: ({ cell: { value } }: Cell<'lastSeenAtAge'>) => {
|
2024-11-21 20:59:14 +08:00
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{value && (
|
|
|
|
<>
|
|
|
|
{value === '10 years' ? (
|
|
|
|
<Text color={'disabled'}>
|
|
|
|
<Trans i18nKey="admin.org-uers.last-seen-never">Never</Trans>
|
|
|
|
</Text>
|
|
|
|
) : (
|
|
|
|
value
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
);
|
2024-03-12 19:18:33 +08:00
|
|
|
},
|
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);
|
2024-07-31 16:46:51 +08:00
|
|
|
const onUserRolesUpdate = async (newRoles: Role[], userId: number, orgId: number | undefined) => {
|
|
|
|
await updateUserRoles(newRoles, userId, orgId);
|
|
|
|
if (onUserRolesChange) {
|
|
|
|
onUserRolesChange();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-10-22 22:21:10 +08:00
|
|
|
if (config.featureToggles.rolePickerDrawer) {
|
|
|
|
return <RolePickerBadges disabled={basicRoleDisabled} user={original} />;
|
|
|
|
}
|
|
|
|
|
2023-09-27 13:55:57 +08:00
|
|
|
return contextSrv.licensedAccessControlEnabled() ? (
|
|
|
|
<UserRolePicker
|
|
|
|
userId={original.userId}
|
2024-07-31 16:46:51 +08:00
|
|
|
roles={original.roles}
|
|
|
|
apply={true}
|
|
|
|
onApplyRoles={onUserRolesUpdate}
|
2023-11-01 18:57:02 +08:00
|
|
|
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
|
2025-03-19 21:49:17 +08:00
|
|
|
aria-label={t('admin.org-users-table.columns.aria-label-role', 'Role')}
|
2023-09-27 13:55:57 +08:00
|
|
|
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>
|
2024-11-21 20:59:14 +08:00
|
|
|
<Trans i18nKey="admin.org-users.not-editable">
|
|
|
|
This user's role is not editable because it is synchronized from your auth provider. Refer
|
|
|
|
to the
|
|
|
|
<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>
|
|
|
|
for details.
|
|
|
|
</Trans>
|
2023-10-18 22:14:07 +08:00
|
|
|
</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}`}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
2024-07-31 16:46:51 +08:00
|
|
|
[rolesLoading, orgId, roleOptions, onUserRolesChange, onRoleChange]
|
2023-09-27 13:55:57 +08:00
|
|
|
);
|
|
|
|
|
|
|
|
return (
|
2023-11-17 20:08:41 +08:00
|
|
|
<Stack direction={'column'} 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"
|
2025-03-19 21:49:17 +08:00
|
|
|
title={t('admin.org-users-table.title-delete', 'Delete')}
|
2023-09-27 13:55:57 +08:00
|
|
|
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
|
|
|
);
|
|
|
|
};
|