From 67d7412d4fe213dacbadb5468cf2fc8daa4d2e46 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 13 Jun 2023 13:15:47 +0200 Subject: [PATCH] feat: reworking the picker slightly to just use groups and decorate the meta for now Signed-off-by: blam --- .../OwnershipEntityRefPicker.test.tsx | 129 +++++++++++++----- .../OwnershipEntityRefPicker.tsx | 72 ++++++---- 2 files changed, 146 insertions(+), 55 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx index 81bbb21438..40e7b09d8f 100644 --- a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.test.tsx @@ -22,7 +22,12 @@ import { OwnershipEntityRefPicker } from './OwnershipEntityRefPicker'; import { TestApiProvider } from '@backstage/test-utils'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Entity } from '@backstage/catalog-model'; -import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; +import { + ErrorApi, + IdentityApi, + errorApiRef, + identityApiRef, +} from '@backstage/core-plugin-api'; import userEvent from '@testing-library/user-event'; // Create a mock IdentityApi @@ -52,9 +57,14 @@ describe('', () => { const required = false; const catalogApi: jest.Mocked = { - getEntities: jest.fn(async () => ({ items: entities })), + getEntitiesByRefs: jest.fn(async () => ({ items: entities })), } as any; + const mockErrorApi: jest.Mocked = { + post: jest.fn(), + error$: jest.fn(), + }; + beforeEach(() => { entities = [ { @@ -78,7 +88,7 @@ describe('', () => { ]; onChange.mockClear(); - catalogApi.getEntities.mockClear(); + catalogApi.getEntitiesByRefs.mockClear(); }); afterEach(() => { @@ -93,7 +103,7 @@ describe('', () => { entity.spec.members.includes('Bob'), ); - catalogApi.getEntities.mockResolvedValue({ items: userGroups }); + catalogApi.getEntitiesByRefs.mockResolvedValue({ items: userGroups }); const props = { onChange, @@ -106,6 +116,7 @@ describe('', () => { apis={[ [identityApiRef, mockIdentityApi], [catalogApiRef, catalogApi], + [errorApiRef, mockErrorApi], ]} > @@ -113,38 +124,35 @@ describe('', () => { ); await waitFor(() => - expect(catalogApi.getEntities).toHaveBeenCalledTimes(1), + expect(catalogApi.getEntitiesByRefs).toHaveBeenCalledTimes(1), ); - expect(catalogApi.getEntities).toHaveBeenCalledWith({ - filter: { - kind: ['Group'], - 'relations.hasMember': ['group:default/group1', 'group:default/group2'], - }, + expect(catalogApi.getEntitiesByRefs).toHaveBeenCalledWith({ + entityRefs: ['group:default/group1', 'group:default/group2'], }); // Check that getEntities was set up to return the correct data - await expect(catalogApi.getEntities.mock.results[0].value).resolves.toEqual( - { - items: [ - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { name: 'group1' }, - spec: { members: ['Bob'] }, - }, - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { name: 'group2' }, - spec: { members: ['Bob'] }, - }, - ], - }, - ); + await expect( + catalogApi.getEntitiesByRefs.mock.results[0].value, + ).resolves.toEqual({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'group1' }, + spec: { members: ['Bob'] }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'group2' }, + spec: { members: ['Bob'] }, + }, + ], + }); await expect( - catalogApi.getEntities.mock.results[0].value, + catalogApi.getEntitiesByRefs.mock.results[0].value, ).resolves.not.toEqual( expect.objectContaining({ items: expect.arrayContaining([ @@ -163,7 +171,7 @@ describe('', () => { Array.isArray(entity.spec.members) && entity.spec.members.includes('Bob'), ); - catalogApi.getEntities.mockResolvedValue({ items: userGroups }); + catalogApi.getEntitiesByRefs.mockResolvedValue({ items: userGroups }); const props = { onChange, @@ -176,6 +184,7 @@ describe('', () => { apis={[ [identityApiRef, mockIdentityApi], [catalogApiRef, catalogApi], + [errorApiRef, mockErrorApi], ]} > @@ -183,7 +192,7 @@ describe('', () => { ); await waitFor(() => - expect(catalogApi.getEntities).toHaveBeenCalledTimes(1), + expect(catalogApi.getEntitiesByRefs).toHaveBeenCalledTimes(1), ); // Simulate user input @@ -202,4 +211,62 @@ describe('', () => { // Assert that 'group3' is not rendered in the component expect(queryByText('group3')).not.toBeInTheDocument(); }); + + it('should call the onChange handler with the correct entityRef and and use a nice display name', async () => { + const userGroups = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'group1', title: 'My First Group' }, + spec: { members: ['Bob'] }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { name: 'group2', title: 'My Second Group' }, + spec: { members: ['Bob'] }, + }, + ]; + + catalogApi.getEntitiesByRefs.mockResolvedValue({ items: userGroups }); + + const props = { + onChange, + schema, + required, + } as unknown as FieldProps; + + const { getByRole } = render( + + + , + ); + + await waitFor(() => + expect(catalogApi.getEntitiesByRefs).toHaveBeenCalledTimes(1), + ); + + const inputField = getByRole('combobox'); + userEvent.click(inputField); + userEvent.type(inputField, 'group'); + + await waitFor(() => { + const option = getByRole('option', { name: 'My First Group' }); + expect(option).toBeInTheDocument(); + }); + + const option = getByRole('option', { name: 'My First Group' }); + userEvent.click(option); + + await waitFor(() => { + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledWith('group:default/group1'); + }); + }); }); diff --git a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx index f89c6ff789..299e8e66d7 100644 --- a/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnershipEntityRefPicker/OwnershipEntityRefPicker.tsx @@ -14,8 +14,12 @@ * limitations under the License. */ -import React, { useEffect, useState } from 'react'; -import { identityApiRef, useApi } from '@backstage/core-plugin-api'; +import React, { useState } from 'react'; +import { + errorApiRef, + identityApiRef, + useApi, +} from '@backstage/core-plugin-api'; import { TextField, FormControl } from '@material-ui/core'; import { OwnershipEntityRefPickerProps, @@ -24,6 +28,8 @@ import { import { Autocomplete } from '@material-ui/lab'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { NotFoundError } from '@backstage/errors'; +import useAsync from 'react-use/lib/useAsync'; +import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; export { OwnershipEntityRefPickerSchema }; @@ -34,34 +40,52 @@ export const OwnershipEntityRefPicker = ( schema: { title, description }, required, rawErrors, + onChange, } = props; const identityApi = useApi(identityApiRef); const catalogApi = useApi(catalogApiRef); - const [groups, setGroups] = useState([]); - const [selectedGroup, setSelectedGroup] = useState(null); + const errorApi = useApi(errorApiRef); + const [groups, setGroups] = useState< + { + label: string; + ref: string; + }[] + >([]); + const [selectedGroup, setSelectedGroup] = useState(null); - useEffect(() => { - const fetchUserGroups = async () => { - const identity = await identityApi.getBackstageIdentity(); - const userIdentity = identity.ownershipEntityRefs; + useAsync(async () => { + const { ownershipEntityRefs } = await identityApi.getBackstageIdentity(); - if (!userIdentity) { - throw new NotFoundError('No ownership entity refs found'); - } + if (!ownershipEntityRefs || !ownershipEntityRefs.length) { + errorApi.post(new NotFoundError('No ownership entity refs found')); + return; + } - const userOwnedGroups = await catalogApi.getEntities({ - filter: { - kind: ['Group'], - 'relations.hasMember': userIdentity, - }, - }); - const groupValues = userOwnedGroups.items.map(item => item.metadata.name); - setGroups(groupValues); - }; + const { items } = await catalogApi.getEntitiesByRefs({ + entityRefs: ownershipEntityRefs, + }); - fetchUserGroups(); - }, [identityApi, catalogApi]); + const groupValues = items + .filter((e): e is Entity => Boolean(e)) + .map(item => ({ + label: item.metadata.title ?? item.metadata.name, + ref: stringifyEntityRef(item), + })); + + setGroups(groupValues); + }); + + const updateChange = ( + _: React.ChangeEvent<{}>, + value: { label: string; ref: string } | null, + ) => { + setSelectedGroup(value); + onChange(value?.ref ?? ''); + }; return ( setSelectedGroup(value || '')} - getOptionLabel={group => group} + onChange={updateChange} + getOptionLabel={group => group.label} renderInput={params => (