Add aggregate members toggle to show aggregate members on groups

Signed-off-by: Robert Bunning <rbunning@webstaurantstore.com>
This commit is contained in:
Robert Bunning
2023-04-24 16:09:38 -04:00
parent 4c30c02945
commit 6e387c077a
8 changed files with 785 additions and 5 deletions
+4 -1
View File
@@ -28,6 +28,7 @@
"clean": "backstage-cli package clean"
},
"dependencies": {
"@backstage/catalog-client": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
@@ -47,11 +48,13 @@
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"devDependencies": {
"@backstage/catalog-client": "workspace:^",
"@backstage/cli": "workspace:^",
"@backstage/core-app-api": "workspace:^",
"@backstage/dev-utils": "workspace:^",
"@backstage/plugin-catalog": "workspace:^",
"@backstage/plugin-permission-react": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@backstage/types": "workspace:^",
"@testing-library/dom": "^8.0.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Backstage Authors
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,14 +19,33 @@ import {
CatalogApi,
catalogApiRef,
EntityProvider,
StarredEntitiesApi,
starredEntitiesApiRef,
} from '@backstage/plugin-catalog-react';
import {
renderInTestApp,
renderWithEffects,
TestApiProvider,
wrapInTestApp,
} from '@backstage/test-utils';
import React from 'react';
import { MembersListCard } from './MembersListCard';
import {
groupA,
mockedCatalogApiSupportingGroups,
mockedGetEntityRelations,
} from '../../../../test-helpers/catalogMocks';
import { permissionApiRef } from '@backstage/plugin-permission-react';
import { EntityLayout } from '@backstage/plugin-catalog';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Observable } from '@backstage/types';
jest.mock('@backstage/plugin-catalog-react', () => ({
...jest.requireActual('@backstage/plugin-catalog-react'),
getEntityRelations: (entity: Entity | undefined, relationType: string) =>
mockedGetEntityRelations(entity, relationType),
}));
// Mock needed because jsdom doesn't correctly implement box-sizing
// https://github.com/ShinyChang/React-Text-Truncate/issues/70
@@ -41,6 +60,20 @@ jest.mock('react-text-truncate', () => {
};
});
const mockedStarredEntitiesApi: Partial<StarredEntitiesApi> = {
starredEntitie$: () => {
return {
subscribe: () => {
return {
unsubscribe() {
// This is intentional
},
};
},
} as Observable<Set<string>>;
},
};
describe('MemberTab Test', () => {
const groupEntity: GroupEntity = {
apiVersion: 'backstage.io/v1alpha1',
@@ -131,4 +164,88 @@ describe('MemberTab Test', () => {
expect(rendered.getByText('Testers (1)')).toBeInTheDocument();
});
describe('Aggregate members toggle', () => {
it('Shows only direct members if the aggregate users switch is turned off', async () => {
await renderInTestApp(
<TestApiProvider
apis={[
[catalogApiRef, mockedCatalogApiSupportingGroups],
[starredEntitiesApiRef, mockedStarredEntitiesApi],
[permissionApiRef, {}],
]}
>
<EntityProvider entity={groupA}>
<EntityLayout>
<EntityLayout.Route path="/" title="Title">
<MembersListCard />
</EntityLayout.Route>
</EntityLayout>
</EntityProvider>
</TestApiProvider>,
);
const displayedMemberNames = screen.queryAllByTestId('user-link');
const duplicatedUserText = screen.getByText('Duplicated User');
const groupAUserOneText = screen.getByText('Group A User One');
expect(displayedMemberNames).toHaveLength(2);
expect(duplicatedUserText).toBeInTheDocument();
expect(groupAUserOneText).toBeInTheDocument();
expect(
duplicatedUserText.compareDocumentPosition(groupAUserOneText),
).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
});
it('Shows all descendant members of the group when the aggregate users switch is turned on, showing duplicated members only once', async () => {
await renderInTestApp(
<TestApiProvider
apis={[
[catalogApiRef, mockedCatalogApiSupportingGroups],
[starredEntitiesApiRef, mockedStarredEntitiesApi],
[permissionApiRef, {}],
]}
>
<EntityProvider entity={groupA}>
<EntityLayout>
<EntityLayout.Route path="/" title="Title">
<MembersListCard />
</EntityLayout.Route>
</EntityLayout>
</EntityProvider>
</TestApiProvider>,
);
// Click the toggle switch
await userEvent.click(screen.getByRole('checkbox'));
const displayedMemberNames = screen.queryAllByTestId('user-link');
const duplicatedUserText = screen.getByText('Duplicated User');
const groupAUserOneText = screen.getByText('Group A User One');
const groupBUserOneText = screen.getByText('Group B User One');
const groupDUserOneText = screen.getByText('Group D User One');
const groupEUserOneText = screen.getByText('Group E User One');
expect(displayedMemberNames).toHaveLength(5);
expect(duplicatedUserText).toBeInTheDocument();
expect(groupAUserOneText).toBeInTheDocument();
expect(groupBUserOneText).toBeInTheDocument();
expect(groupDUserOneText).toBeInTheDocument();
expect(groupEUserOneText).toBeInTheDocument();
expect(
duplicatedUserText.compareDocumentPosition(groupAUserOneText),
).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
expect(groupAUserOneText.compareDocumentPosition(groupBUserOneText)).toBe(
Node.DOCUMENT_POSITION_FOLLOWING,
);
expect(groupBUserOneText.compareDocumentPosition(groupDUserOneText)).toBe(
Node.DOCUMENT_POSITION_FOLLOWING,
);
expect(groupDUserOneText.compareDocumentPosition(groupEUserOneText)).toBe(
Node.DOCUMENT_POSITION_FOLLOWING,
);
});
});
});
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Backstage Authors
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -30,11 +30,12 @@ import {
createStyles,
Grid,
makeStyles,
Switch,
Theme,
Typography,
} from '@material-ui/core';
import Pagination from '@material-ui/lab/Pagination';
import React from 'react';
import React, { useState } from 'react';
import { generatePath } from 'react-router-dom';
import useAsync from 'react-use/lib/useAsync';
@@ -47,6 +48,10 @@ import {
OverflowTooltip,
} from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import {
getAllDesendantMembersForGroupEntity,
removeDuplicateEntitiesFrom,
} from '../../../../helpers/helpers';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -98,6 +103,7 @@ const MemberComponent = (props: { member: UserEntity }) => {
>
<Typography variant="h6">
<Link
data-testid="user-link"
to={generatePath(
`/catalog/:namespace/user/${metaName}`,
entityRouteParams(props.member),
@@ -144,10 +150,17 @@ export const MembersListCard = (props: {
setPage(pageIndex);
};
const [showAggregateUsers, setShowAggregateUsers] = useState(false);
const { value: descendantMembers } = useAsync(
async () =>
await getAllDesendantMembersForGroupEntity(groupEntity, catalogApi),
[catalogApi, groupEntity],
);
const {
loading,
error,
value: members,
value: directMembers,
} = useAsync(async () => {
const membersList = await catalogApi.getEntities({
filter: {
@@ -165,6 +178,18 @@ export const MembersListCard = (props: {
return membersList.items as UserEntity[];
}, [catalogApi, groupEntity]);
const members = removeDuplicateEntitiesFrom(
[
...(directMembers ?? []),
...(descendantMembers && showAggregateUsers ? descendantMembers : []),
].sort((a, b) => {
const nameToCompareInA = a.spec.profile?.displayName ?? a.metadata.name;
const nameToCompareInB = b.spec.profile?.displayName ?? b.metadata.name;
return nameToCompareInA.localeCompare(nameToCompareInB);
}),
) as UserEntity[];
if (loading) {
return <Progress />;
} else if (error) {
@@ -193,6 +218,16 @@ export const MembersListCard = (props: {
subheader={`of ${displayName}`}
{...(nbPages <= 1 ? {} : { actions: pagination })}
>
Direct Members
<Switch
color="primary"
checked={showAggregateUsers}
onChange={() => {
setShowAggregateUsers(!showAggregateUsers);
}}
inputProps={{ 'aria-label': 'Users Type Switch' }}
/>
Aggregated Members
<Grid container spacing={3}>
{members && members.length > 0 ? (
members
+143
View File
@@ -0,0 +1,143 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CatalogApi } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import {
getAllDesendantMembersForGroupEntity,
getMembersFromGroups,
getDescendantGroupsFromGroup,
removeDuplicateEntitiesFrom,
} from './helpers';
import {
groupA,
groupARef,
groupB,
groupBRef,
groupC,
groupCRef,
groupD,
groupDRef,
groupE,
groupERef,
groupAUserOne,
groupBUserOne,
groupDUserOne,
groupEUserOne,
duplicatedUser,
mockedCatalogApiSupportingGroups,
mockedGetEntityRelations,
} from '../test-helpers/catalogMocks';
jest.mock('@backstage/plugin-catalog-react', () => ({
...jest.requireActual('@backstage/plugin-catalog-react'),
getEntityRelations: (entity: Entity | undefined, relationType: string) =>
mockedGetEntityRelations(entity, relationType),
}));
describe('Helper functions', () => {
it('getAllDesendantMembersForGroupEntity correctly recursively returns all descendant members', async () => {
const catalogApi = mockedCatalogApiSupportingGroups as CatalogApi;
const actualGroupADescendantMembers =
await getAllDesendantMembersForGroupEntity(groupA, catalogApi);
const actualGroupCDescendantMembers =
await getAllDesendantMembersForGroupEntity(groupC, catalogApi);
expect(actualGroupADescendantMembers).toStrictEqual([
groupBUserOne,
groupDUserOne,
duplicatedUser,
groupEUserOne,
duplicatedUser,
]);
expect(actualGroupCDescendantMembers).toStrictEqual([]);
});
it('getMembersFromGroups correctly returns all members of provided groups', async () => {
const catalogApi = mockedCatalogApiSupportingGroups as CatalogApi;
const actualNoGroupsMembers = await getMembersFromGroups([], catalogApi);
const actualGroupAMembers = await getMembersFromGroups(
[groupARef],
catalogApi,
);
const actualGroupCMembers = await getMembersFromGroups(
[groupCRef],
catalogApi,
);
const actualMultipleGroupMembers = await getMembersFromGroups(
[groupARef, groupBRef, groupCRef],
catalogApi,
);
expect(actualNoGroupsMembers).toStrictEqual([]);
expect(actualGroupAMembers).toStrictEqual([groupAUserOne, duplicatedUser]);
expect(actualGroupCMembers).toStrictEqual([]);
expect(actualMultipleGroupMembers).toStrictEqual([
groupAUserOne,
duplicatedUser,
groupBUserOne,
]);
});
it('removeDuplicateEntitiesFrom correctly removes duplicate entities', () => {
const actualNoDuplicatedEntitiesResult = removeDuplicateEntitiesFrom([
groupA,
groupB,
groupC,
groupD,
]);
const actualDuplicatedEntitiesResult = removeDuplicateEntitiesFrom([
groupA,
groupB,
groupB,
groupC,
groupC,
groupD,
groupE,
]);
const actualNoEntitiesProvidedResult = removeDuplicateEntitiesFrom([]);
expect(actualNoDuplicatedEntitiesResult).toStrictEqual([
groupA,
groupB,
groupC,
groupD,
]);
expect(actualDuplicatedEntitiesResult).toStrictEqual([
groupA,
groupB,
groupC,
groupD,
groupE,
]);
expect(actualNoEntitiesProvidedResult).toStrictEqual([]);
});
it('getDescendantGroupsFromGroup correctly recursively returns descendant groups, ignoring duplicates', async () => {
const actualDescendantGroups = await getDescendantGroupsFromGroup(
groupA,
mockedCatalogApiSupportingGroups as CatalogApi,
);
expect(actualDescendantGroups).toStrictEqual([
groupBRef,
groupCRef,
groupDRef,
groupERef,
]);
});
});
+117
View File
@@ -0,0 +1,117 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
CompoundEntityRef,
DEFAULT_NAMESPACE,
Entity,
GroupEntity,
RELATION_PARENT_OF,
stringifyEntityRef,
UserEntity,
} from '@backstage/catalog-model';
import {
CatalogApi,
getEntityRelations,
} from '@backstage/plugin-catalog-react';
export const getMembersFromGroups = async (
groups: CompoundEntityRef[],
catalogApi: CatalogApi,
) => {
const membersList =
groups.length === 0
? { items: [] }
: 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'),
}),
),
},
});
return membersList.items as UserEntity[];
};
export const getDescendantGroupsFromGroup = async (
group: GroupEntity,
catalogApi: CatalogApi,
) => {
const alreadyQueuedOrExpandedGroupNames = new Map<string, boolean>();
const groupRef: CompoundEntityRef = {
kind: group.kind,
namespace: group.metadata.namespace ?? DEFAULT_NAMESPACE,
name: group.metadata.name,
};
const groupQueue = [groupRef];
const resultantGroupRefs: CompoundEntityRef[] = [];
// Continue expanding groups until there are no more
while (groupQueue.length > 0) {
const activeGroupRef = groupQueue.shift() as CompoundEntityRef;
const activeGroup = await catalogApi.getEntityByRef(activeGroupRef);
alreadyQueuedOrExpandedGroupNames.set(
stringifyEntityRef(activeGroupRef),
true,
);
const childGroups = getEntityRelations(activeGroup, RELATION_PARENT_OF, {
kind: 'Group',
}).filter(
currentGroup =>
!alreadyQueuedOrExpandedGroupNames.has(
stringifyEntityRef(currentGroup),
),
);
childGroups.forEach(childGroup =>
alreadyQueuedOrExpandedGroupNames.set(
stringifyEntityRef(childGroup),
true,
),
);
groupQueue.push(...childGroups);
resultantGroupRefs.push(...childGroups);
}
return resultantGroupRefs;
};
export const getAllDesendantMembersForGroupEntity = async (
groupEntity: GroupEntity,
catalogApi: CatalogApi,
) =>
getMembersFromGroups(
await getDescendantGroupsFromGroup(groupEntity, catalogApi),
catalogApi,
);
export const removeDuplicateEntitiesFrom = (entityArray: Entity[]) => {
const seenEntities = new Map<string, boolean>();
return entityArray.filter(entity => {
const stringifiedEntity = stringifyEntityRef(entity);
const isDuplicate = seenEntities.has(stringifiedEntity);
seenEntities.set(stringifiedEntity, true);
return !isDuplicate;
});
};
@@ -0,0 +1,357 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
CatalogApi,
GetEntitiesByRefsRequest,
GetEntitiesRequest,
} from '@backstage/catalog-client';
import {
CompoundEntityRef,
Entity,
GroupEntity,
stringifyEntityRef,
} from '@backstage/catalog-model';
export const groupA: GroupEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
name: 'group-a',
title: 'Group A',
description: 'Testing Group A',
},
spec: {
type: 'testing-group',
children: [],
},
};
export const groupAWithALeader: GroupEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
name: 'group-a',
title: 'Group A',
description: 'Testing Group A',
},
spec: {
type: 'testing-group',
children: [],
},
relations: [
{
type: 'teamLeadBy',
targetRef: 'user:default/group-a-user-one',
},
],
};
export const groupARef: CompoundEntityRef = {
kind: 'Group',
name: 'group-a',
namespace: 'default',
};
export const groupB: GroupEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
name: 'group-b',
title: 'Group B',
description: 'Testing Group B',
},
spec: {
type: 'testing-group',
children: [],
},
};
export const groupBRef: CompoundEntityRef = {
kind: 'Group',
name: 'group-b',
namespace: 'default',
};
export const groupC: GroupEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
name: 'group-c',
title: 'Group C',
description: 'Testing Group C',
},
spec: {
type: 'testing-group',
children: [],
},
};
export const groupCRef: CompoundEntityRef = {
kind: 'Group',
name: 'group-c',
namespace: 'default',
};
export const groupD: GroupEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
name: 'group-d',
title: 'Group D',
description: 'Testing Group D',
},
spec: {
type: 'testing-group',
children: [],
},
};
export const groupDRef: CompoundEntityRef = {
kind: 'Group',
name: 'group-d',
namespace: 'default',
};
export const groupE: GroupEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
name: 'group-e',
title: 'Group E',
description: 'Testing Group E',
},
spec: {
type: 'testing-group',
children: [],
},
};
export const groupERef: CompoundEntityRef = {
kind: 'Group',
name: 'group-e',
namespace: 'default',
};
export const groupF: GroupEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
name: 'group-f',
title: 'Group F',
description: 'Testing Group F',
},
spec: {
type: 'testing-group',
children: [],
},
};
interface MockedRelationsType {
[index: string]: CompoundEntityRef[];
ownedBy: CompoundEntityRef[];
parentOf: CompoundEntityRef[];
}
export const groupAUserOne: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: {
name: 'group-a-user-one',
},
spec: {
profile: {
displayName: 'Group A User One',
email: 'group-a-user-one@testing.email',
picture:
'https://avatars.dicebear.com/api/avataaars/breanna-davison@example.com.svg',
},
},
};
export const groupBUserOne: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: {
name: 'group-b-user-one',
},
spec: {
profile: {
displayName: 'Group B User One',
email: 'group-b-user-one@testing.email',
picture:
'https://avatars.dicebear.com/api/avataaars/breanna-davison@example.com.svg',
},
},
};
export const groupDUserOne: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: {
name: 'group-d-user-one',
},
spec: {
profile: {
displayName: 'Group D User One',
email: 'group-d-user-one@testing.email',
picture:
'https://avatars.dicebear.com/api/avataaars/breanna-davison@example.com.svg',
},
},
};
export const groupEUserOne: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: {
name: 'group-e-user-one',
},
spec: {
profile: {
displayName: 'Group E User One',
email: 'group-e-user-one@testing.email',
picture:
'https://avatars.dicebear.com/api/avataaars/breanna-davison@example.com.svg',
},
},
};
export const groupFUserOne: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: {
name: 'group-f-user-one',
},
spec: {
profile: {
displayName: 'Group F User One',
email: 'group-f-user-one@testing.email',
picture:
'https://avatars.dicebear.com/api/avataaars/breanna-davison@example.com.svg',
},
},
};
export const duplicatedUser: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: {
name: 'duplicated-user',
},
spec: {
profile: {
displayName: 'Duplicated User',
email: 'duplicated-user@testing.email',
picture:
'https://avatars.dicebear.com/api/avataaars/breanna-davison@example.com.svg',
},
},
};
const mockedRelationsCatalog = new Map<string, MockedRelationsType>([
[
'group-a',
{
ownedBy: [],
parentOf: [groupBRef, groupCRef],
},
],
[
'group-b',
{
ownedBy: [],
parentOf: [groupARef, groupDRef],
},
],
[
'group-c',
{
ownedBy: [],
parentOf: [],
},
],
[
'group-d',
{
ownedBy: [],
parentOf: [groupCRef, groupERef],
},
],
[
'group-e',
{
ownedBy: [],
parentOf: [],
},
],
]);
const mockedRefsToRelationsMap = new Map<string, GroupEntity>([
['group:default/group-a', groupA],
['group:default/group-b', groupB],
['group:default/group-c', groupC],
['group:default/group-d', groupD],
['group:default/group-e', groupE],
['group:default/group-f', groupF],
]);
const mockedMembersMapping = new Map<string, Entity[]>([
['group:default/group-a', [groupAUserOne, duplicatedUser]],
['group:default/group-b', [groupBUserOne]],
['group:default/group-c', []],
['group:default/group-d', [groupDUserOne, duplicatedUser]],
['group:default/group-e', [groupEUserOne, duplicatedUser]],
['group:default/group-f', [groupFUserOne]],
]);
type Nullable<T> = T | undefined;
const nullMockedRelationsType: MockedRelationsType = {
ownedBy: [],
parentOf: [],
};
export const mockedCatalogApiSupportingGroups: Partial<CatalogApi> = {
getEntities: async (request?: GetEntitiesRequest) => {
const actualFilter = (request?.filter as Nullable<{
'relations.memberof': string[];
}>) ?? { 'relations.memberof': [] };
const items =
actualFilter['relations.memberof'].length > 0
? (actualFilter['relations.memberof']
.map(group => mockedMembersMapping.get(group))
.flat()
.filter(entity => !!entity) as Entity[])
: [...mockedMembersMapping.values()].flat();
return { items };
},
getEntityByRef: async (entityRef: CompoundEntityRef) =>
mockedRefsToRelationsMap.get(stringifyEntityRef(entityRef)),
getEntitiesByRefs: async (request: GetEntitiesByRefsRequest) => {
const items = request.entityRefs.map(entityRef =>
mockedRefsToRelationsMap.get(entityRef),
);
return { items };
},
};
export const mockedGetEntityRelations = (
entity: Nullable<Entity>,
relationType: string,
) =>
(mockedRelationsCatalog.get(entity?.metadata.name ?? 'NULL') ??
nullMockedRelationsType)[relationType];