From 473f5ff4ddb68358b4a2c5de3bfe55ef5635ceff Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Wed, 7 Jun 2023 20:39:08 +0200 Subject: [PATCH] catalog-react: introduce mode on EntityOwnerPicker Signed-off-by: Vincenzo Scamporlino --- .../EntityOwnerPicker/EntityOwnerPicker.tsx | 132 +++++------------- .../EntityOwnerPicker/useFetchEntities.ts | 92 ++++++++++++ 2 files changed, 130 insertions(+), 94 deletions(-) create mode 100644 plugins/catalog-react/src/components/EntityOwnerPicker/useFetchEntities.ts diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index 8b223c50b1..f894368a4a 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -14,7 +14,11 @@ * limitations under the License. */ -import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; +import { + Entity, + parseEntityRef, + stringifyEntityRef, +} from '@backstage/catalog-model'; import { Box, Checkbox, @@ -27,17 +31,14 @@ import CheckBoxIcon from '@material-ui/icons/CheckBox'; import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { Autocomplete } from '@material-ui/lab'; -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { useEntityList } from '../../hooks/useEntityListProvider'; import { EntityOwnerFilter } from '../../filters'; -import { useApi } from '@backstage/core-plugin-api'; -import { catalogApiRef } from '../../api'; -import useAsync from 'react-use/lib/useAsync'; -import useAsyncFn from 'react-use/lib/useAsyncFn'; import { useDebouncedEffect } from '@react-hookz/web'; import PersonIcon from '@material-ui/icons/Person'; import GroupIcon from '@material-ui/icons/Group'; -import { humanizeEntity } from '../EntityRefLink/humanize'; +import { humanizeEntity, humanizeEntityRef } from '../EntityRefLink/humanize'; +import { useFetchEntities } from './useFetchEntities'; /** @public */ export type CatalogReactEntityOwnerPickerClassKey = 'input'; @@ -54,57 +55,22 @@ const useStyles = makeStyles( const icon = ; const checkedIcon = ; +export type EntityOwnerPickerProps = { + mode?: 'owners-only' | 'all'; +}; + /** @public */ -export const EntityOwnerPicker = () => { +export const EntityOwnerPicker = (props?: EntityOwnerPickerProps) => { const classes = useStyles(); + const { mode = 'owners-only' } = props || {}; const { updateFilters, filters, queryParameters: { owners: ownersParameter }, } = useEntityList(); - const catalogApi = useApi(catalogApiRef); const [text, setText] = useState(''); - const [{ value, loading }, handleFetch] = useAsyncFn( - async (request: { text: string } | { cursor: string; prev: Entity[] }) => { - const initialRequest = request as { text: string }; - const cursorRequest = request as { cursor: string; prev: Entity[] }; - const limit = 20; - - if (cursorRequest.cursor) { - const response = await catalogApi.queryEntities({ - cursor: cursorRequest.cursor, - limit, - }); - return { - ...response, - items: [...cursorRequest.prev, ...response.items], - }; - } - - return catalogApi.queryEntities({ - fullTextFilter: { - term: initialRequest.text || '', - fields: [ - 'metadata.name', - 'kind', - 'spec.profile.displayname', - 'metadata.title', - ], - }, - filter: { kind: ['User', 'Group'] }, - orderFields: [{ field: 'metadata.name', order: 'asc' }], - limit, - }); - }, - [text], - ); - - useDebouncedEffect(() => handleFetch({ text }), [text], 250); - - const availableOwners = value?.items || []; - const queryParamOwners = useMemo( () => [ownersParameter].flat().filter(Boolean) as string[], [ownersParameter], @@ -114,7 +80,14 @@ export const EntityOwnerPicker = () => { queryParamOwners.length ? queryParamOwners : filters.owners?.values ?? [], ); - const { getEntity, setEntity } = useSelectedOwners(selectedOwners); + const [{ value, loading }, handleFetch, { getEntity, setEntity }] = + useFetchEntities({ + mode, + initialSelectedOwnersRefs: selectedOwners, + }); + useDebouncedEffect(() => handleFetch({ text }), [text, handleFetch], 250); + + const availableOwners = value?.items || []; // Set selected owners on query parameter updates; this happens at initial page load and from // external updates to the page location. @@ -158,11 +131,15 @@ export const EntityOwnerPicker = () => { return o === v; }} getOptionLabel={o => { - const entity = typeof o === 'string' ? getEntity(o) || o : o; - - return typeof entity === 'string' - ? entity - : humanizeEntity(entity, entity.metadata.name); + const entity = + typeof o === 'string' + ? getEntity(o) || + parseEntityRef(o, { + defaultKind: 'group', + defaultNamespace: 'default', + }) + : o; + return humanizeEntity(entity, humanizeEntityRef(entity)); }} onChange={(_: object, owners) => { setText(''); @@ -181,7 +158,7 @@ export const EntityOwnerPicker = () => { }} filterOptions={x => x} renderOption={(entity, { selected }) => { - const isGroup = entity.kind === 'Group'; + const isGroup = entity.kind.toLocaleLowerCase('en-US') === 'group'; return ( { )}   - {humanizeEntity(entity, entity.metadata.name)} + {humanizeEntity( + entity, + humanizeEntityRef(entity, { defaultKind: entity.kind }), + )} } /> @@ -229,11 +209,8 @@ export const EntityOwnerPicker = () => { element.scrollTop, ) < 1; - if (hasReachedEnd && value?.pageInfo.nextCursor) { - handleFetch({ - cursor: value.pageInfo.nextCursor, - prev: value.items, - }); + if (hasReachedEnd && value?.cursor) { + handleFetch({ items: value.items, cursor: value.cursor }); } }, 'data-testid': 'owner-picker-listbox', @@ -243,36 +220,3 @@ export const EntityOwnerPicker = () => { ); }; - -/** - * Hook used for storing the full entity of the specified owners - * in order to display users and group using the information contained on each entity. - * When a component is rendered for the first time, it loads the content of the entities - * specified by `initialSelectedOwnersRefs` and export the `getEntity` and `setEntity` - * utilities, used to retrieve and modify the owners. - */ -function useSelectedOwners(initialSelectedOwnersRefs: string[]) { - const allEntities = useRef>({}); - const catalogApi = useApi(catalogApiRef); - - useAsync(async () => { - if (initialSelectedOwnersRefs.length === 0) { - return; - } - const initialSelectedEntities = await catalogApi.getEntitiesByRefs({ - entityRefs: initialSelectedOwnersRefs, - }); - initialSelectedEntities.items.forEach(e => { - if (e) { - allEntities.current[stringifyEntityRef(e)] = e; - } - }); - }, []); - - return { - getEntity: (entityRef: string) => allEntities.current[entityRef], - setEntity: (entity: Entity) => { - allEntities.current[stringifyEntityRef(entity)] = entity; - }, - }; -} diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/useFetchEntities.ts b/plugins/catalog-react/src/components/EntityOwnerPicker/useFetchEntities.ts new file mode 100644 index 0000000000..01cef24fd6 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/useFetchEntities.ts @@ -0,0 +1,92 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useRef } from 'react'; +import { useFacetsEntities } from './useFacetsEntities'; +import { useQueryEntities } from './useQueryEntities'; +import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; +import { useApi } from '@backstage/core-plugin-api'; +import { catalogApiRef } from '../../api'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { useMountEffect } from '@react-hookz/web'; + +export function useFetchEntities({ + mode, + initialSelectedOwnersRefs, +}: { + mode: 'owners-only' | 'all'; + initialSelectedOwnersRefs: string[]; +}) { + const isOwnersOnlyMode = mode === 'owners-only'; + const queryEntitiesResponse = useQueryEntities(); + const facetsEntitiesResponse = useFacetsEntities({ + enabled: isOwnersOnlyMode, + }); + + const [state, handleFetch] = isOwnersOnlyMode + ? facetsEntitiesResponse + : queryEntitiesResponse; + + return [ + state, + handleFetch, + useSelectedOwners({ + enabled: !isOwnersOnlyMode, + initialSelectedOwnersRefs, + }), + ] as const; +} + +/** + * Hook used for storing the full entity of the specified owners + * in order to display users and group using the information contained on each entity. + * When a component is rendered for the first time, it loads the content of the entities + * specified by `initialSelectedOwnersRefs` and export the `getEntity` and `setEntity` + * utilities, used to retrieve and modify the owners. + */ +function useSelectedOwners({ + enabled, + initialSelectedOwnersRefs, +}: { + enabled: boolean; + initialSelectedOwnersRefs: string[]; +}) { + const allEntities = useRef>({}); + const catalogApi = useApi(catalogApiRef); + + const [, handleFetch] = useAsyncFn(async () => { + const initialSelectedEntities = await catalogApi.getEntitiesByRefs({ + entityRefs: initialSelectedOwnersRefs, + }); + initialSelectedEntities.items.forEach(e => { + if (e) { + allEntities.current[stringifyEntityRef(e)] = e; + } + }); + }, []); + + useMountEffect(() => { + if (enabled && initialSelectedOwnersRefs.length > 0) { + handleFetch(); + } + }); + + return { + getEntity: (entityRef: string) => allEntities.current[entityRef], + setEntity: (entity: Entity) => { + allEntities.current[stringifyEntityRef(entity)] = entity; + }, + }; +}