1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-04 00:18:01 +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)} onClick={() => setRemoveOpen(true)}
permission={ADMIN} permission={ADMIN}
tooltipProps={{ tooltipProps={{
title: 'Remove group', title: 'Delete group',
}} }}
> >
<StyledDelete /> <StyledDelete />

View File

@ -81,6 +81,7 @@ export const GroupForm: FC<IGroupForm> = ({
value={name} value={name}
onChange={e => setName(e.target.value)} onChange={e => setName(e.target.value)}
data-testid={UG_NAME_ID} data-testid={UG_NAME_ID}
required
/> />
<StyledInputDescription> <StyledInputDescription>
How would you describe your group? 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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { Role } from 'interfaces/group'; import { Role } from 'interfaces/group';
import { Badge } from 'component/common/Badge/Badge';
import { StarRounded } from '@mui/icons-material';
const StyledBadge = styled('div')(({ theme }) => ({ const StyledPopupStar = styled(StarRounded)(({ theme }) => ({
padding: theme.spacing(0.5, 1), color: theme.palette.warning.main,
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,
})); }));
interface IGroupUserRoleCellProps { interface IGroupUserRoleCellProps {
@ -35,8 +21,12 @@ export const GroupUserRoleCell = ({
const renderBadge = () => ( const renderBadge = () => (
<ConditionallyRender <ConditionallyRender
condition={value === Role.Member} condition={value === Role.Member}
show={<StyledBadge>{capitalize(value)}</StyledBadge>} show={<Badge>{capitalize(value)}</Badge>}
elseShow={<StyledOwnerBadge>{capitalize(value)}</StyledOwnerBadge>} elseShow={
<Badge color="success" icon={<StyledPopupStar />}>
{capitalize(value)}
</Badge>
}
/> />
); );

View File

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

View File

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

View File

@ -15,6 +15,9 @@ const StyledAvatars = styled('div')(({ theme }) => ({
const StyledAvatar = styled(UserAvatar)(({ theme }) => ({ const StyledAvatar = styled(UserAvatar)(({ theme }) => ({
outline: `${theme.spacing(0.25)} solid ${theme.palette.background.paper}`, outline: `${theme.spacing(0.25)} solid ${theme.palette.background.paper}`,
marginLeft: theme.spacing(-1), marginLeft: theme.spacing(-1),
'&:hover': {
outlineColor: theme.palette.primary.main,
},
})); }));
interface IGroupCardAvatarsProps { interface IGroupCardAvatarsProps {
@ -44,6 +47,7 @@ export const GroupCardAvatars = ({ users }: IGroupCardAvatarsProps) => {
<StyledAvatars> <StyledAvatars>
{shownUsers.map(user => ( {shownUsers.map(user => (
<StyledAvatar <StyledAvatar
key={user.id}
user={user} user={user}
star={user.role === Role.Owner} star={user.role === Role.Owner}
onMouseEnter={event => { 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 { IGroupUser, Role } from 'interfaces/group';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Badge as StyledBadge } from 'component/common/Badge/Badge'; import { Badge } from 'component/common/Badge/Badge';
import StarIcon from '@mui/icons-material/Star'; import { StarRounded } from '@mui/icons-material';
const StyledPopover = styled(Popover)(({ theme }) => ({ const StyledPopover = styled(Popover)(({ theme }) => ({
pointerEvents: 'none', pointerEvents: 'none',
'.MuiPaper-root': { '.MuiPaper-root': {
padding: '12px', padding: theme.spacing(2),
}, },
})); }));
const StyledPopupStar = styled(StarIcon)(({ theme }) => ({ const StyledPopupStar = styled(StarRounded)(({ theme }) => ({
color: theme.palette.warning.main, color: theme.palette.warning.main,
fontSize: theme.fontSizes.smallBody,
marginLeft: theme.spacing(0.1),
marginTop: theme.spacing(2),
})); }));
const StyledName = styled('div')(({ theme }) => ({ const StyledName = styled('div')(({ theme }) => ({
@ -55,22 +52,10 @@ export const GroupPopover = ({
> >
<ConditionallyRender <ConditionallyRender
condition={user?.role === Role.Member} condition={user?.role === Role.Member}
show={<StyledBadge color="success">{user?.role}</StyledBadge>} show={<Badge>{user?.role}</Badge>}
elseShow={ elseShow={
<Badge <Badge color="success" icon={<StyledPopupStar />}>
overlap="circular" {user?.role}
anchorOrigin={{
vertical: 'top',
horizontal: 'left',
}}
badgeContent={<StyledPopupStar />}
>
<StyledBadge
color="success"
sx={{ paddingLeft: '16px' }}
>
{user?.role}
</StyledBadge>
</Badge> </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 { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { TablePlaceholder } from 'component/common/Table'; import { TablePlaceholder } from 'component/common/Table';
import { GroupCard } from './GroupCard/GroupCard'; import { GroupCard } from './GroupCard/GroupCard';
import { GroupEmpty } from './GroupEmpty/GroupEmpty';
type PageQueryType = Partial<Record<'search', string>>; type PageQueryType = Partial<Record<'search', string>>;
@ -123,12 +124,7 @@ export const GroupsList: VFC = () => {
&rdquo; &rdquo;
</TablePlaceholder> </TablePlaceholder>
} }
elseShow={ elseShow={<GroupEmpty />}
<TablePlaceholder>
No groups available. Get started by adding a new
group.
</TablePlaceholder>
}
/> />
} }
/> />

View File

@ -48,10 +48,10 @@ export const RemoveGroup: FC<IRemoveGroupProps> = ({
onClose={() => { onClose={() => {
setOpen(false); setOpen(false);
}} }}
title="Remove group" title="Delete group"
> >
<Typography> <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 If this group is currently assigned to one or more projects then
users belonging to this group may lose access to those projects. users belonging to this group may lose access to those projects.
</Typography> </Typography>

View File

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

View File

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

View File

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

View File

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