diff --git a/.changeset/grumpy-dolls-call.md b/.changeset/grumpy-dolls-call.md new file mode 100644 index 0000000000..13d96ecb1f --- /dev/null +++ b/.changeset/grumpy-dolls-call.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-catalog-react': minor +'@backstage/plugin-scaffolder': patch +--- + +Updated the software templates list page (`ScaffolderPage`) to use the `useEntityListProvider` hook from #5643. This reduces the code footprint, making it easier to customize the display of this page, and consolidates duplicate approaches to querying the catalog with filters. + +- The `useEntityTypeFilter` hook has been updated along with the underlying `EntityTypeFilter` to work with multiple values, to allow more flexibility for different user interfaces. It's unlikely that this change affects you; however, if you're using either of these directly, you'll need to update your usage. +- `SearchToolbar` was renamed to `EntitySearchBar` and moved to `catalog-react` to be usable by other entity list pages +- `UserListPicker` now has an `availableTypes` prop to restrict which user-related options to present diff --git a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx index 8faa86c3ca..417b085cce 100644 --- a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx @@ -17,7 +17,7 @@ import { render } from '@testing-library/react'; import React from 'react'; import { MockEntityListContextProvider } from '../../testUtils/providers'; -import { EntityKindFilter } from '../../types'; +import { EntityKindFilter } from '../../filters'; import { EntityKindPicker } from './EntityKindPicker'; describe('', () => { diff --git a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx index db695d58cc..92831ea252 100644 --- a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx +++ b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx @@ -17,7 +17,7 @@ import React, { useEffect, useState } from 'react'; import { Alert } from '@material-ui/lab'; import { useEntityListProvider } from '../../hooks'; -import { EntityKindFilter } from '../../types'; +import { EntityKindFilter } from '../../filters'; type EntityKindFilterProps = { initialFilter?: string; diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx index 8e8bb5aeba..8fa0789c10 100644 --- a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx @@ -18,7 +18,7 @@ import { Entity } from '@backstage/catalog-model'; import { fireEvent, render } from '@testing-library/react'; import React from 'react'; import { MockEntityListContextProvider } from '../../testUtils/providers'; -import { EntityLifecycleFilter } from '../../types'; +import { EntityLifecycleFilter } from '../../filters'; import { EntityLifecyclePicker } from './EntityLifecyclePicker'; const sampleEntities: Entity[] = [ diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx index a5fe16ae40..31ab2911f7 100644 --- a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx +++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx @@ -28,7 +28,7 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { Autocomplete } from '@material-ui/lab'; import React, { useMemo } from 'react'; import { useEntityListProvider } from '../../hooks/useEntityListProvider'; -import { EntityLifecycleFilter } from '../../types'; +import { EntityLifecycleFilter } from '../../filters'; const icon = ; const checkedIcon = ; diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx index c39fcb887a..c46f6ed8f8 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx @@ -18,7 +18,7 @@ import { Entity } from '@backstage/catalog-model'; import { fireEvent, render } from '@testing-library/react'; import React from 'react'; import { MockEntityListContextProvider } from '../../testUtils/providers'; -import { EntityOwnerFilter } from '../../types'; +import { EntityOwnerFilter } from '../../filters'; import { EntityOwnerPicker } from './EntityOwnerPicker'; const sampleEntities: Entity[] = [ diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index 665529fddb..955f917c8c 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -28,7 +28,7 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { Autocomplete } from '@material-ui/lab'; import React, { useMemo } from 'react'; import { useEntityListProvider } from '../../hooks/useEntityListProvider'; -import { EntityOwnerFilter } from '../../types'; +import { EntityOwnerFilter } from '../../filters'; import { getEntityRelations } from '../../utils'; import { formatEntityRefTitle } from '../EntityRefLink'; diff --git a/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.test.tsx b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.test.tsx new file mode 100644 index 0000000000..0b9ea9cb98 --- /dev/null +++ b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.test.tsx @@ -0,0 +1,53 @@ +/* + * Copyright 2021 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 React from 'react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; +import { EntitySearchBar } from './EntitySearchBar'; +import { DefaultEntityFilters } from '../../hooks/useEntityListProvider'; +import { EntityTextFilter } from '../../filters'; +import { MockEntityListContextProvider } from '../../testUtils/providers'; + +describe('EntitySearchBar', () => { + it('should display search value and execute set callback', async () => { + const updateFilters = jest.fn(); + + const filters: DefaultEntityFilters = { + text: new EntityTextFilter('hello'), + }; + + const { getByDisplayValue } = render( + + + , + ); + + const searchInput = getByDisplayValue('hello'); + expect(searchInput).toBeInTheDocument(); + + fireEvent.change(searchInput, { target: { value: 'world' } }); + await waitFor(() => expect(updateFilters.mock.calls.length).toBe(1)); + expect(updateFilters).toHaveBeenCalledWith({ + text: new EntityTextFilter('world'), + }); + + fireEvent.change(searchInput, { target: { value: '' } }); + await waitFor(() => expect(updateFilters.mock.calls.length).toBe(2)); + expect(updateFilters).toHaveBeenCalledWith({ + text: undefined, + }); + }); +}); diff --git a/plugins/scaffolder/src/components/SearchToolbar/SearchToolbar.tsx b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx similarity index 76% rename from plugins/scaffolder/src/components/SearchToolbar/SearchToolbar.tsx rename to plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx index 0a7488195c..9fd30b5182 100644 --- a/plugins/scaffolder/src/components/SearchToolbar/SearchToolbar.tsx +++ b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx @@ -13,22 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; + import { FormControl, + IconButton, + Input, InputAdornment, makeStyles, Toolbar, - Input, - IconButton, } from '@material-ui/core'; -import Search from '@material-ui/icons/Search'; import Clear from '@material-ui/icons/Clear'; - -interface Props { - search: string; - setSearch: Function; -} +import Search from '@material-ui/icons/Search'; +import React, { useState } from 'react'; +import { useDebounce } from 'react-use'; +import { useEntityListProvider } from '../../hooks/useEntityListProvider'; +import { EntityTextFilter } from '../../filters'; const useStyles = makeStyles(_theme => ({ searchToolbar: { @@ -37,8 +36,22 @@ const useStyles = makeStyles(_theme => ({ }, })); -const SearchToolbar = ({ search, setSearch }: Props) => { +export const EntitySearchBar = () => { const styles = useStyles(); + + const { filters, updateFilters } = useEntityListProvider(); + const [search, setSearch] = useState(filters.text?.value ?? ''); + + useDebounce( + () => { + updateFilters({ + text: search.length ? new EntityTextFilter(search) : undefined, + }); + }, + 250, + [search, updateFilters], + ); + return ( @@ -70,5 +83,3 @@ const SearchToolbar = ({ search, setSearch }: Props) => { ); }; - -export default SearchToolbar; diff --git a/plugins/scaffolder/src/filter/index.ts b/plugins/catalog-react/src/components/EntitySearchBar/index.ts similarity index 57% rename from plugins/scaffolder/src/filter/index.ts rename to plugins/catalog-react/src/components/EntitySearchBar/index.ts index bed464222f..044b8ee416 100644 --- a/plugins/scaffolder/src/filter/index.ts +++ b/plugins/catalog-react/src/components/EntitySearchBar/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * 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. @@ -14,15 +14,4 @@ * limitations under the License. */ -export { EntityFilterGroupsProvider } from './EntityFilterGroupsProvider'; -export type { - EntityFilterFn, - FilterGroup, - FilterGroupState, - FilterGroupStates, - FilterGroupStatesError, - FilterGroupStatesLoading, - FilterGroupStatesReady, -} from './types'; -export { useEntityFilterGroup } from './useEntityFilterGroup'; -export { useFilteredEntities } from './useFilteredEntities'; +export { EntitySearchBar } from './EntitySearchBar'; diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx index 9d65221b76..8b1b97a261 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx @@ -18,7 +18,7 @@ import { Entity } from '@backstage/catalog-model'; import { fireEvent, render } from '@testing-library/react'; import React from 'react'; import { MockEntityListContextProvider } from '../../testUtils/providers'; -import { EntityTagFilter } from '../../types'; +import { EntityTagFilter } from '../../filters'; import { EntityTagPicker } from './EntityTagPicker'; const taggedEntities: Entity[] = [ diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx index bd92b68478..744ad447d8 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx @@ -28,7 +28,7 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { Autocomplete } from '@material-ui/lab'; import React, { useMemo } from 'react'; import { useEntityListProvider } from '../../hooks/useEntityListProvider'; -import { EntityTagFilter } from '../../types'; +import { EntityTagFilter } from '../../filters'; const icon = ; const checkedIcon = ; diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx index 29ec36d625..fccf985f4f 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx @@ -22,7 +22,7 @@ import { Entity } from '@backstage/catalog-model'; import { EntityTypePicker } from './EntityTypePicker'; import { MockEntityListContextProvider } from '../../testUtils/providers'; import { catalogApiRef } from '../../api'; -import { EntityKindFilter, EntityTypeFilter } from '../../types'; +import { EntityKindFilter, EntityTypeFilter } from '../../filters'; import { AlertApi, alertApiRef } from '@backstage/core-plugin-api'; import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; @@ -123,7 +123,7 @@ describe('', () => { fireEvent.click(rendered.getByText('Service')); expect(updateFilters).toHaveBeenLastCalledWith({ - type: new EntityTypeFilter('service'), + type: new EntityTypeFilter(['service']), }); fireEvent.click(input); diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx index 7b7845b8bc..9713df263d 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx @@ -15,7 +15,7 @@ */ import React, { useEffect } from 'react'; -import { capitalize } from 'lodash'; +import capitalize from 'lodash/capitalize'; import { Box } from '@material-ui/core'; import { useEntityTypeFilter } from '../../hooks/useEntityTypeFilter'; @@ -24,7 +24,12 @@ import { Select } from '@backstage/core-components'; export const EntityTypePicker = () => { const alertApi = useApi(alertApiRef); - const { error, types, selectedType, setType } = useEntityTypeFilter(); + const { + error, + availableTypes, + selectedTypes, + setSelectedTypes, + } = useEntityTypeFilter(); useEffect(() => { if (error) { @@ -35,13 +40,11 @@ export const EntityTypePicker = () => { } }, [error, alertApi]); - if (!types || error) { - return null; - } + if (!availableTypes || error) return null; const items = [ { value: 'all', label: 'All' }, - ...types.map((type: string) => ({ + ...availableTypes.map((type: string) => ({ value: type, label: capitalize(type), })), @@ -52,8 +55,10 @@ export const EntityTypePicker = () => {