diff --git a/.changeset/slow-dolls-type.md b/.changeset/slow-dolls-type.md new file mode 100644 index 0000000000..d2626f3ab3 --- /dev/null +++ b/.changeset/slow-dolls-type.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Changed the way to display entities in EntityPicker to use entityPresentationApi instead of humanizeEntityRef diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx index 8016a6ae83..dcb52a3949 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx @@ -16,14 +16,18 @@ import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; -import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; +import { + CatalogApi, + catalogApiRef, + entityPresentationApiRef, +} from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; - import { fireEvent, screen } from '@testing-library/react'; import React from 'react'; import { EntityPicker } from './EntityPicker'; import { EntityPickerProps } from './schema'; import { ScaffolderRJSFFieldProps as FieldProps } from '@backstage/plugin-scaffolder-react'; +import { DefaultEntityPresentationApi } from '@backstage/plugin-catalog'; const makeEntity = (kind: string, namespace: string, name: string): Entity => ({ apiVersion: 'scaffolder.backstage.io/v1beta3', @@ -50,6 +54,7 @@ describe('', () => { getLocationByRef: jest.fn(), removeEntityByUid: jest.fn(), } as any; + let Wrapper: React.ComponentType>; beforeEach(() => { @@ -59,7 +64,15 @@ describe('', () => { ]; Wrapper = ({ children }: { children?: React.ReactNode }) => ( - + {children} ); @@ -90,7 +103,12 @@ describe('', () => { ); expect(catalogApi.getEntities).toHaveBeenCalledWith({ - fields: ['metadata.name', 'metadata.namespace', 'kind'], + fields: [ + 'metadata.name', + 'metadata.namespace', + 'metadata.title', + 'kind', + ], filter: undefined, }); }); diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx index 69e1b5efad..176f6e9ce0 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx @@ -24,13 +24,16 @@ import { } from '@backstage/catalog-model'; import { useApi } from '@backstage/core-plugin-api'; import { + EntityDisplayName, + EntityRefPresentationSnapshot, catalogApiRef, - humanizeEntityRef, + entityPresentationApiRef, } from '@backstage/plugin-catalog-react'; import TextField from '@material-ui/core/TextField'; import FormControl from '@material-ui/core/FormControl'; import Autocomplete, { AutocompleteChangeReason, + createFilterOptions, } from '@material-ui/lab/Autocomplete'; import React, { useCallback, useEffect } from 'react'; import useAsync from 'react-use/esm/useAsync'; @@ -65,31 +68,55 @@ export const EntityPicker = (props: EntityPickerProps) => { uiSchema['ui:options']?.defaultNamespace || undefined; const catalogApi = useApi(catalogApiRef); + const entityPresentationApi = useApi(entityPresentationApiRef); const { value: entities, loading } = useAsync(async () => { - const fields = ['metadata.name', 'metadata.namespace', 'kind']; + const fields = [ + 'metadata.name', + 'metadata.namespace', + 'metadata.title', + 'kind', + ]; const { items } = await catalogApi.getEntities( catalogFilter ? { filter: catalogFilter, fields } : { filter: undefined, fields }, ); - return items; + + const entityRefToPresentation = new Map< + string, + EntityRefPresentationSnapshot + >( + await Promise.all( + items.map(async item => { + const presentation = await entityPresentationApi.forEntity(item) + .promise; + return [stringifyEntityRef(item), presentation] as [ + string, + EntityRefPresentationSnapshot, + ]; + }), + ), + ); + + return { catalogEntities: items, entityRefToPresentation }; }); + const allowArbitraryValues = uiSchema['ui:options']?.allowArbitraryValues ?? true; const getLabel = useCallback( - (ref: string) => { + (freeSoloValue: string) => { try { - return humanizeEntityRef( - parseEntityRef(ref, { defaultKind, defaultNamespace }), - { - defaultKind, - defaultNamespace, - }, - ); + // Will throw if defaultKind or defaultNamespace are not set + const parsedRef = parseEntityRef(freeSoloValue, { + defaultKind, + defaultNamespace, + }); + + return stringifyEntityRef(parsedRef); } catch (err) { - return ref; + return freeSoloValue; } }, [defaultKind, defaultNamespace], @@ -129,12 +156,12 @@ export const EntityPicker = (props: EntityPickerProps) => { // Since free solo can be enabled, attempt to parse as a full entity ref first, then fall // back to the given value. const selectedEntity = - entities?.find(e => stringifyEntityRef(e) === formData) ?? + entities?.catalogEntities.find(e => stringifyEntityRef(e) === formData) ?? (allowArbitraryValues && formData ? getLabel(formData) : ''); useEffect(() => { - if (entities?.length === 1 && selectedEntity === '') { - onChange(stringifyEntityRef(entities[0])); + if (entities?.catalogEntities.length === 1 && selectedEntity === '') { + onChange(stringifyEntityRef(entities.catalogEntities[0])); } }, [entities, onChange, selectedEntity]); @@ -145,17 +172,18 @@ export const EntityPicker = (props: EntityPickerProps) => { error={rawErrors?.length > 0 && !formData} > // option can be a string due to freeSolo. typeof option === 'string' ? option - : humanizeEntityRef(option, { defaultKind, defaultNamespace })! + : entities?.entityRefToPresentation.get(stringifyEntityRef(option)) + ?.entityRef! } autoSelect freeSolo={allowArbitraryValues} @@ -171,6 +199,12 @@ export const EntityPicker = (props: EntityPickerProps) => { InputProps={params.InputProps} /> )} + renderOption={option => } + filterOptions={createFilterOptions({ + stringify: option => + entities?.entityRefToPresentation.get(stringifyEntityRef(option)) + ?.primaryTitle!, + })} /> ); diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx index 57ca8e53ab..79105e05c3 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx @@ -16,11 +16,16 @@ import { type EntityFilterQuery } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; -import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; +import { + CatalogApi, + catalogApiRef, + entityPresentationApiRef, +} from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { ScaffolderRJSFFieldProps as FieldProps } from '@backstage/plugin-scaffolder-react'; import React from 'react'; import { OwnerPicker } from './OwnerPicker'; +import { DefaultEntityPresentationApi } from '@backstage/plugin-catalog'; const makeEntity = (kind: string, namespace: string, name: string): Entity => ({ apiVersion: 'backstage.io/v1beta1', @@ -64,7 +69,15 @@ describe('', () => { ]; Wrapper = ({ children }: { children?: React.ReactNode }) => ( - + {children} ); @@ -99,7 +112,12 @@ describe('', () => { filter: { kind: ['Group', 'User'], }, - fields: ['metadata.name', 'metadata.namespace', 'kind'], + fields: [ + 'metadata.name', + 'metadata.namespace', + 'metadata.title', + 'kind', + ], }), ); }); @@ -132,7 +150,12 @@ describe('', () => { filter: { kind: ['User'], }, - fields: ['metadata.name', 'metadata.namespace', 'kind'], + fields: [ + 'metadata.name', + 'metadata.namespace', + 'metadata.title', + 'kind', + ], }), ); });