diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx index 86101d3579..e5497d022a 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx @@ -111,8 +111,12 @@ const ownerEntitiesBatch2: Entity[] = [ const mockedQueryEntities: jest.MockedFn = jest.fn(); +const mockedGetEntitiesByRef: jest.MockedFn = + jest.fn(); + const mockCatalogApi: Partial = { queryEntities: mockedQueryEntities, + getEntitiesByRefs: mockedGetEntitiesByRef, }; const mockErrorApi = new MockErrorApi(); @@ -146,7 +150,7 @@ describe('', () => { }); }); - it('renders all owners', async () => { + it('renders all users and groups', async () => { await renderWithEffects( @@ -171,6 +175,7 @@ describe('', () => { }); expect(mockedQueryEntities).toHaveBeenCalledTimes(1); + expect(mockedGetEntitiesByRef).not.toHaveBeenCalled(); fireEvent.scroll(screen.getByTestId('owner-picker-listbox')); @@ -205,11 +210,71 @@ describe('', () => { , ); + 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( + + + + + , + ); + + 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('', () => { , ); + expect(mockedGetEntitiesByRef).not.toHaveBeenCalled(); expect(updateFilters).toHaveBeenLastCalledWith({ owners: undefined, }); @@ -250,6 +316,9 @@ describe('', () => { , ); + expect(mockedGetEntitiesByRef).toHaveBeenCalledWith({ + entityRefs: ['group:default/some-owner'], + }); expect(updateFilters).toHaveBeenLastCalledWith({ owners: new EntityOwnerFilter(['group:default/some-owner']), }); @@ -279,6 +348,9 @@ describe('', () => { , ); + expect(mockedGetEntitiesByRef).toHaveBeenCalledWith({ + entityRefs: ['team-a'], + }); expect(updateFilters).toHaveBeenLastCalledWith({ owners: new EntityOwnerFilter(['group:default/team-a']), }); diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index 7eae97b73a..2b85b49434 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -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 = () => { ); }; + +/** + * 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>({}); + 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; + }, + }; +}