feat(MyGroupsPicker): use entityPresentationApi for consistent display

Signed-off-by: Julien <julien.hery@ext.adeo.com>
This commit is contained in:
Julien
2024-07-25 11:44:43 +02:00
parent 6010595e1f
commit b0bee161ee
3 changed files with 100 additions and 38 deletions
@@ -19,7 +19,10 @@ import { render, waitFor } from '@testing-library/react';
import { CatalogApi } from '@backstage/catalog-client';
import { MyGroupsPicker } from './MyGroupsPicker';
import { TestApiProvider } from '@backstage/test-utils';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import {
catalogApiRef,
entityPresentationApiRef,
} from '@backstage/plugin-catalog-react';
import { Entity } from '@backstage/catalog-model';
import {
ErrorApi,
@@ -29,6 +32,7 @@ import {
} from '@backstage/core-plugin-api';
import userEvent from '@testing-library/user-event';
import { ScaffolderRJSFFieldProps as FieldProps } from '@backstage/plugin-scaffolder-react';
import { DefaultEntityPresentationApi } from '@backstage/plugin-catalog';
// Create a mock IdentityApi
const mockIdentityApi: IdentityApi = {
@@ -117,6 +121,10 @@ describe('<MyGroupsPicker />', () => {
[identityApiRef, mockIdentityApi],
[catalogApiRef, catalogApi],
[errorApiRef, mockErrorApi],
[
entityPresentationApiRef,
DefaultEntityPresentationApi.create({ catalogApi }),
],
]}
>
<MyGroupsPicker {...props} />
@@ -189,6 +197,10 @@ describe('<MyGroupsPicker />', () => {
[identityApiRef, mockIdentityApi],
[catalogApiRef, catalogApi],
[errorApiRef, mockErrorApi],
[
entityPresentationApiRef,
DefaultEntityPresentationApi.create({ catalogApi }),
],
]}
>
<MyGroupsPicker {...props} />
@@ -246,6 +258,10 @@ describe('<MyGroupsPicker />', () => {
[identityApiRef, mockIdentityApi],
[catalogApiRef, catalogApi],
[errorApiRef, mockErrorApi],
[
entityPresentationApiRef,
DefaultEntityPresentationApi.create({ catalogApi }),
],
]}
>
<MyGroupsPicker {...props} />
@@ -306,6 +322,10 @@ describe('<MyGroupsPicker />', () => {
[identityApiRef, mockIdentityApi],
[catalogApiRef, catalogApi],
[errorApiRef, mockErrorApi],
[
entityPresentationApiRef,
DefaultEntityPresentationApi.create({ catalogApi }),
],
]}
>
<MyGroupsPicker {...props} />
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { useState } from 'react';
import React, { useEffect } from 'react';
import {
errorApiRef,
identityApiRef,
@@ -23,11 +23,19 @@ import {
import TextField from '@material-ui/core/TextField';
import FormControl from '@material-ui/core/FormControl';
import { MyGroupsPickerProps, MyGroupsPickerSchema } from './schema';
import Autocomplete from '@material-ui/lab/Autocomplete';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import Autocomplete, {
createFilterOptions,
} from '@material-ui/lab/Autocomplete';
import {
catalogApiRef,
EntityDisplayName,
entityPresentationApiRef,
EntityRefPresentationSnapshot,
} from '@backstage/plugin-catalog-react';
import { NotFoundError } from '@backstage/errors';
import useAsync from 'react-use/esm/useAsync';
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
import { VirtualizedListbox } from '../VirtualizedListbox';
export { MyGroupsPickerSchema };
@@ -43,19 +51,14 @@ export const MyGroupsPicker = (props: MyGroupsPickerProps) => {
const identityApi = useApi(identityApiRef);
const catalogApi = useApi(catalogApiRef);
const errorApi = useApi(errorApiRef);
const [groups, setGroups] = useState<
{
label: string;
ref: string;
}[]
>([]);
const entityPresentationApi = useApi(entityPresentationApiRef);
useAsync(async () => {
const { value: groups, loading } = useAsync(async () => {
const { userEntityRef } = await identityApi.getBackstageIdentity();
if (!userEntityRef) {
errorApi.post(new NotFoundError('No user entity ref found'));
return;
return { catalogEntities: [], entityRefToPresentation: new Map() };
}
const { items } = await catalogApi.getEntities({
@@ -65,24 +68,38 @@ export const MyGroupsPicker = (props: MyGroupsPickerProps) => {
},
});
const groupValues = items
.filter((e): e is Entity => Boolean(e))
.map(item => ({
label: item.metadata.title ?? item.metadata.name,
ref: stringifyEntityRef(item),
}));
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,
];
}),
),
);
setGroups(groupValues);
return { catalogEntities: items, entityRefToPresentation };
});
const updateChange = (
_: React.ChangeEvent<{}>,
value: { label: string; ref: string } | null,
) => {
onChange(value?.ref ?? '');
const updateChange = (_: React.ChangeEvent<{}>, value: Entity | null) => {
onChange(value ? stringifyEntityRef(value) : '');
};
const selectedEntity = groups?.find(e => e.ref === formData) || null;
const selectedEntity =
groups?.catalogEntities.find(e => stringifyEntityRef(e) === formData) ||
null;
useEffect(() => {
if (groups?.catalogEntities.length === 1 && !selectedEntity) {
onChange(stringifyEntityRef(groups.catalogEntities[0]));
}
}, [groups, onChange, selectedEntity]);
return (
<FormControl
@@ -91,11 +108,17 @@ export const MyGroupsPicker = (props: MyGroupsPickerProps) => {
error={rawErrors?.length > 0}
>
<Autocomplete
disabled={groups?.catalogEntities.length === 1}
id="OwnershipEntityRefPicker-dropdown"
options={groups || []}
options={groups?.catalogEntities || []}
value={selectedEntity}
loading={loading}
onChange={updateChange}
getOptionLabel={group => group.label}
getOptionLabel={option =>
groups?.entityRefToPresentation.get(stringifyEntityRef(option))
?.primaryTitle!
}
autoSelect
renderInput={params => (
<TextField
{...params}
@@ -105,8 +128,16 @@ export const MyGroupsPicker = (props: MyGroupsPickerProps) => {
FormHelperTextProps={{ margin: 'dense', style: { marginLeft: 0 } }}
variant="outlined"
required={required}
InputProps={params.InputProps}
/>
)}
renderOption={option => <EntityDisplayName entityRef={option} />}
filterOptions={createFilterOptions<Entity>({
stringify: option =>
groups?.entityRefToPresentation.get(stringifyEntityRef(option))
?.primaryTitle!,
})}
ListboxComponent={VirtualizedListbox}
/>
</FormControl>
);
@@ -22,29 +22,40 @@ const renderRow = (props: ListChildComponentProps) => {
return React.cloneElement(data[index], { style });
};
const OuterElementContext = React.createContext({});
const OuterElementType = React.forwardRef<HTMLDivElement>((props, ref) => {
const outerProps = React.useContext(OuterElementContext);
return <div ref={ref} {...props} {...outerProps} />;
});
export const VirtualizedListbox = React.forwardRef<
HTMLDivElement,
{ children?: React.ReactNode }
>((props, ref) => {
const itemData = React.Children.toArray(props.children);
const { children, ...other } = props;
const itemData = React.Children.toArray(children);
const itemCount = itemData.length;
const itemSize = 36;
const itemsToShow = Math.min(10, itemCount);
const height = Math.max(itemSize, itemsToShow * itemSize - 0.5 * itemSize);
const height = itemsToShow * itemSize;
return (
<div ref={ref}>
<FixedSizeList
height={height}
itemData={itemData}
itemCount={itemCount}
itemSize={itemSize}
width="100%"
>
{renderRow}
</FixedSizeList>
<OuterElementContext.Provider value={other}>
<FixedSizeList
height={height}
itemData={itemData}
itemCount={itemCount}
itemSize={itemSize}
outerElementType={OuterElementType}
width="100%"
>
{renderRow}
</FixedSizeList>
</OuterElementContext.Provider>
</div>
);
});