From b0bee161ee1213af097865281f1ec01b3100cd82 Mon Sep 17 00:00:00 2001 From: Julien Date: Thu, 25 Jul 2024 11:44:43 +0200 Subject: [PATCH 1/4] feat(MyGroupsPicker): use entityPresentationApi for consistent display Signed-off-by: Julien --- .../MyGroupsPicker/MyGroupsPicker.test.tsx | 22 ++++- .../fields/MyGroupsPicker/MyGroupsPicker.tsx | 83 +++++++++++++------ .../components/fields/VirtualizedListbox.tsx | 33 +++++--- 3 files changed, 100 insertions(+), 38 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx b/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx index 94261ce0e4..4e54ee99ad 100644 --- a/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx @@ -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('', () => { [identityApiRef, mockIdentityApi], [catalogApiRef, catalogApi], [errorApiRef, mockErrorApi], + [ + entityPresentationApiRef, + DefaultEntityPresentationApi.create({ catalogApi }), + ], ]} > @@ -189,6 +197,10 @@ describe('', () => { [identityApiRef, mockIdentityApi], [catalogApiRef, catalogApi], [errorApiRef, mockErrorApi], + [ + entityPresentationApiRef, + DefaultEntityPresentationApi.create({ catalogApi }), + ], ]} > @@ -246,6 +258,10 @@ describe('', () => { [identityApiRef, mockIdentityApi], [catalogApiRef, catalogApi], [errorApiRef, mockErrorApi], + [ + entityPresentationApiRef, + DefaultEntityPresentationApi.create({ catalogApi }), + ], ]} > @@ -306,6 +322,10 @@ describe('', () => { [identityApiRef, mockIdentityApi], [catalogApiRef, catalogApi], [errorApiRef, mockErrorApi], + [ + entityPresentationApiRef, + DefaultEntityPresentationApi.create({ catalogApi }), + ], ]} > diff --git a/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.tsx b/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.tsx index 1e78fa45a1..dcb4be4c44 100644 --- a/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.tsx +++ b/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.tsx @@ -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 ( { error={rawErrors?.length > 0} > group.label} + getOptionLabel={option => + groups?.entityRefToPresentation.get(stringifyEntityRef(option)) + ?.primaryTitle! + } + autoSelect renderInput={params => ( { FormHelperTextProps={{ margin: 'dense', style: { marginLeft: 0 } }} variant="outlined" required={required} + InputProps={params.InputProps} /> )} + renderOption={option => } + filterOptions={createFilterOptions({ + stringify: option => + groups?.entityRefToPresentation.get(stringifyEntityRef(option)) + ?.primaryTitle!, + })} + ListboxComponent={VirtualizedListbox} /> ); diff --git a/plugins/scaffolder/src/components/fields/VirtualizedListbox.tsx b/plugins/scaffolder/src/components/fields/VirtualizedListbox.tsx index b0920b626a..b2bd611cec 100644 --- a/plugins/scaffolder/src/components/fields/VirtualizedListbox.tsx +++ b/plugins/scaffolder/src/components/fields/VirtualizedListbox.tsx @@ -22,29 +22,40 @@ const renderRow = (props: ListChildComponentProps) => { return React.cloneElement(data[index], { style }); }; +const OuterElementContext = React.createContext({}); + +const OuterElementType = React.forwardRef((props, ref) => { + const outerProps = React.useContext(OuterElementContext); + return
; +}); + 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 (
- - {renderRow} - + + + {renderRow} + +
); }); From 1552c334bab3e4ddcc45e77f7a45ed967d84e98e Mon Sep 17 00:00:00 2001 From: Julien Date: Thu, 25 Jul 2024 11:51:45 +0200 Subject: [PATCH 2/4] chore: add changeset Signed-off-by: Julien --- .changeset/perfect-cars-jam.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/perfect-cars-jam.md diff --git a/.changeset/perfect-cars-jam.md b/.changeset/perfect-cars-jam.md new file mode 100644 index 0000000000..a22791034a --- /dev/null +++ b/.changeset/perfect-cars-jam.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Changed the way to display entities in `MyGroupsPicker` to use entityPresentationApi and make it consistent across scaffolder pickers From a40904dc7d38419ed9a693992b73026634d726fc Mon Sep 17 00:00:00 2001 From: Julien Date: Mon, 12 Aug 2024 09:33:13 +0200 Subject: [PATCH 3/4] fix: add link to mui docs + fix typo in changeset Signed-off-by: Julien --- .changeset/perfect-cars-jam.md | 2 +- plugins/scaffolder/src/components/fields/VirtualizedListbox.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/perfect-cars-jam.md b/.changeset/perfect-cars-jam.md index a22791034a..45166ac823 100644 --- a/.changeset/perfect-cars-jam.md +++ b/.changeset/perfect-cars-jam.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder': minor --- -Changed the way to display entities in `MyGroupsPicker` to use entityPresentationApi and make it consistent across scaffolder pickers +Changed the way to display entities in `MyGroupsPicker` to use `entityPresentationApi` and make it consistent across scaffolder pickers diff --git a/plugins/scaffolder/src/components/fields/VirtualizedListbox.tsx b/plugins/scaffolder/src/components/fields/VirtualizedListbox.tsx index b2bd611cec..2fad510918 100644 --- a/plugins/scaffolder/src/components/fields/VirtualizedListbox.tsx +++ b/plugins/scaffolder/src/components/fields/VirtualizedListbox.tsx @@ -22,6 +22,7 @@ const renderRow = (props: ListChildComponentProps) => { return React.cloneElement(data[index], { style }); }; +// Context needed to keep Autocomplete working correctly : https://v4.mui.com/components/autocomplete/#virtualization const OuterElementContext = React.createContext({}); const OuterElementType = React.forwardRef((props, ref) => { From 11ffdbd7f3e4cf516a682ac2eeb029b62875ac85 Mon Sep 17 00:00:00 2001 From: Julien Date: Mon, 12 Aug 2024 09:34:37 +0200 Subject: [PATCH 4/4] fix: add correct link to mui docs for Virtualization Signed-off-by: Julien --- plugins/scaffolder/src/components/fields/VirtualizedListbox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/scaffolder/src/components/fields/VirtualizedListbox.tsx b/plugins/scaffolder/src/components/fields/VirtualizedListbox.tsx index 2fad510918..61e96cd7a9 100644 --- a/plugins/scaffolder/src/components/fields/VirtualizedListbox.tsx +++ b/plugins/scaffolder/src/components/fields/VirtualizedListbox.tsx @@ -22,7 +22,7 @@ const renderRow = (props: ListChildComponentProps) => { return React.cloneElement(data[index], { style }); }; -// Context needed to keep Autocomplete working correctly : https://v4.mui.com/components/autocomplete/#virtualization +// Context needed to keep Autocomplete working correctly : https://v4.mui.com/components/autocomplete/#Virtualize.tsx const OuterElementContext = React.createContext({}); const OuterElementType = React.forwardRef((props, ref) => {