fix(org): improve relationship entity toggle

Signed-off-by: Carl-Erik Bergström <cbergstrom@spotify.com>
This commit is contained in:
Carl-Erik Bergström
2023-07-04 15:52:09 +02:00
parent c80013badd
commit 43a2137bb8
8 changed files with 2976 additions and 3157 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-org': patch
---
Ensure direct relations are shown for User entities while keeping support for aggregating closest parent group ownership
+1
View File
@@ -59,6 +59,7 @@
"@testing-library/dom": "^8.0.0",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.0.0",
"@types/node": "^16.11.26",
"@types/react": "^16.13.1 || ^17.0.0",
@@ -33,7 +33,7 @@ import {
import React from 'react';
import pluralize from 'pluralize';
import { catalogIndexRouteRef } from '../../../routes';
import { useGetEntities } from './useGetEntities';
import { AnyRelationsType, useGetEntities } from './useGetEntities';
const useStyles = makeStyles((theme: BackstageTheme) =>
createStyles({
@@ -109,13 +109,11 @@ const EntityCountTile = ({
export const ComponentsGrid = ({
entity,
relationsType,
isGroup,
entityFilterKind,
entityLimit = 6,
}: {
entity: Entity;
relationsType: string;
isGroup: boolean;
relationsType: AnyRelationsType;
entityFilterKind?: string[];
entityLimit?: number;
}) => {
@@ -123,7 +121,6 @@ export const ComponentsGrid = ({
const { componentsWithCounters, loading, error } = useGetEntities(
entity,
relationsType,
isGroup,
entityFilterKind,
entityLimit,
);
@@ -299,7 +299,7 @@ describe('OwnershipCard', () => {
const { getByText } = await renderInTestApp(
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<EntityProvider entity={userEntity}>
<OwnershipCard />
<OwnershipCard relationsType="aggregated" />
</EntityProvider>
</TestApiProvider>,
{
@@ -27,6 +27,7 @@ import {
} from '@material-ui/core';
import React, { useState } from 'react';
import { ComponentsGrid } from './ComponentsGrid';
import { AnyRelationsType, DefaultRelationType } from './useGetEntities';
const useStyles = makeStyles(theme => ({
list: {
@@ -56,7 +57,7 @@ export const OwnershipCard = (props: {
variant?: InfoCardVariants;
entityFilterKind?: string[];
hideRelationsToggle?: boolean;
relationsType?: string;
relationsType?: AnyRelationsType;
entityLimit?: number;
}) => {
const {
@@ -70,9 +71,8 @@ export const OwnershipCard = (props: {
hideRelationsToggle === undefined ? false : hideRelationsToggle;
const classes = useStyles();
const { entity } = useEntity();
const isGroup = entity.kind === 'Group';
const [getRelationsType, setRelationsType] = useState(
relationsType || 'direct',
relationsType || DefaultRelationType.Direct,
);
return (
@@ -102,7 +102,6 @@ export const OwnershipCard = (props: {
}
name="pin"
inputProps={{ 'aria-label': 'Ownership Type Switch' }}
disabled={!isGroup}
/>
</Tooltip>
Aggregated Relations
@@ -114,7 +113,6 @@ export const OwnershipCard = (props: {
entity={entity}
entityLimit={entityLimit}
relationsType={getRelationsType}
isGroup={isGroup}
entityFilterKind={entityFilterKind}
/>
</InfoCard>
@@ -0,0 +1,65 @@
/*
* 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 { Entity } from '@backstage/catalog-model';
import { useGetEntities } from './useGetEntities';
import { CatalogApi } from '@backstage/catalog-client';
import { renderHook } from '@testing-library/react-hooks';
const catalogApiMock: Pick<CatalogApi, 'getEntities'> = {
getEntities: jest.fn(async () => {
return Promise.resolve({ items: [] });
}),
};
jest.mock('@backstage/core-plugin-api', () => ({
useApi: jest.fn(() => catalogApiMock),
}));
jest.mock('@backstage/plugin-catalog-react', () => ({
catalogApiRef: {},
getEntityRelations: jest.fn(() => []),
}));
describe('useGetEntities', () => {
describe('given aggregated relationsType', () => {
it('when entity is group should aggregate child ownership', async () => {
const givenSquad = 'team.squad1';
const whenEntity = {
kind: 'Group',
metadata: {
name: givenSquad,
},
} as Partial<Entity> as Entity;
const hook = renderHook(
({ entity }) => useGetEntities(entity, 'aggregated'),
{
initialProps: { entity: whenEntity },
},
);
await hook.waitForNextUpdate();
expect(catalogApiMock.getEntities).toHaveBeenCalledWith(
expect.objectContaining({
filter: expect.arrayContaining([
expect.objectContaining({
'relations.ownedBy': [`group:default/${givenSquad}`],
}),
]),
}),
);
});
});
});
@@ -57,22 +57,18 @@ const getQueryParams = (
return qs.stringify({ filters }, { arrayFormat: 'repeat' });
};
const getOwnersEntityRef = (owner: Entity): string[] => {
let owners = [stringifyEntityRef(owner)];
if (owner.kind === 'User') {
const ownerGroups = getEntityRelations(owner, RELATION_MEMBER_OF, {
kind: 'Group',
});
const ownerGroupsName = ownerGroups.map(ownerGroup =>
stringifyEntityRef({
kind: ownerGroup.kind,
namespace: ownerGroup.namespace,
name: ownerGroup.name,
}),
);
owners = [...owners, ...ownerGroupsName];
}
return owners;
const getMemberOfEntityRefs = (owner: Entity): string[] => {
const ownerGroups = getEntityRelations(owner, RELATION_MEMBER_OF, {
kind: 'Group',
});
const ownerGroupsNames = ownerGroups.map(ownerGroup =>
stringifyEntityRef({
kind: ownerGroup.kind,
namespace: ownerGroup.namespace,
name: ownerGroup.name,
}),
);
return ownerGroupsNames;
};
const getAggregatedOwnersEntityRef = async (
@@ -82,7 +78,6 @@ const getAggregatedOwnersEntityRef = async (
const requestedEntities: Entity[] = [];
const outstandingEntities = new Map<string, Promise<Entity | undefined>>();
const processedEntities = new Set<string>();
requestedEntities.push(parentGroup);
let currentEntity = parentGroup;
while (requestedEntities.length > 0) {
@@ -125,10 +120,45 @@ const getAggregatedOwnersEntityRef = async (
return Array.from(processedEntities);
};
const getOwners = async (
entity: Entity,
relationsType: AnyRelationsType,
catalogApi: CatalogApi,
): Promise<string[]> => {
const isGroup = entity.kind === 'Group';
const isAggregated = relationsType === 'aggregated';
const isUserEntity = entity.kind === 'User';
const owners = [stringifyEntityRef(entity)];
if (isAggregated && isGroup) {
const childEntityRefs = await getAggregatedOwnersEntityRef(
entity,
catalogApi,
);
owners.push.apply(owners, childEntityRefs);
}
if (isAggregated && isUserEntity) {
const parentEntityRefs = getMemberOfEntityRefs(entity);
owners.push.apply(owners, parentEntityRefs);
}
return owners;
};
export const DefaultRelationType = {
Direct: 'direct',
Aggregated: 'aggregated',
} as const;
export type AnyRelationsType =
| (typeof DefaultRelationType)[keyof typeof DefaultRelationType]
| string;
export function useGetEntities(
entity: Entity,
relationsType: string,
isGroup: boolean,
relationsType: AnyRelationsType,
entityFilterKind?: string[],
entityLimit = 6,
): {
@@ -151,10 +181,8 @@ export function useGetEntities(
error,
value: componentsWithCounters,
} = useAsync(async () => {
const owners =
relationsType === 'aggregated' && isGroup
? await getAggregatedOwnersEntityRef(entity, catalogApi)
: getOwnersEntityRef(entity);
const owners = await getOwners(entity, relationsType, catalogApi);
const ownedEntitiesList = await catalogApi.getEntities({
filter: [
{
+2848 -3123
View File
File diff suppressed because it is too large Load Diff