From 8ab24fd3bfb6dae8d08c94ca65cd49fa0d01eb9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Thu, 13 Mar 2025 09:02:06 +0000 Subject: [PATCH] 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) --- .../UsersActionsCell/UsersActionsCell.tsx | 204 +++++++++++------- .../admin/users/UsersList/UsersList.tsx | 32 ++- 2 files changed, 137 insertions(+), 99 deletions(-) diff --git a/frontend/src/component/admin/users/UsersList/UsersActionsCell/UsersActionsCell.tsx b/frontend/src/component/admin/users/UsersList/UsersActionsCell/UsersActionsCell.tsx index 715c365d05..a61bed269b 100644 --- a/frontend/src/component/admin/users/UsersList/UsersActionsCell/UsersActionsCell.tsx +++ b/frontend/src/component/admin/users/UsersList/UsersActionsCell/UsersActionsCell.tsx @@ -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 = ({ +export const UsersActionsCell = ({ onEdit, onViewAccess, onChangePassword, onResetPassword, onDelete, isScimUser, -}) => { + userId, +}: IUsersActionsCellProps) => { + const [anchorEl, setAnchorEl] = useState(null); + + const open = Boolean(anchorEl); + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + + const id = `user-${userId}-actions`; + const menuId = `${id}-menu`; + + return ( + + + + + + + + + + Edit user + + {onViewAccess && ( + + Access overview + + )} + { + onChangePassword(); + handleClose(); + }} + isScimUser={isScimUser} + > + Change password + + { + onResetPassword(); + handleClose(); + }} + isScimUser={isScimUser} + > + Reset password + + { + onDelete(); + handleClose(); + }} + > + Remove user + + + + + ); +}; + +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 ( - - - - - - - - - } - /> - - - - - - - - - - - + +
+ + + {children} + + +
+
); }; diff --git a/frontend/src/component/admin/users/UsersList/UsersList.tsx b/frontend/src/component/admin/users/UsersList/UsersList.tsx index 687e60f558..a92d081ee5 100644 --- a/frontend/src/component/admin/users/UsersList/UsersList.tsx +++ b/frontend/src/component/admin/users/UsersList/UsersList.tsx @@ -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) => { - e.preventDefault(); - setDelDialog(true); - setDelUser(user); - }; - const openPwDialog = - (user: IUser) => (e: React.SyntheticEvent) => { - 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) => { - 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