diff --git a/.changeset/new-roses-fail.md b/.changeset/new-roses-fail.md new file mode 100644 index 0000000000..7c92775d7b --- /dev/null +++ b/.changeset/new-roses-fail.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org': patch +--- + +Changed the MembersListCard component to allow displaying aggregated members when viewing a group. Now, a toggle switch is displayed that lets you switch between showing direct members and aggregated members. diff --git a/plugins/org/package.json b/plugins/org/package.json index a5b3308922..08d8f4bf9a 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -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", diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx index cc524b60a2..4ef5939d33 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx @@ -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 = { + starredEntitie$: () => { + return { + subscribe: () => { + return { + unsubscribe() { + // This is intentional + }, + }; + }, + } as Observable>; + }, +}; + 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( + + + + + + + + + , + ); + + 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( + + + + + + + + + , + ); + + // 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, + ); + }); + }); }); diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx index eb61989144..4e9be6d6ad 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx @@ -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 }) => { > + 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 ; } else if (error) { @@ -193,6 +218,16 @@ export const MembersListCard = (props: { subheader={`of ${displayName}`} {...(nbPages <= 1 ? {} : { actions: pagination })} > + Direct Members + { + setShowAggregateUsers(!showAggregateUsers); + }} + inputProps={{ 'aria-label': 'Users Type Switch' }} + /> + Aggregated Members {members && members.length > 0 ? ( members diff --git a/plugins/org/src/helpers/helpers.test.ts b/plugins/org/src/helpers/helpers.test.ts new file mode 100644 index 0000000000..b80be83a8b --- /dev/null +++ b/plugins/org/src/helpers/helpers.test.ts @@ -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, + ]); + }); +}); diff --git a/plugins/org/src/helpers/helpers.ts b/plugins/org/src/helpers/helpers.ts new file mode 100644 index 0000000000..e0e83bec67 --- /dev/null +++ b/plugins/org/src/helpers/helpers.ts @@ -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(); + 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(); + return entityArray.filter(entity => { + const stringifiedEntity = stringifyEntityRef(entity); + const isDuplicate = seenEntities.has(stringifiedEntity); + + seenEntities.set(stringifiedEntity, true); + return !isDuplicate; + }); +}; diff --git a/plugins/org/src/test-helpers/catalogMocks.ts b/plugins/org/src/test-helpers/catalogMocks.ts new file mode 100644 index 0000000000..e979247b94 --- /dev/null +++ b/plugins/org/src/test-helpers/catalogMocks.ts @@ -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([ + [ + '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([ + ['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([ + ['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 | undefined; +const nullMockedRelationsType: MockedRelationsType = { + ownedBy: [], + parentOf: [], +}; + +export const mockedCatalogApiSupportingGroups: Partial = { + 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, + relationType: string, +) => + (mockedRelationsCatalog.get(entity?.metadata.name ?? 'NULL') ?? + nullMockedRelationsType)[relationType]; diff --git a/yarn.lock b/yarn.lock index 95a9a07c70..79ab29cc49 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7465,9 +7465,12 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/plugin-permission-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" + "@backstage/types": "workspace:^" "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.61