Merge pull request #24970 from brianphillips/custom-member-relationship

Add relationType option to EntityMembersListCard component
This commit is contained in:
Fredrik Adelöw
2024-06-24 13:29:17 +02:00
committed by GitHub
10 changed files with 101 additions and 35 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-org': patch
---
Added `relationType` property to EntityMembersListCard component that allows for display users related to a group via some other relationship aside from `memberOf`.
Also, as a side effect, the `relationsType` property has been deprecated in favor of a more accurately named `relationAggregation` property.
+6
View File
@@ -23,7 +23,9 @@ export const EntityMembersListCard: (props: {
memberDisplayTitle?: string | undefined;
pageSize?: number | undefined;
showAggregateMembersToggle?: boolean | undefined;
relationType?: string | undefined;
relationsType?: EntityRelationAggregation | undefined;
relationAggregation?: EntityRelationAggregation | undefined;
}) => JSX_2.Element;
// @public (undocumented)
@@ -32,6 +34,7 @@ export const EntityOwnershipCard: (props: {
entityFilterKind?: string[] | undefined;
hideRelationsToggle?: boolean | undefined;
relationsType?: EntityRelationAggregation | undefined;
relationAggregation?: EntityRelationAggregation | undefined;
entityLimit?: number | undefined;
}) => JSX_2.Element;
@@ -55,7 +58,9 @@ export const MembersListCard: (props: {
memberDisplayTitle?: string;
pageSize?: number;
showAggregateMembersToggle?: boolean;
relationType?: string;
relationsType?: EntityRelationAggregation;
relationAggregation?: EntityRelationAggregation;
}) => React_2.JSX.Element;
// @public
@@ -82,6 +87,7 @@ export const OwnershipCard: (props: {
entityFilterKind?: string[];
hideRelationsToggle?: boolean;
relationsType?: EntityRelationAggregation;
relationAggregation?: EntityRelationAggregation;
entityLimit?: number;
}) => React_2.JSX.Element;
@@ -99,6 +99,7 @@ describe('MemberTab Test', () => {
] as Entity[],
}),
};
const getEntitiesSpy = jest.spyOn(catalogApi, 'getEntities');
it('Display Profile Card', async () => {
await renderInTestApp(
@@ -115,6 +116,12 @@ describe('MemberTab Test', () => {
},
},
);
expect(getEntitiesSpy).toHaveBeenCalledWith({
filter: {
kind: 'User',
'relations.memberof': ['group:default/team-d'],
},
});
expect(screen.getByAltText('Tara MacGovern')).toHaveAttribute(
'src',
@@ -149,6 +156,29 @@ describe('MemberTab Test', () => {
expect(screen.getByText('Testers (1)')).toBeInTheDocument();
});
it('Can query a different relationship', async () => {
await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<EntityProvider entity={groupEntity}>
<MembersListCard relationType="leaderOf" />
</EntityProvider>
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
'/catalog': rootRouteRef,
},
},
);
expect(getEntitiesSpy).toHaveBeenCalledWith({
filter: {
kind: 'User',
'relations.leaderof': ['group:default/team-d'],
},
});
});
describe('Aggregate members toggle', () => {
it('Does not show the aggregate members toggle if the showAggregateMembersToggle prop is undefined', async () => {
await renderInTestApp(
@@ -346,7 +376,7 @@ describe('MemberTab Test', () => {
<EntityLayout.Route path="/" title="Title">
<MembersListCard
showAggregateMembersToggle
relationsType="aggregated"
relationAggregation="aggregated"
/>
</EntityLayout.Route>
</EntityLayout>
@@ -384,7 +414,7 @@ describe('MemberTab Test', () => {
<EntityProvider entity={groupA}>
<EntityLayout>
<EntityLayout.Route path="/" title="Title">
<MembersListCard relationsType="aggregated" />
<MembersListCard relationAggregation="aggregated" />
</EntityLayout.Route>
</EntityLayout>
</EntityProvider>
@@ -138,14 +138,19 @@ export const MembersListCard = (props: {
memberDisplayTitle?: string;
pageSize?: number;
showAggregateMembersToggle?: boolean;
relationType?: string;
/** @deprecated Please use `relationAggregation` instead */
relationsType?: EntityRelationAggregation;
relationAggregation?: EntityRelationAggregation;
}) => {
const {
memberDisplayTitle = 'Members',
pageSize = 50,
showAggregateMembersToggle,
relationsType = 'direct',
relationType = 'memberof',
} = props;
const relationAggregation =
props.relationAggregation ?? props.relationsType ?? 'direct';
const classes = useListStyles();
const { entity: groupEntity } = useEntity<GroupEntity>();
@@ -165,7 +170,7 @@ export const MembersListCard = (props: {
};
const [showAggregateMembers, setShowAggregateMembers] = useState(
relationsType === 'aggregated',
relationAggregation === 'aggregated',
);
const { loading: loadingDescendantMembers, value: descendantMembers } =
@@ -177,6 +182,7 @@ export const MembersListCard = (props: {
return await getAllDesendantMembersForGroupEntity(
groupEntity,
catalogApi,
relationType,
);
}, [catalogApi, groupEntity, showAggregateMembers]);
const {
@@ -187,7 +193,7 @@ export const MembersListCard = (props: {
const membersList = await catalogApi.getEntities({
filter: {
kind: 'User',
'relations.memberof': [
[`relations.${relationType.toLocaleLowerCase('en-US')}`]: [
stringifyEntityRef({
kind: 'group',
namespace: groupNamespace.toLocaleLowerCase('en-US'),
@@ -114,19 +114,27 @@ export const ComponentsGrid = ({
className,
entity,
relationsType,
relationAggregation,
entityFilterKind,
entityLimit = 6,
}: {
className?: string;
entity: Entity;
relationsType: EntityRelationAggregation;
/** @deprecated Please use relationAggregation instead */
relationsType?: EntityRelationAggregation;
relationAggregation?: EntityRelationAggregation;
entityFilterKind?: string[];
entityLimit?: number;
}) => {
const catalogLink = useRouteRef(catalogIndexRouteRef);
if (!relationsType && !relationAggregation) {
throw new Error(
'The relationAggregation property must be set as an EntityRelationAggregation type.',
);
}
const { componentsWithCounters, loading, error } = useGetEntities(
entity,
relationsType,
(relationAggregation ?? relationsType)!, // we can safely use the non-null assertion here because of the run-time check above
entityFilterKind,
entityLimit,
);
@@ -288,7 +288,7 @@ describe('OwnershipCard', () => {
const { getByText } = await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<EntityProvider entity={userEntity}>
<OwnershipCard relationsType="aggregated" />
<OwnershipCard relationAggregation="aggregated" />
</EntityProvider>
</TestApiProvider>,
{
@@ -67,31 +67,34 @@ export const OwnershipCard = (props: {
variant?: InfoCardVariants;
entityFilterKind?: string[];
hideRelationsToggle?: boolean;
/** @deprecated Please use relationAggregation instead */
relationsType?: EntityRelationAggregation;
relationAggregation?: EntityRelationAggregation;
entityLimit?: number;
}) => {
const {
variant,
entityFilterKind,
hideRelationsToggle,
relationsType,
entityLimit = 6,
} = props;
const relationAggregation = props.relationAggregation ?? props.relationsType;
const relationsToggle =
hideRelationsToggle === undefined ? false : hideRelationsToggle;
const classes = useStyles();
const { entity } = useEntity();
const defaultRelationsType = entity.kind === 'User' ? 'aggregated' : 'direct';
const [getRelationsType, setRelationsType] = useState(
relationsType ?? defaultRelationsType,
const defaultRelationAggregation =
entity.kind === 'User' ? 'aggregated' : 'direct';
const [getRelationAggregation, setRelationAggregation] = useState(
relationAggregation ?? defaultRelationAggregation,
);
useEffect(() => {
if (!relationsType) {
setRelationsType(defaultRelationsType);
if (!relationAggregation) {
setRelationAggregation(defaultRelationAggregation);
}
}, [setRelationsType, defaultRelationsType, relationsType]);
}, [setRelationAggregation, defaultRelationAggregation, relationAggregation]);
return (
<InfoCard
@@ -112,16 +115,18 @@ export const OwnershipCard = (props: {
placement="top"
arrow
title={`${
getRelationsType === 'direct' ? 'Direct' : 'Aggregated'
getRelationAggregation === 'direct' ? 'Direct' : 'Aggregated'
} Relations`}
>
<Switch
color="primary"
checked={getRelationsType !== 'direct'}
checked={getRelationAggregation !== 'direct'}
onChange={() => {
const updatedRelationsType =
getRelationsType === 'direct' ? 'aggregated' : 'direct';
setRelationsType(updatedRelationsType);
const updatedRelationAggregation =
getRelationAggregation === 'direct'
? 'aggregated'
: 'direct';
setRelationAggregation(updatedRelationAggregation);
}}
name="pin"
inputProps={{ 'aria-label': 'Ownership Type Switch' }}
@@ -136,7 +141,7 @@ export const OwnershipCard = (props: {
className={classes.grid}
entity={entity}
entityLimit={entityLimit}
relationsType={getRelationsType}
relationAggregation={getRelationAggregation}
entityFilterKind={entityFilterKind}
/>
</InfoCard>
@@ -64,7 +64,7 @@ describe('useGetEntities', () => {
]),
});
describe('given aggregated relationsType', () => {
describe('given aggregated relationAggregation', () => {
const whenHookIsCalledWith = async (_entity: Entity) => {
const { result } = renderHook(
({ entity }) => useGetEntities(entity, 'aggregated'),
@@ -205,7 +205,7 @@ describe('useGetEntities', () => {
});
});
describe('given direct relationsType', () => {
describe('given direct relationAggregation', () => {
const whenHookIsCalledWith = async (_entity: Entity) => {
const { result } = renderHook(
({ entity }) => useGetEntities(entity, 'direct'),
@@ -125,11 +125,11 @@ const getChildOwnershipEntityRefs = async (
const getOwners = async (
entity: Entity,
relations: EntityRelationAggregation,
relationAggregation: EntityRelationAggregation,
catalogApi: CatalogApi,
): Promise<string[]> => {
const isGroup = entity.kind === 'Group';
const isAggregated = relations === 'aggregated';
const isAggregated = relationAggregation === 'aggregated';
const isUserEntity = entity.kind === 'User';
if (isAggregated && isGroup) {
@@ -166,7 +166,7 @@ const getOwnedEntitiesByOwners = (
export function useGetEntities(
entity: Entity,
relations: EntityRelationAggregation,
relationAggregation: EntityRelationAggregation,
entityFilterKind?: string[],
entityLimit = 6,
): {
@@ -189,7 +189,7 @@ export function useGetEntities(
error,
value: componentsWithCounters,
} = useAsync(async () => {
const owners = await getOwners(entity, relations, catalogApi);
const owners = await getOwners(entity, relationAggregation, catalogApi);
const ownedEntitiesList = await getOwnedEntitiesByOwners(
owners,
@@ -230,7 +230,7 @@ export function useGetEntities(
kind: string;
queryParams: string;
}>;
}, [catalogApi, entity, relations]);
}, [catalogApi, entity, relationAggregation]);
return {
componentsWithCounters,
+11 -7
View File
@@ -31,6 +31,7 @@ import {
export const getMembersFromGroups = async (
groups: CompoundEntityRef[],
catalogApi: CatalogApi,
relationship = 'memberof',
) => {
const membersList =
groups.length === 0
@@ -38,13 +39,14 @@ export const getMembersFromGroups = async (
: await catalogApi.getEntities({
filter: {
kind: 'User',
'relations.memberof': groups.map(group =>
stringifyEntityRef({
kind: 'group',
namespace: group.namespace.toLocaleLowerCase('en-US'),
name: group.name.toLocaleLowerCase('en-US'),
}),
),
[`relations.${relationship.toLocaleLowerCase('en-US')}`]:
groups.map(group =>
stringifyEntityRef({
kind: 'group',
namespace: group.namespace.toLocaleLowerCase('en-US'),
name: group.name.toLocaleLowerCase('en-US'),
}),
),
},
});
@@ -99,10 +101,12 @@ export const getDescendantGroupsFromGroup = async (
export const getAllDesendantMembersForGroupEntity = async (
groupEntity: GroupEntity,
catalogApi: CatalogApi,
relationship = 'memberof',
) =>
getMembersFromGroups(
await getDescendantGroupsFromGroup(groupEntity, catalogApi),
catalogApi,
relationship,
);
export const removeDuplicateEntitiesFrom = (entityArray: Entity[]) => {