Merge pull request #29984 from backstage/camilaibs/api-org-ownership-entity-card
[NFS] Enhance user profile card configurations
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
---
|
||||
'@backstage/plugin-org': patch
|
||||
---
|
||||
|
||||
Enhance user profile card configuration:
|
||||
|
||||
- Added a new optional `maxRelations` numerical configuration that controls over how many user groups are shown directly on the profile card:
|
||||
- If the setting is omitted, all relations will be shown.
|
||||
- If `maxRelations` is set to `0`, the list of user groups is not displayed.
|
||||
- If `maxRelations` is set to a positive number, up to that many groups are displayed.
|
||||
- If the user belongs to more groups than the specified limit, a clickable link appears that opens a dialog showing all associated user groups.
|
||||
- A complementary boolean configuration, `hideIcons`, was added to optionally hide the visual icons associated with each group in the displayed list.
|
||||
- Usage example:
|
||||
```yaml
|
||||
# Example in app-config.yaml
|
||||
app:
|
||||
extensions:
|
||||
- entity-card:org/user-profile:
|
||||
config:
|
||||
maxRelations: 5 # (optional) Show up to 5 groups on the card
|
||||
hideIcons: true # (optional) Hide the group icons
|
||||
```
|
||||
@@ -56,7 +56,10 @@ app:
|
||||
- entity-card:org/group-profile
|
||||
- entity-card:org/members-list
|
||||
- entity-card:org/ownership
|
||||
- entity-card:org/user-profile
|
||||
- entity-card:org/user-profile:
|
||||
config:
|
||||
maxRelations: 5
|
||||
hideIcons: true
|
||||
# - entity-card:azure-devops/readme
|
||||
|
||||
# Entity page contents
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { AnyExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { EntityCardType } from '@backstage/plugin-catalog-react/alpha';
|
||||
import { EntityPredicate } from '@backstage/plugin-catalog-react/alpha';
|
||||
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionInput } from '@backstage/frontend-plugin-api';
|
||||
import { ExternalRouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { FrontendPlugin } from '@backstage/frontend-plugin-api';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
@@ -143,13 +145,17 @@ const _default: FrontendPlugin<
|
||||
};
|
||||
}>;
|
||||
'entity-card:org/user-profile': ExtensionDefinition<{
|
||||
kind: 'entity-card';
|
||||
name: 'user-profile';
|
||||
config: {
|
||||
maxRelations: number | undefined;
|
||||
hideIcons: boolean;
|
||||
} & {
|
||||
filter: EntityPredicate | undefined;
|
||||
type: 'content' | 'summary' | 'info' | undefined;
|
||||
};
|
||||
configInput: {
|
||||
hideIcons?: boolean | undefined;
|
||||
maxRelations?: number | undefined;
|
||||
} & {
|
||||
filter?: EntityPredicate | undefined;
|
||||
type?: 'content' | 'summary' | 'info' | undefined;
|
||||
};
|
||||
@@ -176,7 +182,17 @@ const _default: FrontendPlugin<
|
||||
optional: true;
|
||||
}
|
||||
>;
|
||||
inputs: {};
|
||||
inputs: {
|
||||
[x: string]: ExtensionInput<
|
||||
AnyExtensionDataRef,
|
||||
{
|
||||
optional: boolean;
|
||||
singleton: boolean;
|
||||
}
|
||||
>;
|
||||
};
|
||||
kind: 'entity-card';
|
||||
name: 'user-profile';
|
||||
params: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?: string | EntityPredicate | ((entity: Entity) => boolean);
|
||||
|
||||
@@ -42,6 +42,8 @@ export type EntityRelationAggregation = 'direct' | 'aggregated';
|
||||
export const EntityUserProfileCard: (props: {
|
||||
variant?: InfoCardVariants;
|
||||
showLinks?: boolean;
|
||||
maxRelations?: number;
|
||||
hideIcons?: boolean;
|
||||
}) => JSX_2.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -98,5 +100,7 @@ export const OwnershipCard: (props: {
|
||||
export const UserProfileCard: (props: {
|
||||
variant?: InfoCardVariants;
|
||||
showLinks?: boolean;
|
||||
maxRelations?: number;
|
||||
hideIcons?: boolean;
|
||||
}) => JSX_2.Element;
|
||||
```
|
||||
|
||||
@@ -59,14 +59,28 @@ const EntityOwnershipCard = EntityCardBlueprint.make({
|
||||
});
|
||||
|
||||
/** @alpha */
|
||||
const EntityUserProfileCard = EntityCardBlueprint.make({
|
||||
const EntityUserProfileCard = EntityCardBlueprint.makeWithOverrides({
|
||||
name: 'user-profile',
|
||||
params: {
|
||||
filter: 'kind:user',
|
||||
loader: async () =>
|
||||
import('./components/Cards/User/UserProfileCard/UserProfileCard').then(
|
||||
m => compatWrapper(<m.UserProfileCard />),
|
||||
),
|
||||
config: {
|
||||
schema: {
|
||||
maxRelations: z => z.number().optional(),
|
||||
hideIcons: z => z.boolean().default(false),
|
||||
},
|
||||
},
|
||||
factory(originalFactory, { config }) {
|
||||
return originalFactory({
|
||||
filter: 'kind:user',
|
||||
loader: async () =>
|
||||
import('./components/Cards/User/UserProfileCard/UserProfileCard').then(
|
||||
m =>
|
||||
compatWrapper(
|
||||
<m.UserProfileCard
|
||||
maxRelations={config.maxRelations}
|
||||
hideIcons={config.hideIcons}
|
||||
/>,
|
||||
),
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { UserProfileCard } from './UserProfileCard';
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('UserSummary Test', () => {
|
||||
const userEntity: UserEntity = {
|
||||
@@ -70,6 +71,100 @@ describe('UserSummary Test', () => {
|
||||
);
|
||||
expect(screen.getByText('Super awesome human')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('displayed user relations limit', () => {
|
||||
const entity = {
|
||||
...userEntity,
|
||||
relations: [
|
||||
{ type: 'memberOf', targetRef: 'group:default/team-a' },
|
||||
{ type: 'memberOf', targetRef: 'group:default/team-b' },
|
||||
{ type: 'memberOf', targetRef: 'group:default/team-c' },
|
||||
{ type: 'memberOf', targetRef: 'group:default/team-d' },
|
||||
{ type: 'memberOf', targetRef: 'group:default/team-e' },
|
||||
],
|
||||
};
|
||||
|
||||
it('Should limit the number of displayed groups in the card', async () => {
|
||||
await renderInTestApp(
|
||||
<EntityProvider entity={entity}>
|
||||
<UserProfileCard variant="gridItem" maxRelations={3} />
|
||||
</EntityProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// should show the first 3 groups
|
||||
expect(screen.getByText('team-a')).toBeInTheDocument();
|
||||
expect(screen.getByText('team-b')).toBeInTheDocument();
|
||||
expect(screen.getByText('team-c')).toBeInTheDocument();
|
||||
|
||||
// should not show the rest
|
||||
expect(screen.queryByText('team-d')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('team-e')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
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} />
|
||||
</EntityProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// clicking the button should open a dialog listing all groups
|
||||
await userEvent.click(screen.getByText('...More (2)'));
|
||||
|
||||
expect(screen.getByText('team-d')).toBeInTheDocument();
|
||||
expect(screen.getByText('team-e')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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} />
|
||||
</EntityProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(screen.getByText('team-a')).toBeInTheDocument();
|
||||
expect(screen.getByText('team-b')).toBeInTheDocument();
|
||||
expect(screen.getByText('team-c')).toBeInTheDocument();
|
||||
expect(screen.getByText('team-d')).toBeInTheDocument();
|
||||
expect(screen.getByText('team-e')).toBeInTheDocument();
|
||||
expect(screen.queryByText('...More (0)')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Hide all groups when max relations is equals to zero', async () => {
|
||||
await renderInTestApp(
|
||||
<EntityProvider entity={entity}>
|
||||
<UserProfileCard variant="gridItem" maxRelations={0} />
|
||||
</EntityProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(screen.queryByText('team-a')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('team-b')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('team-c')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('team-d')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('team-e')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('...More (5)')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edit Button', () => {
|
||||
|
||||
@@ -25,14 +25,22 @@ import {
|
||||
InfoCardVariants,
|
||||
Link,
|
||||
} from '@backstage/core-components';
|
||||
import { 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';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import {
|
||||
EntityRefLinks,
|
||||
getEntityRelations,
|
||||
@@ -46,6 +54,24 @@ import GroupIcon from '@material-ui/icons/Group';
|
||||
import { LinksGroup } from '../../Meta';
|
||||
import PersonIcon from '@material-ui/icons/Person';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
const useStyles = makeStyles((theme: any) => ({
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
right: theme.spacing(1),
|
||||
top: theme.spacing(1),
|
||||
color: theme.palette.grey[500],
|
||||
},
|
||||
moreButton: {
|
||||
display: 'contents',
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
dialogPaper: {
|
||||
minHeight: 400,
|
||||
},
|
||||
}));
|
||||
|
||||
const CardTitle = (props: { title?: string }) =>
|
||||
props.title ? (
|
||||
<Box display="flex" alignItems="center">
|
||||
@@ -58,8 +84,23 @@ const CardTitle = (props: { title?: string }) =>
|
||||
export const UserProfileCard = (props: {
|
||||
variant?: InfoCardVariants;
|
||||
showLinks?: boolean;
|
||||
maxRelations?: number;
|
||||
hideIcons?: boolean;
|
||||
}) => {
|
||||
const { maxRelations, hideIcons } = props;
|
||||
|
||||
const classes = useStyles();
|
||||
const { entity: user } = useEntity<UserEntity>();
|
||||
const [isAllGroupsDialogOpen, setIsAllGroupsDialogOpen] = useState(false);
|
||||
|
||||
const toggleAllGroupsDialog = useCallback(
|
||||
() =>
|
||||
setIsAllGroupsDialogOpen(
|
||||
prevIsViewAllGroupsDialogOpen => !prevIsViewAllGroupsDialogOpen,
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
return <Alert severity="error">User not found</Alert>;
|
||||
}
|
||||
@@ -117,24 +158,67 @@ export const UserProfileCard = (props: {
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Tooltip title="Member of">
|
||||
<GroupIcon />
|
||||
</Tooltip>
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
<EntityRefLinks
|
||||
entityRefs={memberOfRelations}
|
||||
defaultKind="Group"
|
||||
/>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
{maxRelations === undefined || maxRelations > 0 ? (
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Tooltip title="Member of">
|
||||
<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
|
||||
>
|
||||
{` ...More (${
|
||||
memberOfRelations.length - maxRelations
|
||||
})`}
|
||||
</BaseButton>
|
||||
</>
|
||||
) : null}
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
) : null}
|
||||
{props?.showLinks && <LinksGroup links={links} />}
|
||||
</List>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Dialog
|
||||
classes={{ paper: classes.dialogPaper }}
|
||||
open={isAllGroupsDialogOpen}
|
||||
onClose={toggleAllGroupsDialog}
|
||||
scroll="paper"
|
||||
aria-labelledby="view-all-groups-dialog-title"
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle id="view-all-groups-dialog-title">
|
||||
All {user.metadata.name}'s groups:
|
||||
<IconButton
|
||||
className={classes.closeButton}
|
||||
aria-label="close"
|
||||
onClick={toggleAllGroupsDialog}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<EntityRefLinks entityRefs={memberOfRelations} defaultKind="Group" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={toggleAllGroupsDialog}>Close</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user