diff --git a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCard.tsx b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCard.tsx index d85ec189c7..a3028c640e 100644 --- a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCard.tsx +++ b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCard.tsx @@ -1,4 +1,4 @@ -import { styled, Tooltip } from '@mui/material'; +import { Box, Card, styled } from '@mui/material'; import type { IGroup } from 'interfaces/group'; import { Link, useNavigate } from 'react-router-dom'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; @@ -7,88 +7,119 @@ import { GroupCardActions } from './GroupCardActions/GroupCardActions'; import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined'; import { RoleBadge } from 'component/common/RoleBadge/RoleBadge'; import { useScimSettings } from 'hooks/api/getters/useScimSettings/useScimSettings'; -import { AvatarGroup } from 'component/common/AvatarGroup/AvatarGroup'; +import { + AvatarComponent, + AvatarGroup, +} from 'component/common/AvatarGroup/AvatarGroup'; +import GroupsIcon from '@mui/icons-material/GroupsOutlined'; +import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; +import { Highlighter } from 'component/common/Highlighter/Highlighter'; +import { Truncator } from 'component/common/Truncator/Truncator'; +import { TooltipLink } from 'component/common/TooltipLink/TooltipLink'; -const StyledLink = styled(Link)(({ theme }) => ({ +const StyledCardLink = styled(Link)(({ theme }) => ({ + color: 'inherit', textDecoration: 'none', - color: theme.palette.text.primary, + border: 'none', + padding: '0', + background: 'transparent', + fontFamily: theme.typography.fontFamily, + pointer: 'cursor', })); -const StyledGroupCard = styled('aside')(({ theme }) => ({ - padding: theme.spacing(2.5), - height: '100%', - border: `1px solid ${theme.palette.divider}`, - borderRadius: theme.shape.borderRadiusLarge, - boxShadow: theme.boxShadows.card, +const StyledCard = styled(Card)(({ theme }) => ({ display: 'flex', flexDirection: 'column', - [theme.breakpoints.up('md')]: { - padding: theme.spacing(4), + justifyContent: 'space-between', + height: '100%', + boxShadow: 'none', + border: `1px solid ${theme.palette.divider}`, + [theme.breakpoints.down('sm')]: { + justifyContent: 'center', }, + transition: 'background-color 0.2s ease-in-out', + backgroundColor: theme.palette.background.default, '&:hover': { - transition: 'background-color 0.2s ease-in-out', backgroundColor: theme.palette.neutral.light, }, + borderRadius: theme.shape.borderRadiusMedium, })); -const StyledRow = styled('div')(() => ({ +const StyledCardBody = styled(Box)(({ theme }) => ({ + padding: theme.spacing(2), display: 'flex', + flexFlow: 'column', + height: '100%', + position: 'relative', +})); + +const StyledCardBodyHeader = styled('div')(({ theme }) => ({ + display: 'flex', + gap: theme.spacing(1), + width: '100%', alignItems: 'center', justifyContent: 'space-between', })); -const StyledTitleRow = styled(StyledRow)(() => ({ - alignItems: 'flex-start', +const StyledCardIconContainer = styled(Box)(({ theme }) => ({ + display: 'grid', + placeItems: 'center', + padding: theme.spacing(0.5), + alignSelf: 'baseline', + backgroundColor: theme.palette.secondary.light, + color: theme.palette.primary.main, + borderRadius: theme.shape.borderRadiusMedium, + '& > svg': { + height: theme.spacing(2), + width: theme.spacing(2), + }, })); -const StyledBottomRow = styled(StyledRow)(({ theme }) => ({ - marginTop: 'auto', - alignItems: 'flex-end', +const StyledCardTitle = styled('h3')(({ theme }) => ({ + margin: 0, + marginRight: 'auto', + fontWeight: theme.typography.fontWeightRegular, + fontSize: theme.typography.body1.fontSize, + lineHeight: '1.2', +})); + +const StyledCardDescription = styled('p')(({ theme }) => ({ + color: theme.palette.text.secondary, + fontSize: theme.fontSizes.smallBody, + marginTop: theme.spacing(2), +})); + +const StyledCardFooter = styled(Box)(({ theme }) => ({ + padding: theme.spacing(0, 2), + display: 'flex', + background: theme.palette.envAccordion.expanded, + boxShadow: theme.boxShadows.accordionFooter, + alignItems: 'center', + justifyContent: 'space-between', + borderTop: `1px solid ${theme.palette.divider}`, + minHeight: theme.spacing(6.25), +})); + +const StyledCardFooterSpan = styled('span')(({ theme }) => ({ + fontSize: theme.fontSizes.smallerBody, + color: theme.palette.text.secondary, + textWrap: 'nowrap', +})); + +const StyledAvatarComponent = styled(AvatarComponent)(({ theme }) => ({ + height: theme.spacing(2.5), + width: theme.spacing(2.5), + marginLeft: theme.spacing(-0.75), +})); + +const StyledProjectsTooltip = styled(Box)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', gap: theme.spacing(1), })); -const StyledHeaderTitle = styled('h2')(({ theme }) => ({ - fontSize: theme.fontSizes.mainHeader, - fontWeight: theme.fontWeight.medium, -})); - -const StyledHeaderActions = styled('div')(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - color: theme.palette.text.secondary, - fontSize: theme.fontSizes.smallBody, -})); - -const StyledDescription = styled('p')(({ theme }) => ({ - color: theme.palette.text.secondary, - fontSize: theme.fontSizes.smallBody, - marginTop: theme.spacing(1), - marginBottom: theme.spacing(4), -})); - -const StyledCounterDescription = styled('span')(({ theme }) => ({ - color: theme.palette.text.secondary, - marginLeft: theme.spacing(1), -})); - -const ProjectBadgeContainer = styled('div')(({ theme }) => ({ - maxWidth: '50%', - display: 'flex', - justifyContent: 'flex-end', - gap: theme.spacing(0.5), - flexWrap: 'wrap', -})); - -const InfoBadgeDescription = styled('span')(({ theme }) => ({ - display: 'flex', - color: theme.palette.text.secondary, - alignItems: 'center', - gap: theme.spacing(1), - fontSize: theme.fontSizes.smallBody, -})); - -const ProjectNameBadge = styled(Badge)({ - wordBreak: 'break-word', +const StyledProjectBadge = styled(Badge)({ + cursor: 'pointer', }); interface IGroupCardProps { @@ -104,6 +135,8 @@ export const GroupCard = ({ }: IGroupCardProps) => { const navigate = useNavigate(); + const { searchQuery } = useSearchHighlightContext(); + const { settings: { enabled: scimEnabled }, } = useScimSettings(); @@ -111,84 +144,104 @@ export const GroupCard = ({ return ( <> - - - - {group.name} - + + + + + + + + + + {group.name} + + + + } + /> onEditUsers(group)} onRemove={() => onRemoveGroup(group)} isScimGroup={isScimGroup} /> - - - -

Root role:

- - - } - /> - - {group.description} - + 0} - show={} - elseShow={ - - This group has no users. - + condition={Boolean(group.description)} + show={ + + + {group.description} + + } /> - - 0} - show={group.projects.map((project) => ( - - { - e.preventDefault(); - navigate( - `/projects/${project}/settings/access`, - ); - }} - color='secondary' - icon={} - > - {project} - - - ))} - elseShow={ - - Not used - - } - /> - } - /> - - -
-
+ + + 0} + show={ + + } + elseShow={ + + This group has no users + + } + /> + 0} + show={ + + {group.projects.map((project) => ( + { + e.preventDefault(); + navigate( + `/projects/${project}/settings/access`, + ); + }} + color='secondary' + icon={} + > + {project} + + ))} + + } + > + + {group.projects.length} project + {group.projects.length !== 1 && 's'} + + + } + /> + + + ); }; diff --git a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardActions/GroupCardActions.tsx b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardActions/GroupCardActions.tsx index 6e9b0849a5..a38f563e27 100644 --- a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardActions/GroupCardActions.tsx +++ b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardActions/GroupCardActions.tsx @@ -18,9 +18,11 @@ import { Link } from 'react-router-dom'; import { scimGroupTooltip } from 'component/admin/groups/group-constants'; const StyledActions = styled('div')(({ theme }) => ({ + margin: theme.spacing(-1), + marginLeft: theme.spacing(-0.5), display: 'flex', justifyContent: 'center', - transform: 'translate3d(8px, -6px, 0)', + alignItems: 'center', })); const StyledPopover = styled(Popover)(({ theme }) => ({ @@ -51,7 +53,7 @@ export const GroupCardActions: FC = ({ setAnchorEl(null); }; - const id = `feature-${groupId}-actions`; + const id = `group-${groupId}-actions`; const menuId = `${id}-menu`; return ( @@ -74,6 +76,7 @@ export const GroupCardActions: FC = ({ aria-expanded={open ? 'true' : undefined} onClick={handleClick} type='button' + size='small' > diff --git a/frontend/src/component/admin/groups/GroupsList/GroupsList.tsx b/frontend/src/component/admin/groups/GroupsList/GroupsList.tsx index d7a9f553e3..d15098522c 100644 --- a/frontend/src/component/admin/groups/GroupsList/GroupsList.tsx +++ b/frontend/src/component/admin/groups/GroupsList/GroupsList.tsx @@ -6,7 +6,7 @@ import { PageContent } from 'component/common/PageContent/PageContent'; import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { Search } from 'component/common/Search/Search'; -import { Grid, useMediaQuery } from '@mui/material'; +import { styled, useMediaQuery } from '@mui/material'; import theme from 'themes/theme'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { TablePlaceholder } from 'component/common/Table'; @@ -19,6 +19,12 @@ import { NAVIGATE_TO_CREATE_GROUP } from 'utils/testIds'; import { EditGroupUsers } from '../Group/EditGroupUsers/EditGroupUsers'; import { RemoveGroup } from '../RemoveGroup/RemoveGroup'; +const StyledGridContainer = styled('div')(({ theme }) => ({ + display: 'grid', + gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', + gap: theme.spacing(2), +})); + type PageQueryType = Partial>; const groupsSearch = (group: IGroup, searchValue: string) => { @@ -131,17 +137,16 @@ export const GroupsList: VFC = () => { } > - + {data.map((group) => ( - - - + ))} - + { +export const RoleBadge = ({ roleId, hideIcon, children }: IRoleBadgeProps) => { const { role } = useRole(roleId.toString()); + const icon = hideIcon ? undefined : ; + if (!role) { if (children) return ( - }> + {children} ); @@ -24,11 +27,7 @@ export const RoleBadge = ({ roleId, children }: IRoleBadgeProps) => { return ( } arrow> - } - sx={{ cursor: 'pointer' }} - > + {role.name} diff --git a/frontend/src/component/common/StringTruncator/StringTruncator.tsx b/frontend/src/component/common/StringTruncator/StringTruncator.tsx index 74205701f8..416b4c4fd5 100644 --- a/frontend/src/component/common/StringTruncator/StringTruncator.tsx +++ b/frontend/src/component/common/StringTruncator/StringTruncator.tsx @@ -8,6 +8,9 @@ interface IStringTruncatorProps { maxLength: number; } +/** + * @Deprecated in favor of Truncator + */ const StringTruncator = ({ text, maxWidth, diff --git a/frontend/src/component/common/Truncator/Truncator.tsx b/frontend/src/component/common/Truncator/Truncator.tsx new file mode 100644 index 0000000000..5d989f74a0 --- /dev/null +++ b/frontend/src/component/common/Truncator/Truncator.tsx @@ -0,0 +1,75 @@ +import { useState, useEffect, useRef } from 'react'; +import { + Box, + type BoxProps, + styled, + Tooltip, + type TooltipProps, +} from '@mui/material'; + +const StyledTruncatorContainer = styled(Box, { + shouldForwardProp: (prop) => prop !== 'lines', +})<{ lines: number }>(({ lines }) => ({ + lineClamp: `${lines}`, + WebkitLineClamp: lines, + display: '-webkit-box', + boxOrient: 'vertical', + textOverflow: 'ellipsis', + overflow: 'hidden', + alignItems: 'flex-start', + WebkitBoxOrient: 'vertical', + wordBreak: 'break-word', +})); + +type OverridableTooltipProps = Omit; + +interface ITruncatorProps extends BoxProps { + lines?: number; + title?: string; + arrow?: boolean; + tooltipProps?: OverridableTooltipProps; + children: React.ReactNode; +} + +export const Truncator = ({ + lines = 1, + title, + arrow, + tooltipProps, + children, + ...props +}: ITruncatorProps) => { + const [isTruncated, setIsTruncated] = useState(false); + const ref = useRef(null); + + const checkTruncation = () => { + if (ref.current) { + setIsTruncated(ref.current.scrollHeight > ref.current.offsetHeight); + } + }; + + useEffect(() => { + const resizeObserver = new ResizeObserver(checkTruncation); + if (ref.current) { + resizeObserver.observe(ref.current); + } + return () => resizeObserver.disconnect(); + }, [title, children]); + + const overridableTooltipProps: OverridableTooltipProps = { + title, + arrow, + ...tooltipProps, + }; + + const { title: tooltipTitle, ...otherTooltipProps } = + overridableTooltipProps; + + return ( + + + {children} + + + ); +};