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}
+
+
+ );
+};