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:
parent
4486901a4b
commit
d10c151dea
@ -255,7 +255,7 @@ export const Group: VFC = () => {
|
||||
onClick={() => setRemoveOpen(true)}
|
||||
permission={ADMIN}
|
||||
tooltipProps={{
|
||||
title: 'Remove group',
|
||||
title: 'Delete group',
|
||||
}}
|
||||
>
|
||||
<StyledDelete />
|
||||
|
@ -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?
|
||||
|
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -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
|
||||
|
@ -97,7 +97,7 @@ export const GroupCardActions: FC<IGroupCardActions> = ({
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
<Typography variant="body2">
|
||||
Remove group
|
||||
Delete group
|
||||
</Typography>
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
|
@ -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 => {
|
||||
|
@ -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>
|
||||
}
|
||||
/>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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 = () => {
|
||||
”
|
||||
</TablePlaceholder>
|
||||
}
|
||||
elseShow={
|
||||
<TablePlaceholder>
|
||||
No groups available. Get started by adding a new
|
||||
group.
|
||||
</TablePlaceholder>
|
||||
}
|
||||
elseShow={<GroupEmpty />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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={
|
||||
|
Loading…
Reference in New Issue
Block a user