Fix test cases and update async logic.

Signed-off-by: Aramis Sennyey <sennyeya@amazon.com>
This commit is contained in:
Aramis Sennyey
2023-02-22 11:52:57 -05:00
committed by Fredrik Adelöw
parent 487d699993
commit 3555c2741e
3 changed files with 109 additions and 115 deletions
@@ -14,15 +14,20 @@
* limitations under the License.
*/
import { Entity, parseEntityRef } from '@backstage/catalog-model';
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
import { fireEvent, screen } from '@testing-library/react';
import React from 'react';
import { MockEntityListContextProvider } from '../../testUtils/providers';
import { EntityOwnerFilter } from '../../filters';
import { EntityOwnerPicker } from './EntityOwnerPicker';
import { ApiProvider } from '@backstage/core-app-api';
import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils';
import {
MockErrorApi,
renderWithEffects,
TestApiRegistry,
} from '@backstage/test-utils';
import { catalogApiRef, CatalogApi } from '../..';
import { errorApiRef } from '@backstage/core-plugin-api';
const ownerEntities: Entity[] = [
{
@@ -38,12 +43,18 @@ const ownerEntities: Entity[] = [
metadata: {
name: 'some-owner-2',
},
spec: {
profile: {
displayName: 'Some Owner 2',
},
},
},
{
apiVersion: '1',
kind: 'Group',
metadata: {
name: 'another-owner',
title: 'Another Owner',
},
},
];
@@ -96,57 +107,20 @@ const sampleEntities: Entity[] = [
const getEntitiesByRefs = jest.fn(async ({ entityRefs }) => ({
items: entityRefs.map((e: string) =>
ownerEntities.find(f => f.metadata.name === e),
ownerEntities.find(f => stringifyEntityRef(f) === e),
),
}));
const mockCatalogApi: Partial<CatalogApi> = {
getEntitiesByRefs,
};
const mockErrorApi = new MockErrorApi();
describe('<EntityOwnerPicker/>', () => {
const mockApis = TestApiRegistry.from([catalogApiRef, mockCatalogApi]);
const mockApis = TestApiRegistry.from(
[catalogApiRef, mockCatalogApi],
[errorApiRef, mockErrorApi],
);
it('renders display name when available', async () => {
const names: Record<string, string> = {
'some-owner': 'Some Team',
'some-owner-2': 'Other Team',
'another-owner': 'AnotherTeam',
};
getEntitiesByRefs.mockResolvedValueOnce({
items: ownerEntities.map(e => {
e.spec = {
...e.spec,
profile: {
displayName: names[e.metadata.name],
},
};
return e;
}),
});
await renderWithEffects(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider
value={{ entities: sampleEntities, backendEntities: sampleEntities }}
>
<EntityOwnerPicker />
</MockEntityListContextProvider>
</ApiProvider>,
);
expect(screen.getByText('Owner')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('owner-picker-expand'));
sampleEntities
.flatMap(e => e.relations?.map(r => parseEntityRef(r.targetRef).name))
.forEach(owner => {
expect(screen.getByText(names[owner as string])).toBeInTheDocument();
});
});
/**
* All previous test cases are still applicable for the case where there is no
* owner entity returned from the API or the owner entity returned does not have
* a display name.
*/
it('renders all owners', async () => {
await renderWithEffects(
<ApiProvider apis={mockApis}>
@@ -160,11 +134,9 @@ describe('<EntityOwnerPicker/>', () => {
expect(screen.getByText('Owner')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('owner-picker-expand'));
sampleEntities
.flatMap(e => e.relations?.map(r => parseEntityRef(r.targetRef).name))
.forEach(owner => {
expect(screen.getByText(owner as string)).toBeInTheDocument();
});
['Another Owner', 'some-owner', 'Some Owner 2'].forEach(owner => {
expect(screen.getByText(owner)).toBeInTheDocument();
});
});
it('renders unique owners in alphabetical order', async () => {
@@ -182,9 +154,9 @@ describe('<EntityOwnerPicker/>', () => {
fireEvent.click(screen.getByTestId('owner-picker-expand'));
expect(screen.getAllByRole('option').map(o => o.textContent)).toEqual([
'another-owner',
'Another Owner',
'some-owner',
'some-owner-2',
'Some Owner 2',
]);
});
@@ -16,7 +16,6 @@
import {
Entity,
parseEntityRef,
RELATION_OWNED_BY,
stringifyEntityRef,
} from '@backstage/catalog-model';
@@ -36,11 +35,10 @@ import React, { useEffect, useMemo, useState } from 'react';
import { useEntityList } from '../../hooks/useEntityListProvider';
import { EntityOwnerFilter } from '../../filters';
import { getEntityRelations } from '../../utils';
import { humanizeEntityRef } from '../EntityRefLink';
import useAsync from 'react-use/lib/useAsync';
import { useApi } from '@backstage/core-plugin-api';
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
import { catalogApiRef } from '../../api';
import { humanizeEntity } from '../EntityRefLink/humanize';
import { humanizeEntity, humanizeEntityRef } from '../EntityRefLink/humanize';
/** @public */
export type CatalogReactEntityOwnerPickerClassKey = 'input';
@@ -67,12 +65,74 @@ export const EntityOwnerPicker = () => {
queryParameters: { owners: ownersParameter },
} = useEntityList();
const catalogApi = useApi(catalogApiRef);
const errorApi = useApi(errorApiRef);
const queryParamOwners = useMemo(
() => [ownersParameter].flat().filter(Boolean) as string[],
[ownersParameter],
);
const {
loading,
error,
value: ownerEntities,
} = useAsync(async () => {
const availableOwners = [
...new Set(
backendEntities
.flatMap((e: Entity) =>
getEntityRelations(e, RELATION_OWNED_BY).map(o =>
stringifyEntityRef(o),
),
)
.filter(Boolean) as string[],
),
];
const { items } = await catalogApi.getEntitiesByRefs({
entityRefs: availableOwners,
fields: [
'kind',
'metadata.name',
'metadata.title',
'metadata.namespace',
'spec.profile.displayName',
],
});
return (
availableOwners
.map(
(e, i) =>
items[i] || ({ metadata: { name: e }, kind: 'Group' } as Entity),
)
// Keep the previous sorting logic.
.sort((a, b) => {
const nameA = humanizeEntity(a).toLocaleUpperCase('en-US'); // ignore upper and lowercase
const nameB = humanizeEntity(b).toLocaleUpperCase('en-US'); // ignore upper and lowercase
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
// names must be equal
return 0;
})
);
}, [backendEntities]);
useEffect(() => {
if (error) {
errorApi.post(
{
...error,
message: `EntityOwnerPicker failed to initialize: ${error.message}`,
},
{},
);
}
}, [error, errorApi]);
const [selectedOwners, setSelectedOwners] = useState(
queryParamOwners.length ? queryParamOwners : filters.owners?.values ?? [],
);
@@ -85,55 +145,16 @@ export const EntityOwnerPicker = () => {
}
}, [queryParamOwners]);
const availableOwners = useMemo(
() =>
[
...new Set(
backendEntities
.flatMap((e: Entity) =>
getEntityRelations(e, RELATION_OWNED_BY).map(o =>
humanizeEntityRef(o, { defaultKind: 'group' }),
),
)
.filter(Boolean) as string[],
),
].sort(),
[backendEntities],
);
const {
loading,
error,
value: ownerEntities,
} = useAsync(async () => {
const { items } = await catalogApi.getEntitiesByRefs({
entityRefs: availableOwners.map(ref =>
stringifyEntityRef(parseEntityRef(ref, { defaultKind: 'Group' })),
),
fields: [
'kind',
'metadata.name',
'metadata.title',
'spec.profile.displayName',
],
});
return availableOwners.map(
(e, i) =>
items.at(i) || ({ metadata: { name: e }, kind: 'Group' } as Entity),
);
}, [availableOwners]);
useEffect(() => {
updateFilters({
owners:
selectedOwners.length && availableOwners.length
? new EntityOwnerFilter(selectedOwners)
: undefined,
});
}, [selectedOwners, updateFilters, availableOwners]);
if (!availableOwners.length) return null;
if (error) throw error;
if (!loading && ownerEntities) {
updateFilters({
owners:
selectedOwners.length && ownerEntities.length
? new EntityOwnerFilter(selectedOwners)
: undefined,
});
}
}, [selectedOwners, updateFilters, ownerEntities, loading]);
return (
<Box pb={1} pt={1}>
@@ -152,7 +173,9 @@ export const EntityOwnerPicker = () => {
) ?? []
}
onChange={(_: object, value: Entity[]) =>
setSelectedOwners(value.map(e => e.metadata.name))
setSelectedOwners(
value.map(e => humanizeEntityRef(e, { defaultKind: 'Group' })),
)
}
getOptionLabel={option =>
humanizeEntity(option, { defaultKind: 'Group' })
@@ -175,6 +198,7 @@ export const EntityOwnerPicker = () => {
renderInput={params => (
<TextField
{...params}
error={!!error}
className={classes.input}
variant="outlined"
/>
@@ -18,8 +18,8 @@ import {
Entity,
CompoundEntityRef,
DEFAULT_NAMESPACE,
UserEntity,
} from '@backstage/catalog-model';
import get from 'lodash/get';
/**
* @param defaultNamespace - if set to false then namespace is never omitted,
@@ -88,14 +88,12 @@ export function humanizeEntity(
defaultNamespace?: string | false;
},
) {
let title: string | undefined = undefined;
switch (entity.kind) {
case 'User':
case 'Group':
title = (entity as UserEntity).spec?.profile?.displayName;
break;
default:
title = entity.metadata.title;
for (const path of ['spec.profile.displayName', 'metadata.title']) {
const value = get(entity, path);
if (value && typeof value === 'string') {
return value;
}
}
return title || humanizeEntityRef(entity, opts);
return humanizeEntityRef(entity, opts);
}