feat(MyGroupsPicker): use entityPresentationApi for consistent display
Signed-off-by: Julien <julien.hery@ext.adeo.com>
This commit is contained in:
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user