From 5666efa9da685403cf2249791c6bb4427e3a5298 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Wed, 12 May 2021 14:18:48 -0600 Subject: [PATCH] Move type-filtering logic to a hook Signed-off-by: Tim Hansen --- plugins/catalog-react/src/hooks/index.ts | 1 + .../src/hooks/useEntityTypeFilter.tsx | 97 +++++++++++++++++++ .../EntityTypePicker/EntityTypePicker.tsx | 62 ++---------- 3 files changed, 105 insertions(+), 55 deletions(-) create mode 100644 plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx diff --git a/plugins/catalog-react/src/hooks/index.ts b/plugins/catalog-react/src/hooks/index.ts index 77026d1245..5fbdc1ced2 100644 --- a/plugins/catalog-react/src/hooks/index.ts +++ b/plugins/catalog-react/src/hooks/index.ts @@ -21,6 +21,7 @@ export { useEntityListProvider, } from './useEntityListProvider'; export type { DefaultEntityFilters } from './useEntityListProvider'; +export { useEntityTypeFilter } from './useEntityTypeFilter'; export { useOwnUser } from './useOwnUser'; export { useRelatedEntities } from './useRelatedEntities'; export { useStarredEntities } from './useStarredEntities'; diff --git a/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx new file mode 100644 index 0000000000..c5e03b90c6 --- /dev/null +++ b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx @@ -0,0 +1,97 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { useCallback, useEffect, useMemo, useState } from 'react'; +import { useAsync } from 'react-use'; +import { useApi } from '@backstage/core'; +import { catalogApiRef } from '../api'; +import { + DefaultEntityFilters, + useEntityListProvider, +} from './useEntityListProvider'; +import { EntityTypeFilter } from '../types'; + +type EntityTypeReturn = { + loading: boolean; + error?: Error; + types: string[]; + selectedType: string | undefined; + setType: (type: string | undefined) => void; +}; + +/** + * A hook built on top of `useEntityListProvider` for enabling selection of valid `spec.type` values + * based on the selected EntityKindFilter. + */ +export function useEntityTypeFilter(): EntityTypeReturn { + const catalogApi = useApi(catalogApiRef); + const { + filters: { kind: kindFilter, type: typeFilter }, + updateFilters, + } = useEntityListProvider(); + + const [types, setTypes] = useState([]); + const kind = useMemo(() => kindFilter?.value, [kindFilter]); + + // Load all valid spec.type values straight from the catalogApi, paying attention to only the + // kind filter for a complete list. + const { error, loading, value: entities } = useAsync(async () => { + if (kind) { + const items = await catalogApi + .getEntities({ + filter: { kind }, + fields: ['spec.type'], + }) + .then(response => response.items); + return items; + } + return []; + }, [kind, catalogApi]); + + useEffect(() => { + // Resolve the unique set of types from returned entities; could be optimized by a new endpoint + // in the catalog-backend that does this, rather than loading entities with redundant types. + const newTypes = [ + ...new Set( + (entities ?? []).map(e => e.spec?.type).filter(Boolean) as string[], + ), + ].sort(); + setTypes(newTypes); + + // Reset type filter if no longer applicable + updateFilters((oldFilters: DefaultEntityFilters) => + oldFilters.type && !newTypes.includes(oldFilters.type.value) + ? { type: undefined } + : {}, + ); + }, [updateFilters, entities]); + + const setType = useCallback( + (type: string | undefined) => + updateFilters({ + type: type === undefined ? undefined : new EntityTypeFilter(type), + }), + [updateFilters], + ); + + return { + loading, + error, + types, + selectedType: typeFilter?.value, + setType, + }; +} diff --git a/plugins/catalog/src/components/EntityTypePicker/EntityTypePicker.tsx b/plugins/catalog/src/components/EntityTypePicker/EntityTypePicker.tsx index 94ad2323c9..d404c8ea85 100644 --- a/plugins/catalog/src/components/EntityTypePicker/EntityTypePicker.tsx +++ b/plugins/catalog/src/components/EntityTypePicker/EntityTypePicker.tsx @@ -14,65 +14,21 @@ * limitations under the License. */ -import React, { useEffect, useMemo, useState } from 'react'; +import React from 'react'; import { capitalize } from 'lodash'; -import { useAsync } from 'react-use'; import { Box } from '@material-ui/core'; import { alertApiRef, Select, useApi } from '@backstage/core'; -import { - catalogApiRef, - DefaultEntityFilters, - EntityTypeFilter, - useEntityListProvider, -} from '@backstage/plugin-catalog-react'; +import { useEntityTypeFilter } from '@backstage/plugin-catalog-react'; export const EntityTypePicker = () => { - const catalogApi = useApi(catalogApiRef); const alertApi = useApi(alertApiRef); - - const { - filters: { kind: kindFilter, type: typeFilter }, - updateFilters, - } = useEntityListProvider(); - const [types, setTypes] = useState([]); - - const kind = useMemo(() => kindFilter?.value, [kindFilter]); - - // Load all valid spec.type values straight from the catalogApi - we want the full set for the - // selected kinds, not an otherwise filtered set. - const { error, value: entities } = useAsync(async () => { - if (kind) { - const items = await catalogApi - .getEntities({ - filter: { kind }, - fields: ['spec.type'], - }) - .then(response => response.items); - return items; - } - return []; - }, [kind, catalogApi]); - - useEffect(() => { - const newTypes = [ - ...new Set( - (entities ?? []).map(e => e.spec?.type).filter(Boolean) as string[], - ), - ].sort(); - setTypes(newTypes); - - updateFilters((oldFilters: DefaultEntityFilters) => - oldFilters.type && !newTypes.includes(oldFilters.type.value) - ? { type: undefined } - : {}, - ); - }, [updateFilters, entities]); + const { error, types, selectedType, setType } = useEntityTypeFilter(); if (!types) return null; if (error) { alertApi.post({ - message: `Failed to load types for ${kind}`, + message: `Failed to load entity types`, severity: 'error', }); return null; @@ -80,23 +36,19 @@ export const EntityTypePicker = () => { const items = [ { value: 'all', label: 'All' }, - ...types.map(type => ({ + ...types.map((type: string) => ({ value: type, label: capitalize(type), })), ]; - const onChange = (value: any) => { - updateFilters({ type: new EntityTypeFilter(value) }); - }; - return (