Merge pull request #33082 from backstage/bui-migrate-catalog-entity-cards

feat(catalog,org): migrate entity cards from MUI/InfoCard to BUI
This commit is contained in:
Patrik Oldsberg
2026-03-04 11:50:41 +01:00
committed by GitHub
33 changed files with 528 additions and 373 deletions
+1
View File
@@ -57,6 +57,7 @@
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-catalog-react": "workspace:^",
"@backstage/ui": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
-4
View File
@@ -18,7 +18,6 @@ export type ComponentsGridClassKey =
// @public (undocumented)
export const EntityGroupProfileCard: (props: {
variant?: InfoCardVariants;
showLinks?: boolean;
}) => JSX_2.Element;
@@ -48,7 +47,6 @@ export type EntityRelationAggregation = 'direct' | 'aggregated';
// @public (undocumented)
export const EntityUserProfileCard: (props: {
variant?: InfoCardVariants;
showLinks?: boolean;
maxRelations?: number;
hideIcons?: boolean;
@@ -56,7 +54,6 @@ export const EntityUserProfileCard: (props: {
// @public (undocumented)
export const GroupProfileCard: (props: {
variant?: InfoCardVariants;
showLinks?: boolean;
}) => JSX_2.Element;
@@ -116,7 +113,6 @@ export type OwnershipCardClassKey =
// @public (undocumented)
export const UserProfileCard: (props: {
variant?: InfoCardVariants;
showLinks?: boolean;
maxRelations?: number;
hideIcons?: boolean;
@@ -22,13 +22,7 @@ import {
RELATION_PARENT_OF,
stringifyEntityRef,
} from '@backstage/catalog-model';
import {
Avatar,
InfoCard,
InfoCardVariants,
Link,
} from '@backstage/core-components';
import Box from '@material-ui/core/Box';
import { Link } from '@backstage/core-components';
import IconButton from '@material-ui/core/IconButton';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
@@ -36,6 +30,7 @@ import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import Tooltip from '@material-ui/core/Tooltip';
import {
EntityInfoCard,
EntityRefLinks,
catalogApiRef,
getEntityRelations,
@@ -57,6 +52,7 @@ import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { orgTranslationRef } from '../../../../translation';
import { makeStyles } from '@material-ui/core/styles';
import { Avatar, Flex, Box, Text } from '@backstage/ui';
const useStyles = makeStyles(theme => ({
container: {
@@ -71,18 +67,21 @@ const useStyles = makeStyles(theme => ({
},
}));
const CardTitle = (props: { title: string }) => (
<Box display="flex" alignItems="center">
<GroupIcon fontSize="inherit" />
<Box ml={1}>{props.title}</Box>
</Box>
const CardTitle = (props: { title: string; pictureSrc?: string }) => (
<Flex align="center" gap="2">
<Avatar
size="small"
purpose="decoration"
name={props.title}
src={props.pictureSrc || ''}
/>
{props.title}
</Flex>
);
/** @public */
export const GroupProfileCard = (props: {
variant?: InfoCardVariants;
showLinks?: boolean;
}) => {
export const GroupProfileCard = (props: { showLinks?: boolean }) => {
const { showLinks } = props;
const catalogApi = useApi(catalogApiRef);
const alertApi = useApi(alertApiRef);
const { entity: group } = useEntity<GroupEntity>();
@@ -148,11 +147,9 @@ export const GroupProfileCard = (props: {
);
return (
<InfoCard
title={<CardTitle title={displayName} />}
subheader={description}
variant={props.variant}
action={
<EntityInfoCard
title={<CardTitle title={displayName} pictureSrc={profile?.picture} />}
headerActions={
<>
{allowRefresh && canRefresh && (
<IconButton
@@ -167,8 +164,8 @@ export const GroupProfileCard = (props: {
</>
}
>
<Box className={classes.container}>
<Avatar displayName={displayName} picture={profile?.picture} />
{description && <Text color="secondary">{description}</Text>}
<Box p="2">
<List className={classes.list}>
<ListItem>
<ListItemIcon>
@@ -234,9 +231,9 @@ export const GroupProfileCard = (props: {
secondary={t('groupProfileCard.listItemTitle.childGroups')}
/>
</ListItem>
{props?.showLinks && <LinksGroup links={links} />}
{showLinks && <LinksGroup links={links} />}
</List>
</Box>
</InfoCard>
</EntityInfoCard>
);
};
@@ -55,7 +55,7 @@ export const Default = () => (
<EntityProvider entity={defaultEntity}>
<Grid container spacing={4}>
<Grid item xs={12} md={4}>
<UserProfileCard variant="gridItem" />
<UserProfileCard />
</Grid>
</Grid>
</EntityProvider>
@@ -82,7 +82,7 @@ export const NoImage = () => (
<EntityProvider entity={noImageEntity}>
<Grid container spacing={4}>
<Grid item xs={12} md={4}>
<UserProfileCard variant="gridItem" />
<UserProfileCard />
</Grid>
</Grid>
</EntityProvider>
@@ -134,7 +134,7 @@ export const ExtraDetails = () => (
<EntityProvider entity={extraDetailsEntity}>
<Grid container spacing={4}>
<Grid item xs={12} md={4}>
<UserProfileCard variant="gridItem" />
<UserProfileCard />
</Grid>
</Grid>
</EntityProvider>
@@ -51,7 +51,7 @@ describe('UserSummary Test', () => {
it('Display Profile Card', async () => {
await renderInTestApp(
<EntityProvider entity={userEntity}>
<UserProfileCard variant="gridItem" />
<UserProfileCard />
</EntityProvider>,
{
mountedRoutes: {
@@ -61,10 +61,10 @@ describe('UserSummary Test', () => {
);
expect(screen.getByText('calum-leavy@example.com')).toBeInTheDocument();
expect(screen.getByAltText('Calum Leavy')).toHaveAttribute(
'src',
'https://example.com/staff/calum.jpeg',
);
// BUI Avatar is decorative (aria-hidden), because the name is
// present as text right beside it.
expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument();
expect(screen.getByText('Calum Leavy')).toBeInTheDocument();
expect(screen.getByText('examplegroup').closest('a')).toHaveAttribute(
'href',
'/catalog/default/group/examplegroup',
@@ -87,7 +87,7 @@ describe('UserSummary Test', () => {
it('Should limit the number of displayed groups in the card', async () => {
await renderInTestApp(
<EntityProvider entity={entity}>
<UserProfileCard variant="gridItem" maxRelations={3} />
<UserProfileCard maxRelations={3} />
</EntityProvider>,
{
mountedRoutes: {
@@ -109,7 +109,7 @@ describe('UserSummary Test', () => {
it('Show more groups button when there are more user relations than the maximum', async () => {
await renderInTestApp(
<EntityProvider entity={entity}>
<UserProfileCard variant="gridItem" maxRelations={3} />
<UserProfileCard maxRelations={3} />
</EntityProvider>,
{
mountedRoutes: {
@@ -128,7 +128,7 @@ describe('UserSummary Test', () => {
it('Hide more groups button when limit value is less than or equal to user relations', async () => {
await renderInTestApp(
<EntityProvider entity={entity}>
<UserProfileCard variant="gridItem" maxRelations={5} />
<UserProfileCard maxRelations={5} />
</EntityProvider>,
{
mountedRoutes: {
@@ -148,7 +148,7 @@ describe('UserSummary Test', () => {
it('Hide all groups when max relations is equals to zero', async () => {
await renderInTestApp(
<EntityProvider entity={entity}>
<UserProfileCard variant="gridItem" maxRelations={0} />
<UserProfileCard maxRelations={0} />
</EntityProvider>,
{
mountedRoutes: {
@@ -193,7 +193,7 @@ describe('Edit Button', () => {
await renderInTestApp(
<EntityProvider entity={userEntity}>
<UserProfileCard variant="gridItem" />
<UserProfileCard />
</EntityProvider>,
{
mountedRoutes: {
@@ -234,7 +234,7 @@ describe('Edit Button', () => {
await renderInTestApp(
<EntityProvider entity={userEntity}>
<UserProfileCard variant="gridItem" />
<UserProfileCard />
</EntityProvider>,
{
mountedRoutes: {
@@ -285,7 +285,7 @@ describe('Edit Button', () => {
await renderInTestApp(
<EntityProvider entity={userEntity}>
<UserProfileCard variant="gridItem" />
<UserProfileCard />
</EntityProvider>,
{
mountedRoutes: {
@@ -337,7 +337,7 @@ describe('Edit Button', () => {
await renderInTestApp(
<EntityProvider entity={userEntity}>
<UserProfileCard showLinks variant="gridItem" />
<UserProfileCard showLinks />
</EntityProvider>,
{
mountedRoutes: {
@@ -19,15 +19,10 @@ import {
RELATION_MEMBER_OF,
UserEntity,
} from '@backstage/catalog-model';
import {
Avatar,
InfoCard,
InfoCardVariants,
Link,
} from '@backstage/core-components';
import { Link } from '@backstage/core-components';
import { EntityInfoCard } from '@backstage/plugin-catalog-react';
import { Avatar, Box, Flex, Text } from '@backstage/ui';
import { createStyles, makeStyles } from '@material-ui/core/styles';
import Box from '@material-ui/core/Box';
import Grid from '@material-ui/core/Grid';
import BaseButton from '@material-ui/core/ButtonBase';
import IconButton from '@material-ui/core/IconButton';
import List from '@material-ui/core/List';
@@ -52,7 +47,6 @@ import EditIcon from '@material-ui/icons/Edit';
import EmailIcon from '@material-ui/icons/Email';
import GroupIcon from '@material-ui/icons/Group';
import { LinksGroup } from '../../Meta';
import PersonIcon from '@material-ui/icons/Person';
import { useCallback, useState } from 'react';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
@@ -84,17 +78,21 @@ const useStyles = makeStyles(
{ name: 'PluginOrgUserProfileCard' },
);
const CardTitle = (props: { title?: string }) =>
const CardTitle = (props: { title: string; pictureSrc?: string }) =>
props.title ? (
<Box display="flex" alignItems="center">
<PersonIcon fontSize="inherit" />
<Box ml={1}>{props.title}</Box>
</Box>
<Flex align="center" gap="2">
<Avatar
size="small"
purpose="decoration"
name={props.title}
src={props.pictureSrc || ''}
/>
{props.title}
</Flex>
) : null;
/** @public */
export const UserProfileCard = (props: {
variant?: InfoCardVariants;
showLinks?: boolean;
maxRelations?: number;
hideIcons?: boolean;
@@ -132,11 +130,9 @@ export const UserProfileCard = (props: {
});
return (
<InfoCard
title={<CardTitle title={displayName} />}
subheader={description}
variant={props.variant}
action={
<EntityInfoCard
title={<CardTitle title={displayName} pictureSrc={profile?.picture} />}
headerActions={
<>
{entityMetadataEditUrl && (
<IconButton
@@ -151,62 +147,55 @@ export const UserProfileCard = (props: {
</>
}
>
<Grid container spacing={3} alignItems="flex-start">
<Grid item xs={12} sm={2} xl={1}>
<Avatar displayName={displayName} picture={profile?.picture} />
</Grid>
{description && <Text color="secondary">{description}</Text>}
<Box p="2">
<List>
{profile?.email && (
<ListItem>
<ListItemIcon>
<Tooltip title={t('userProfileCard.listItemTitle.email')}>
<EmailIcon />
</Tooltip>
</ListItemIcon>
<ListItemText>
<Link to={emailHref ?? ''}>{profile.email}</Link>
</ListItemText>
</ListItem>
)}
<Grid item md={10} xl={11}>
<List>
{profile?.email && (
<ListItem>
<ListItemIcon>
<Tooltip title={t('userProfileCard.listItemTitle.email')}>
<EmailIcon />
</Tooltip>
</ListItemIcon>
<ListItemText>
<Link to={emailHref ?? ''}>{profile.email}</Link>
</ListItemText>
</ListItem>
)}
{maxRelations === undefined || maxRelations > 0 ? (
<ListItem>
<ListItemIcon>
<Tooltip title={t('userProfileCard.listItemTitle.memberOf')}>
<GroupIcon />
</Tooltip>
</ListItemIcon>
<ListItemText>
<EntityRefLinks
entityRefs={memberOfRelations.slice(0, maxRelations)}
defaultKind="Group"
hideIcons={hideIcons}
/>
{maxRelations && memberOfRelations.length > maxRelations ? (
<>
,
<BaseButton
className={classes.moreButton}
onClick={toggleAllGroupsDialog}
disableRipple
>
{t('userProfileCard.moreGroupButtonTitle', {
number: String(
memberOfRelations.length - maxRelations,
),
})}
</BaseButton>
</>
) : null}
</ListItemText>
</ListItem>
) : null}
{props?.showLinks && <LinksGroup links={links} />}
</List>
</Grid>
</Grid>
{maxRelations === undefined || maxRelations > 0 ? (
<ListItem>
<ListItemIcon>
<Tooltip title={t('userProfileCard.listItemTitle.memberOf')}>
<GroupIcon />
</Tooltip>
</ListItemIcon>
<ListItemText>
<EntityRefLinks
entityRefs={memberOfRelations.slice(0, maxRelations)}
defaultKind="Group"
hideIcons={hideIcons}
/>
{maxRelations && memberOfRelations.length > maxRelations ? (
<>
,
<BaseButton
className={classes.moreButton}
onClick={toggleAllGroupsDialog}
disableRipple
>
{t('userProfileCard.moreGroupButtonTitle', {
number: String(memberOfRelations.length - maxRelations),
})}
</BaseButton>
</>
) : null}
</ListItemText>
</ListItem>
) : null}
{props?.showLinks && <LinksGroup links={links} />}
</List>
</Box>
<Dialog
classes={{ paper: classes.dialogPaper }}
@@ -238,6 +227,6 @@ export const UserProfileCard = (props: {
</Button>
</DialogActions>
</Dialog>
</InfoCard>
</EntityInfoCard>
);
};