1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

chore: users actions menu (#9525)

https://linear.app/unleash/issue/2-3342/new-entrance-point-create-dot-dot-dot-menu-instead-of-icons

Adds a new users actions menu.

Should this change be behind a flag? I'm leaning towards no, but if you
think otherwise let me know.

### Previous

![image](https://github.com/user-attachments/assets/6becffc5-c5e2-4e21-88bf-8644d1337c68)

### After

![image](https://github.com/user-attachments/assets/968859f0-f562-4252-bc93-fe362c5bc378)

### If user is SCIM-managed

![image](https://github.com/user-attachments/assets/275581b5-4cd2-4a8b-9f35-42e9f493102f)
This commit is contained in:
Nuno Góis 2025-03-13 09:02:06 +00:00 committed by GitHub
parent 1b7f91cd4b
commit 8ab24fd3bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 137 additions and 99 deletions

View File

@ -1,101 +1,145 @@
import Delete from '@mui/icons-material/Delete';
import Edit from '@mui/icons-material/Edit';
import Key from '@mui/icons-material/Key';
import Lock from '@mui/icons-material/Lock';
import LockReset from '@mui/icons-material/LockReset';
import { Box, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import type { VFC } from 'react';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import {
IconButton,
ListItemText,
MenuItem,
MenuList,
Popover,
styled,
Tooltip,
Typography,
} from '@mui/material';
import { useState } from 'react';
const StyledBox = styled(Box)(() => ({
const StyledActions = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
margin: theme.spacing(-1),
marginLeft: theme.spacing(-0.5),
}));
const StyledPopover = styled(Popover)(({ theme }) => ({
borderRadius: theme.shape.borderRadiusLarge,
padding: theme.spacing(1, 1.5),
}));
interface IUsersActionsCellProps {
onEdit: (event: React.SyntheticEvent) => void;
onViewAccess?: (event: React.SyntheticEvent) => void;
onChangePassword: (event: React.SyntheticEvent) => void;
onResetPassword: (event: React.SyntheticEvent) => void;
onDelete: (event: React.SyntheticEvent) => void;
onEdit: () => void;
onViewAccess?: () => void;
onChangePassword: () => void;
onResetPassword: () => void;
onDelete: () => void;
isScimUser?: boolean;
userId: number;
}
export const UsersActionsCell: VFC<IUsersActionsCellProps> = ({
export const UsersActionsCell = ({
onEdit,
onViewAccess,
onChangePassword,
onResetPassword,
onDelete,
isScimUser,
}) => {
userId,
}: IUsersActionsCellProps) => {
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const id = `user-${userId}-actions`;
const menuId = `${id}-menu`;
return (
<StyledActions>
<Tooltip title='User actions' arrow describeChild>
<IconButton
id={id}
aria-controls={open ? 'actions-menu' : undefined}
aria-haspopup='true'
aria-expanded={open ? 'true' : undefined}
onClick={handleClick}
type='button'
size='small'
>
<MoreVertIcon />
</IconButton>
</Tooltip>
<StyledPopover
id={menuId}
anchorEl={anchorEl}
open={open}
onClose={handleClose}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
disableScrollLock={true}
>
<MenuList aria-labelledby={id}>
<UserAction onClick={onEdit} isScimUser={isScimUser}>
Edit user
</UserAction>
{onViewAccess && (
<UserAction onClick={onViewAccess}>
Access overview
</UserAction>
)}
<UserAction
onClick={() => {
onChangePassword();
handleClose();
}}
isScimUser={isScimUser}
>
Change password
</UserAction>
<UserAction
onClick={() => {
onResetPassword();
handleClose();
}}
isScimUser={isScimUser}
>
Reset password
</UserAction>
<UserAction
onClick={() => {
onDelete();
handleClose();
}}
>
Remove user
</UserAction>
</MenuList>
</StyledPopover>
</StyledActions>
);
};
interface IUserActionProps {
onClick: () => void;
isScimUser?: boolean;
children: React.ReactNode;
}
const UserAction = ({ onClick, isScimUser, children }: IUserActionProps) => {
const scimTooltip =
'This user is managed by your SCIM provider and cannot be changed manually';
return (
<StyledBox>
<PermissionIconButton
data-loading
onClick={onEdit}
permission={ADMIN}
tooltipProps={{
title: isScimUser ? scimTooltip : 'Edit user',
}}
disabled={isScimUser}
>
<Edit />
</PermissionIconButton>
<ConditionallyRender
condition={Boolean(onViewAccess)}
show={
<PermissionIconButton
data-loading
onClick={onViewAccess!}
permission={ADMIN}
tooltipProps={{
title: 'Access matrix',
}}
>
<Key />
</PermissionIconButton>
}
/>
<PermissionIconButton
data-loading
onClick={onChangePassword}
permission={ADMIN}
tooltipProps={{
title: isScimUser ? scimTooltip : 'Change password',
}}
disabled={isScimUser}
>
<Lock />
</PermissionIconButton>
<PermissionIconButton
data-loading
onClick={onResetPassword}
permission={ADMIN}
tooltipProps={{
title: isScimUser ? scimTooltip : 'Reset password',
}}
disabled={isScimUser}
>
<LockReset />
</PermissionIconButton>
<PermissionIconButton
data-loading
onClick={onDelete}
permission={ADMIN}
tooltipProps={{
title: 'Remove user',
}}
>
<Delete />
</PermissionIconButton>
</StyledBox>
<Tooltip title={isScimUser ? scimTooltip : ''} arrow placement='left'>
<div>
<MenuItem onClick={onClick} disabled={isScimUser}>
<ListItemText>
<Typography variant='body2'>{children}</Typography>
</ListItemText>
</MenuItem>
</div>
</Tooltip>
);
};

View File

@ -1,4 +1,3 @@
import type React from 'react';
import { useMemo, useState } from 'react';
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
import ChangePassword from './ChangePassword/ChangePassword';
@ -82,23 +81,17 @@ const UsersList = () => {
setDelUser(undefined);
};
const openDelDialog =
(user: IUser) => (e: React.SyntheticEvent<Element, Event>) => {
e.preventDefault();
setDelDialog(true);
setDelUser(user);
};
const openPwDialog =
(user: IUser) => (e: React.SyntheticEvent<Element, Event>) => {
e.preventDefault();
setPwDialog({ open: true, user });
};
const openDelDialog = (user: IUser) => () => {
setDelDialog(true);
setDelUser(user);
};
const openPwDialog = (user: IUser) => () => {
setPwDialog({ open: true, user });
};
const openResetPwDialog =
(user: IUser) => (e: React.SyntheticEvent<Element, Event>) => {
e.preventDefault();
setResetPwDialog({ open: true, user });
};
const openResetPwDialog = (user: IUser) => () => {
setResetPwDialog({ open: true, user });
};
const closePwDialog = () => {
setPwDialog({ open: false });
@ -215,7 +208,7 @@ const UsersList = () => {
sortType: 'boolean',
},
{
Header: 'Actions',
Header: '',
id: 'Actions',
align: 'center',
Cell: ({
@ -238,9 +231,10 @@ const UsersList = () => {
onResetPassword={openResetPwDialog(user)}
onDelete={openDelDialog(user)}
isScimUser={scimEnabled && Boolean(user.scimId)}
userId={user.id}
/>
),
width: userAccessUIEnabled ? 240 : 200,
width: 80,
disableSortBy: true,
},
// Always hidden -- for search