catalog-react: humanize display selected entities

Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>
This commit is contained in:
Vincenzo Scamporlino
2023-05-15 14:53:43 +02:00
parent 85b6e588eb
commit 3648a4ebb8
2 changed files with 127 additions and 5 deletions
@@ -111,8 +111,12 @@ const ownerEntitiesBatch2: Entity[] = [
const mockedQueryEntities: jest.MockedFn<CatalogApi['queryEntities']> =
jest.fn();
const mockedGetEntitiesByRef: jest.MockedFn<CatalogApi['getEntitiesByRefs']> =
jest.fn();
const mockCatalogApi: Partial<CatalogApi> = {
queryEntities: mockedQueryEntities,
getEntitiesByRefs: mockedGetEntitiesByRef,
};
const mockErrorApi = new MockErrorApi();
@@ -146,7 +150,7 @@ describe('<EntityOwnerPicker/>', () => {
});
});
it('renders all owners', async () => {
it('renders all users and groups', async () => {
await renderWithEffects(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider value={{}}>
@@ -171,6 +175,7 @@ describe('<EntityOwnerPicker/>', () => {
});
expect(mockedQueryEntities).toHaveBeenCalledTimes(1);
expect(mockedGetEntitiesByRef).not.toHaveBeenCalled();
fireEvent.scroll(screen.getByTestId('owner-picker-listbox'));
@@ -205,11 +210,71 @@ describe('<EntityOwnerPicker/>', () => {
</ApiProvider>,
);
expect(mockedGetEntitiesByRef).toHaveBeenCalledWith({
entityRefs: ['another-owner'],
});
expect(updateFilters).toHaveBeenLastCalledWith({
owners: new EntityOwnerFilter(['group:default/another-owner']),
});
});
it('should display the selected owners as humanized entities', async () => {
const updateFilters = jest.fn();
const queryParameters = { owners: ['another-owner'] };
mockedGetEntitiesByRef.mockResolvedValue({
items: [
{
metadata: {
name: 'another-owner',
title: 'Beautiful display name',
namespace: 'default',
},
apiVersion: '1',
kind: 'group',
},
],
});
await renderWithEffects(
<ApiProvider apis={mockApis}>
<MockEntityListContextProvider
value={{
updateFilters,
queryParameters,
}}
>
<EntityOwnerPicker />
</MockEntityListContextProvider>
</ApiProvider>,
);
await waitFor(() =>
expect(
screen.getByRole('button', {
name: 'Beautiful display name',
}),
).toBeInTheDocument(),
);
expect(mockedGetEntitiesByRef).toHaveBeenCalledWith({
entityRefs: ['another-owner'],
});
fireEvent.click(screen.getByTestId('owner-picker-expand'));
await waitFor(() => screen.getByText('Some Owner 2'));
fireEvent.click(screen.getByText('Some Owner 2'));
expect(mockedGetEntitiesByRef).toHaveBeenCalledTimes(1);
await waitFor(() =>
expect(
screen.getByRole('button', {
name: 'Some Owner 2',
}),
).toBeInTheDocument(),
);
});
it('adds owners to filters', async () => {
const updateFilters = jest.fn();
await renderWithEffects(
@@ -223,6 +288,7 @@ describe('<EntityOwnerPicker/>', () => {
</MockEntityListContextProvider>
</ApiProvider>,
);
expect(mockedGetEntitiesByRef).not.toHaveBeenCalled();
expect(updateFilters).toHaveBeenLastCalledWith({
owners: undefined,
});
@@ -250,6 +316,9 @@ describe('<EntityOwnerPicker/>', () => {
</MockEntityListContextProvider>
</ApiProvider>,
);
expect(mockedGetEntitiesByRef).toHaveBeenCalledWith({
entityRefs: ['group:default/some-owner'],
});
expect(updateFilters).toHaveBeenLastCalledWith({
owners: new EntityOwnerFilter(['group:default/some-owner']),
});
@@ -279,6 +348,9 @@ describe('<EntityOwnerPicker/>', () => {
</MockEntityListContextProvider>
</ApiProvider>,
);
expect(mockedGetEntitiesByRef).toHaveBeenCalledWith({
entityRefs: ['team-a'],
});
expect(updateFilters).toHaveBeenLastCalledWith({
owners: new EntityOwnerFilter(['group:default/team-a']),
});
@@ -26,11 +26,12 @@ import CheckBoxIcon from '@material-ui/icons/CheckBox';
import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import { Autocomplete } from '@material-ui/lab';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useEntityList } from '../../hooks/useEntityListProvider';
import { EntityOwnerFilter } from '../../filters';
import { useApi } from '@backstage/core-plugin-api';
import { catalogApiRef } from '../../api';
import useAsync from 'react-use/lib/useAsync';
import useAsyncFn from 'react-use/lib/useAsyncFn';
import { useDebouncedEffect } from '@react-hookz/web';
import PersonIcon from '@material-ui/icons/Person';
@@ -102,6 +103,8 @@ export const EntityOwnerPicker = () => {
queryParamOwners.length ? queryParamOwners : filters.owners?.values ?? [],
);
const { getEntity, setEntity } = useSelectedOwners(selectedOwners);
// Set selected owners on query parameter updates; this happens at initial page load and from
// external updates to the page location.
useEffect(() => {
@@ -143,12 +146,26 @@ export const EntityOwnerPicker = () => {
}
return o === v;
}}
getOptionLabel={o => {
const entity = typeof o === 'string' ? getEntity(o) || o : o;
return typeof entity === 'string'
? entity
: humanizeEntity(entity, entity.metadata.name);
}}
onChange={(_: object, owners) => {
setText('');
setSelectedOwners(
owners.map(e =>
typeof e === 'string' ? e : stringifyEntityRef(e),
),
owners.map(e => {
const entityRef =
typeof e === 'string' ? e : stringifyEntityRef(e);
if (typeof e !== 'string') {
setEntity(e);
}
return entityRef;
}),
);
}}
filterOptions={x => x}
@@ -214,3 +231,36 @@ export const EntityOwnerPicker = () => {
</Box>
);
};
/**
* Hook used for storing the full entity of the specified owners
* in order to display users and group using the information contained on each entity.
* When a component is rendered for the first time, it loads the content of the entities
* specified by `initialSelectedOwnersRefs` and export the `getEntity` and `setEntity`
* utilities, used to retrieve and modify the owners.
*/
function useSelectedOwners(initialSelectedOwnersRefs: string[]) {
const allEntities = useRef<Record<string, Entity>>({});
const catalogApi = useApi(catalogApiRef);
useAsync(async () => {
if (initialSelectedOwnersRefs.length === 0) {
return;
}
const initialSelectedEntities = await catalogApi.getEntitiesByRefs({
entityRefs: initialSelectedOwnersRefs,
});
initialSelectedEntities.items.forEach(e => {
if (e) {
allEntities.current[stringifyEntityRef(e)] = e;
}
});
}, []);
return {
getEntity: (entityRef: string) => allEntities.current[entityRef],
setEntity: (entity: Entity) => {
allEntities.current[stringifyEntityRef(entity)] = entity;
},
};
}