feat(EntityPicker): use EntityPresentationApi to display entities in the picker (#24081)
@Julien-Hery Thank you for your contribution! 🎉
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': minor
|
||||
---
|
||||
|
||||
Changed the way to display entities in EntityPicker to use entityPresentationApi instead of humanizeEntityRef
|
||||
@@ -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('<EntityPicker />', () => {
|
||||
getLocationByRef: jest.fn(),
|
||||
removeEntityByUid: jest.fn(),
|
||||
} as any;
|
||||
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -59,7 +64,15 @@ describe('<EntityPicker />', () => {
|
||||
];
|
||||
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, catalogApi],
|
||||
[
|
||||
entityPresentationApiRef,
|
||||
DefaultEntityPresentationApi.create({ catalogApi }),
|
||||
],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
);
|
||||
@@ -90,7 +103,12 @@ describe('<EntityPicker />', () => {
|
||||
);
|
||||
|
||||
expect(catalogApi.getEntities).toHaveBeenCalledWith({
|
||||
fields: ['metadata.name', 'metadata.namespace', 'kind'],
|
||||
fields: [
|
||||
'metadata.name',
|
||||
'metadata.namespace',
|
||||
'metadata.title',
|
||||
'kind',
|
||||
],
|
||||
filter: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<Autocomplete
|
||||
disabled={entities?.length === 1}
|
||||
disabled={entities?.catalogEntities.length === 1}
|
||||
id={idSchema?.$id}
|
||||
value={selectedEntity}
|
||||
loading={loading}
|
||||
onChange={onSelect}
|
||||
options={entities || []}
|
||||
options={entities?.catalogEntities || []}
|
||||
getOptionLabel={option =>
|
||||
// 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 => <EntityDisplayName entityRef={option} />}
|
||||
filterOptions={createFilterOptions<Entity>({
|
||||
stringify: option =>
|
||||
entities?.entityRefToPresentation.get(stringifyEntityRef(option))
|
||||
?.primaryTitle!,
|
||||
})}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
|
||||
@@ -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('<OwnerPicker />', () => {
|
||||
];
|
||||
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, catalogApi],
|
||||
[
|
||||
entityPresentationApiRef,
|
||||
DefaultEntityPresentationApi.create({ catalogApi }),
|
||||
],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
);
|
||||
@@ -99,7 +112,12 @@ describe('<OwnerPicker />', () => {
|
||||
filter: {
|
||||
kind: ['Group', 'User'],
|
||||
},
|
||||
fields: ['metadata.name', 'metadata.namespace', 'kind'],
|
||||
fields: [
|
||||
'metadata.name',
|
||||
'metadata.namespace',
|
||||
'metadata.title',
|
||||
'kind',
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -132,7 +150,12 @@ describe('<OwnerPicker />', () => {
|
||||
filter: {
|
||||
kind: ['User'],
|
||||
},
|
||||
fields: ['metadata.name', 'metadata.namespace', 'kind'],
|
||||
fields: [
|
||||
'metadata.name',
|
||||
'metadata.namespace',
|
||||
'metadata.title',
|
||||
'kind',
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user