1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

Feat/groups refinement (#1190)

* Button for 0 groups

* Highlight name on exist

* Add hover to groups

* Change avatar size to 28px

* Add tooltip to project and fix error

* Fix tooltip

* Link to project, change to flex etc

* Reuse badges better

* Limit to max 50% width

* Refinements

* UI refinements

* Update

* Remove import

* Refinement fixes

* Refinement

* Refinement

* Refinement

* Star to star rounded
This commit is contained in:
sjaanus 2022-08-03 21:57:48 +03:00 committed by GitHub
parent 4486901a4b
commit d10c151dea
14 changed files with 157 additions and 85 deletions

View File

@ -255,7 +255,7 @@ export const Group: VFC = () => {
onClick={() => setRemoveOpen(true)}
permission={ADMIN}
tooltipProps={{
title: 'Remove group',
title: 'Delete group',
}}
>
<StyledDelete />

View File

@ -81,6 +81,7 @@ export const GroupForm: FC<IGroupForm> = ({
value={name}
onChange={e => setName(e.target.value)}
data-testid={UG_NAME_ID}
required
/>
<StyledInputDescription>
How would you describe your group?

View File

@ -2,25 +2,11 @@ import { capitalize, MenuItem, Select, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { Role } from 'interfaces/group';
import { Badge } from 'component/common/Badge/Badge';
import { StarRounded } from '@mui/icons-material';
const StyledBadge = styled('div')(({ theme }) => ({
padding: theme.spacing(0.5, 1),
textDecoration: 'none',
color: theme.palette.text.secondary,
border: `1px solid ${theme.palette.dividerAlternative}`,
background: theme.palette.activityIndicators.unknown,
display: 'inline-block',
borderRadius: theme.shape.borderRadius,
marginLeft: theme.spacing(1.5),
fontSize: theme.fontSizes.smallerBody,
fontWeight: theme.fontWeight.bold,
lineHeight: 1,
}));
const StyledOwnerBadge = styled(StyledBadge)(({ theme }) => ({
color: theme.palette.success.dark,
border: `1px solid ${theme.palette.success.border}`,
background: theme.palette.success.light,
const StyledPopupStar = styled(StarRounded)(({ theme }) => ({
color: theme.palette.warning.main,
}));
interface IGroupUserRoleCellProps {
@ -35,8 +21,12 @@ export const GroupUserRoleCell = ({
const renderBadge = () => (
<ConditionallyRender
condition={value === Role.Member}
show={<StyledBadge>{capitalize(value)}</StyledBadge>}
elseShow={<StyledOwnerBadge>{capitalize(value)}</StyledOwnerBadge>}
show={<Badge>{capitalize(value)}</Badge>}
elseShow={
<Badge color="success" icon={<StyledPopupStar />}>
{capitalize(value)}
</Badge>
}
/>
);

View File

@ -1,6 +1,6 @@
import { styled, Tooltip } from '@mui/material';
import { IGroup } from 'interfaces/group';
import { Link } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { GroupCardAvatars } from './GroupCardAvatars/GroupCardAvatars';
import { Badge } from 'component/common/Badge/Badge';
@ -20,9 +20,15 @@ const StyledGroupCard = styled('aside')(({ theme }) => ({
border: `1px solid ${theme.palette.dividerAlternative}`,
borderRadius: theme.shape.borderRadiusLarge,
boxShadow: theme.boxShadows.card,
display: 'flex',
flexDirection: 'column',
[theme.breakpoints.up('md')]: {
padding: theme.spacing(4),
},
'&:hover': {
transition: 'background-color 0.2s ease-in-out',
backgroundColor: theme.palette.neutral.light,
},
}));
const StyledRow = styled('div')(() => ({
@ -31,6 +37,14 @@ const StyledRow = styled('div')(() => ({
justifyContent: 'space-between',
}));
const StyledTitleRow = styled(StyledRow)(() => ({
alignItems: 'flex-start',
}));
const StyledBottomRow = styled(StyledRow)(() => ({
marginTop: 'auto',
}));
const StyledHeaderTitle = styled('h2')(({ theme }) => ({
fontSize: theme.fontSizes.mainHeader,
fontWeight: theme.fontWeight.medium,
@ -55,7 +69,13 @@ const StyledCounterDescription = styled('span')(({ theme }) => ({
marginLeft: theme.spacing(1),
}));
const ProjectBadgeContainer = styled('div')(() => ({}));
const ProjectBadgeContainer = styled('div')(() => ({
maxWidth: '50%',
}));
const StyledBadge = styled(Badge)(() => ({
marginRight: 0.5,
}));
interface IGroupCardProps {
group: IGroup;
@ -63,12 +83,12 @@ interface IGroupCardProps {
export const GroupCard = ({ group }: IGroupCardProps) => {
const [removeOpen, setRemoveOpen] = useState(false);
const navigate = useNavigate();
return (
<>
<StyledLink key={group.id} to={`/admin/groups/${group.id}`}>
<StyledGroupCard>
<StyledRow>
<StyledTitleRow>
<StyledHeaderTitle>{group.name}</StyledHeaderTitle>
<StyledHeaderActions>
<GroupCardActions
@ -76,9 +96,9 @@ export const GroupCard = ({ group }: IGroupCardProps) => {
onRemove={() => setRemoveOpen(true)}
/>
</StyledHeaderActions>
</StyledRow>
</StyledTitleRow>
<StyledDescription>{group.description}</StyledDescription>
<StyledRow>
<StyledBottomRow>
<ConditionallyRender
condition={group.users?.length > 0}
show={<GroupCardAvatars users={group.users} />}
@ -92,13 +112,26 @@ export const GroupCard = ({ group }: IGroupCardProps) => {
<ConditionallyRender
condition={group.projects.length > 0}
show={group.projects.map(project => (
<Badge
color="secondary"
icon={<TopicOutlinedIcon />}
sx={{ marginRight: 0.5 }}
<Tooltip
key={project}
title="View project"
arrow
placement="bottom-end"
describeChild
>
{project}
</Badge>
<StyledBadge
onClick={e => {
e.preventDefault();
navigate(
`/projects/${project}/access`
);
}}
color="secondary"
icon={<TopicOutlinedIcon />}
>
{project}
</StyledBadge>
</Tooltip>
))}
elseShow={
<Tooltip
@ -111,7 +144,7 @@ export const GroupCard = ({ group }: IGroupCardProps) => {
}
/>
</ProjectBadgeContainer>
</StyledRow>
</StyledBottomRow>
</StyledGroupCard>
</StyledLink>
<RemoveGroup

View File

@ -97,7 +97,7 @@ export const GroupCardActions: FC<IGroupCardActions> = ({
</ListItemIcon>
<ListItemText>
<Typography variant="body2">
Remove group
Delete group
</Typography>
</ListItemText>
</MenuItem>

View File

@ -15,6 +15,9 @@ const StyledAvatars = styled('div')(({ theme }) => ({
const StyledAvatar = styled(UserAvatar)(({ theme }) => ({
outline: `${theme.spacing(0.25)} solid ${theme.palette.background.paper}`,
marginLeft: theme.spacing(-1),
'&:hover': {
outlineColor: theme.palette.primary.main,
},
}));
interface IGroupCardAvatarsProps {
@ -44,6 +47,7 @@ export const GroupCardAvatars = ({ users }: IGroupCardAvatarsProps) => {
<StyledAvatars>
{shownUsers.map(user => (
<StyledAvatar
key={user.id}
user={user}
star={user.role === Role.Owner}
onMouseEnter={event => {

View File

@ -1,21 +1,18 @@
import { Badge, Popover, styled } from '@mui/material';
import { Popover, styled } from '@mui/material';
import { IGroupUser, Role } from 'interfaces/group';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Badge as StyledBadge } from 'component/common/Badge/Badge';
import StarIcon from '@mui/icons-material/Star';
import { Badge } from 'component/common/Badge/Badge';
import { StarRounded } from '@mui/icons-material';
const StyledPopover = styled(Popover)(({ theme }) => ({
pointerEvents: 'none',
'.MuiPaper-root': {
padding: '12px',
padding: theme.spacing(2),
},
}));
const StyledPopupStar = styled(StarIcon)(({ theme }) => ({
const StyledPopupStar = styled(StarRounded)(({ theme }) => ({
color: theme.palette.warning.main,
fontSize: theme.fontSizes.smallBody,
marginLeft: theme.spacing(0.1),
marginTop: theme.spacing(2),
}));
const StyledName = styled('div')(({ theme }) => ({
@ -55,22 +52,10 @@ export const GroupPopover = ({
>
<ConditionallyRender
condition={user?.role === Role.Member}
show={<StyledBadge color="success">{user?.role}</StyledBadge>}
show={<Badge>{user?.role}</Badge>}
elseShow={
<Badge
overlap="circular"
anchorOrigin={{
vertical: 'top',
horizontal: 'left',
}}
badgeContent={<StyledPopupStar />}
>
<StyledBadge
color="success"
sx={{ paddingLeft: '16px' }}
>
{user?.role}
</StyledBadge>
<Badge color="success" icon={<StyledPopupStar />}>
{user?.role}
</Badge>
}
/>

View File

@ -0,0 +1,35 @@
import { Button, styled, Typography } from '@mui/material';
import { Link } from 'react-router-dom';
export const GroupEmpty = () => {
const StyledContainerDiv = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
margin: theme.spacing(6),
marginLeft: 'auto',
marginRight: 'auto',
}));
const StyledTitle = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.bodySize,
marginBottom: theme.spacing(2.5),
}));
return (
<StyledContainerDiv>
<StyledTitle>
No groups available. Get started by adding a new group.
</StyledTitle>
<Button
to="/admin/groups/create-group"
component={Link}
variant="outlined"
color="secondary"
>
Create your first group
</Button>
</StyledContainerDiv>
);
};

View File

@ -11,6 +11,7 @@ import theme from 'themes/theme';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { TablePlaceholder } from 'component/common/Table';
import { GroupCard } from './GroupCard/GroupCard';
import { GroupEmpty } from './GroupEmpty/GroupEmpty';
type PageQueryType = Partial<Record<'search', string>>;
@ -123,12 +124,7 @@ export const GroupsList: VFC = () => {
&rdquo;
</TablePlaceholder>
}
elseShow={
<TablePlaceholder>
No groups available. Get started by adding a new
group.
</TablePlaceholder>
}
elseShow={<GroupEmpty />}
/>
}
/>

View File

@ -48,10 +48,10 @@ export const RemoveGroup: FC<IRemoveGroupProps> = ({
onClose={() => {
setOpen(false);
}}
title="Remove group"
title="Delete group"
>
<Typography>
Are you sure you wish to remove <strong>{group.name}</strong>?
Are you sure you wish to delete <strong>{group.name}</strong>?
If this group is currently assigned to one or more projects then
users belonging to this group may lose access to those projects.
</Typography>

View File

@ -1,5 +1,5 @@
import { styled, SxProps, Theme } from '@mui/material';
import {
import React, {
cloneElement,
FC,
ForwardedRef,
@ -17,6 +17,8 @@ interface IBadgeProps {
className?: string;
sx?: SxProps<Theme>;
children?: ReactNode;
title?: string;
onClick?: (event: React.SyntheticEvent) => void;
}
interface IBadgeIconProps {

View File

@ -1,6 +1,7 @@
import { Paper, styled } from '@mui/material';
import { usePageTitle } from 'hooks/usePageTitle';
import { ReactNode } from 'react';
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
const StyledMainHeader = styled(Paper)(({ theme }) => ({
borderRadius: theme.shape.borderRadiusLarge,
@ -49,7 +50,15 @@ export const MainHeader = ({
<StyledTitle>{title}</StyledTitle>
<StyledActions>{actions}</StyledActions>
</StyledTitleHeader>
Description:<StyledDescription>{description}</StyledDescription>
<ConditionallyRender
condition={Boolean(description?.length)}
show={
<>
Description:
<StyledDescription>{description}</StyledDescription>
</>
}
/>
</StyledMainHeader>
);
};

View File

@ -9,11 +9,11 @@ import {
import { IUser } from 'interfaces/user';
import { FC } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import StarIcon from '@mui/icons-material/Star';
import { StarRounded } from '@mui/icons-material';
const StyledAvatar = styled(Avatar)(({ theme }) => ({
width: theme.spacing(4),
height: theme.spacing(4),
width: theme.spacing(3.5),
height: theme.spacing(3.5),
margin: 'auto',
backgroundColor: theme.palette.secondary.light,
color: theme.palette.text.primary,
@ -21,7 +21,7 @@ const StyledAvatar = styled(Avatar)(({ theme }) => ({
fontWeight: theme.fontWeight.bold,
}));
const StyledStar = styled(StarIcon)(({ theme }) => ({
const StyledStar = styled(StarRounded)(({ theme }) => ({
color: theme.palette.warning.main,
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadiusExtraLarge,

View File

@ -44,27 +44,44 @@ export const ProjectRoleDescription: VFC<IProjectRoleDescriptionProps> = ({
const environments = useMemo(() => {
const environments = new Set<string>();
role.permissions
?.filter((permission: any) => permission.environment !== '')
?.filter((permission: any) => permission.environment)
.forEach((permission: any) => {
environments.add(permission.environment);
});
return [...environments].sort();
}, [role]);
const projectPermissions = useMemo(() => {
return role.permissions?.filter(
(permission: any) => !permission.environment
);
}, [role]);
return (
<StyledDescription>
<StyledDescriptionHeader>
Project permissions
</StyledDescriptionHeader>
<StyledDescriptionBlock>
{role.permissions
?.filter((permission: any) => permission.environment === '')
.map((permission: any) => permission.displayName)
.sort()
.map((permission: any) => (
<p key={permission}>{permission}</p>
))}
</StyledDescriptionBlock>
<ConditionallyRender
condition={Boolean(projectPermissions?.length)}
show={
<>
<StyledDescriptionHeader>
Project permissions
</StyledDescriptionHeader>
<StyledDescriptionBlock>
{role.permissions
?.filter(
(permission: any) => !permission.environment
)
.map(
(permission: any) => permission.displayName
)
.sort()
.map((permission: any) => (
<p key={permission}>{permission}</p>
))}
</StyledDescriptionBlock>
</>
}
/>
<ConditionallyRender
condition={Boolean(environments.length)}
show={