feat: MultiEntityPicker uses entityPresentationApi to display entity instead of humanizeEntityRef

Signed-off-by: NIKUNJ LALITKUMAR HUDKA <nikunjhudka123@gmail.com>
This commit is contained in:
NIKUNJ LALITKUMAR HUDKA
2024-04-20 12:09:43 -03:00
parent ff03fd55de
commit 929cb26118
2 changed files with 60 additions and 33 deletions
@@ -16,7 +16,11 @@
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';
@@ -24,6 +28,7 @@ import React from 'react';
import { MultiEntityPicker } from './MultiEntityPicker';
import { MultiEntityPickerProps } 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',
@@ -59,7 +64,15 @@ describe('<MultiEntityPicker />', () => {
];
Wrapper = ({ children }: { children?: React.ReactNode }) => (
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<TestApiProvider
apis={[
[catalogApiRef, catalogApi],
[
entityPresentationApiRef,
DefaultEntityPresentationApi.create({ catalogApi }),
],
]}
>
{children}
</TestApiProvider>
);
@@ -25,7 +25,9 @@ import {
import { useApi } from '@backstage/core-plugin-api';
import {
catalogApiRef,
humanizeEntityRef,
entityPresentationApiRef,
EntityRefPresentationSnapshot,
EntityDisplayName,
} from '@backstage/plugin-catalog-react';
import TextField from '@material-ui/core/TextField';
import FormControl from '@material-ui/core/FormControl';
@@ -64,40 +66,35 @@ export const MultiEntityPicker = (props: MultiEntityPickerProps) => {
uiSchema['ui:options']?.defaultNamespace || undefined;
const catalogApi = useApi(catalogApiRef);
const entityPresentationApi = useApi(entityPresentationApiRef);
const { value: entities, loading } = useAsync(async () => {
const { items } = await catalogApi.getEntities(
catalogFilter ? { filter: catalogFilter } : undefined,
);
return items;
const primaryTitles: string[] = [];
for (const item of items) {
const entityPresentation = (await entityPresentationApi.forEntity(item)
?.promise) as EntityRefPresentationSnapshot[];
entityPresentation.map(e => primaryTitles.push(e.primaryTitle));
}
return { items, primaryTitles };
});
const allowArbitraryValues =
uiSchema['ui:options']?.allowArbitraryValues ?? true;
const getLabel = useCallback(
(ref: string) => {
try {
return humanizeEntityRef(
parseEntityRef(ref, { defaultKind, defaultNamespace }),
{
defaultKind,
defaultNamespace,
},
);
} catch (err) {
return ref;
}
},
[defaultKind, defaultNamespace],
);
const onSelect = useCallback(
(_: any, refs: (string | Entity)[], reason: AutocompleteChangeReason) => {
const values = refs
.map(ref => {
if (typeof ref !== 'string') {
// if ref does not exist: pass 'undefined' to trigger validation for required value
return ref ? stringifyEntityRef(ref as Entity) : undefined;
return ref
? entityPresentationApi.forEntity(ref, {
defaultKind,
defaultNamespace,
}).snapshot.entityRef
: undefined;
}
if (reason === 'blur' || reason === 'create-option') {
// Add in default namespace, etc.
@@ -126,14 +123,21 @@ export const MultiEntityPicker = (props: MultiEntityPickerProps) => {
onChange(values);
},
[onChange, formData, defaultKind, defaultNamespace, allowArbitraryValues],
[
onChange,
formData,
defaultKind,
defaultNamespace,
allowArbitraryValues,
entityPresentationApi,
],
);
useEffect(() => {
if (entities?.length === 1) {
onChange([stringifyEntityRef(entities[0])]);
if (entities?.items?.length === 1) {
onChange([stringifyEntityRef(entities.items[0])]);
}
}, [entities, onChange]);
}, [entities?.items, onChange]);
return (
<FormControl
@@ -144,23 +148,30 @@ export const MultiEntityPicker = (props: MultiEntityPickerProps) => {
<Autocomplete
multiple
filterSelectedOptions
disabled={entities?.length === 1}
disabled={entities?.items?.length === 1}
id={idSchema?.$id}
value={
// Since free solo can be enabled, attempt to parse as a full entity ref first, then fall
// back to the given value.
entities?.filter(
entities?.items?.filter(
e => formData && formData.includes(stringifyEntityRef(e)),
) ?? (allowArbitraryValues && formData ? formData.map(getLabel) : [])
) ??
(allowArbitraryValues && formData
? entities?.primaryTitles || []
: [])
}
loading={loading}
onChange={onSelect}
options={entities || []}
options={entities?.items || []}
renderOption={option => <EntityDisplayName entityRef={option} />}
getOptionLabel={option =>
// option can be a string due to freeSolo.
typeof option === 'string'
? option
: humanizeEntityRef(option, { defaultKind, defaultNamespace })!
: entityPresentationApi.forEntity(option, {
defaultKind,
defaultNamespace,
}).snapshot.entityRef!
}
autoSelect
freeSolo={allowArbitraryValues}
@@ -170,7 +181,10 @@ export const MultiEntityPicker = (props: MultiEntityPickerProps) => {
label={title}
margin="dense"
helperText={description}
FormHelperTextProps={{ margin: 'dense', style: { marginLeft: 0 } }}
FormHelperTextProps={{
margin: 'dense',
style: { marginLeft: 0 },
}}
variant="outlined"
required={required}
InputProps={params.InputProps}