diff --git a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx new file mode 100644 index 0000000000..2973074a5b --- /dev/null +++ b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx @@ -0,0 +1,40 @@ +/* + * 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 { render } from '@testing-library/react'; +import React from 'react'; +import { MockEntityListContextProvider } from '../../testUtils/providers'; +import { EntityKindFilter } from '../../types'; +import { EntityKindPicker } from './EntityKindPicker'; + +describe('', () => { + it('sets the selected kind filter', async () => { + const updateFilters = jest.fn(); + render( + + , + ); + + expect(updateFilters).toHaveBeenLastCalledWith({ + kind: new EntityKindFilter('component'), + }); + }); +}); diff --git a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx new file mode 100644 index 0000000000..e8c783de39 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx @@ -0,0 +1,43 @@ +/* + * 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 React, { useEffect, useState } from 'react'; +import { Alert } from '@material-ui/lab'; +import { useEntityListProvider } from '../../hooks'; +import { EntityKindFilter } from '../../types'; + +type EntityKindFilterProps = { + initialFilter?: string; + hidden: boolean; +}; + +export const EntityKindPicker = ({ + initialFilter, + hidden, +}: EntityKindFilterProps) => { + const [selectedKind] = useState(initialFilter); + const { updateFilters } = useEntityListProvider(); + + useEffect(() => { + updateFilters({ + kind: selectedKind ? new EntityKindFilter(selectedKind) : undefined, + }); + }, [selectedKind, updateFilters]); + + if (hidden) return null; + + return Kind filter not yet available; +}; diff --git a/plugins/catalog-react/src/components/EntityKindPicker/index.ts b/plugins/catalog-react/src/components/EntityKindPicker/index.ts new file mode 100644 index 0000000000..ec9fde9cde --- /dev/null +++ b/plugins/catalog-react/src/components/EntityKindPicker/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { EntityKindPicker } from './EntityKindPicker'; diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index 55ff0c199d..dc3338645c 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -32,7 +32,7 @@ import { identityApiRef, storageApiRef, } from '@backstage/core'; -import { EntityTagFilter } from '../../types'; +import { EntityTagFilter, UserListFilter } from '../../types'; import { CatalogApi } from '@backstage/catalog-client'; import { catalogApiRef } from '../../api'; import { MockStorageApi } from '@backstage/test-utils'; @@ -68,14 +68,16 @@ const apis = ApiRegistry.from([ [storageApiRef, MockStorageApi.create()], ]); +const mockIsStarredEntity = (entity: Entity) => + entity.metadata.name === 'component-3'; + jest.mock('../../hooks', () => { const actual = jest.requireActual('../../hooks'); return { ...actual, useOwnUser: () => ({ value: mockUser }), useStarredEntities: () => ({ - isStarredEntity: (entity: Entity) => - entity.metadata.name === 'component-3', + isStarredEntity: mockIsStarredEntity, }), }; }); @@ -212,7 +214,8 @@ describe('', () => { fireEvent.click(getByText('Starred')); - expect(updateFilters).toHaveBeenCalledTimes(1); - expect(updateFilters.mock.calls[0][0].user.value).toEqual('starred'); + expect(updateFilters).toHaveBeenLastCalledWith({ + user: new UserListFilter('starred', mockUser, mockIsStarredEntity), + }); }); }); diff --git a/plugins/catalog-react/src/components/index.ts b/plugins/catalog-react/src/components/index.ts index dea6f56095..4aad517b32 100644 --- a/plugins/catalog-react/src/components/index.ts +++ b/plugins/catalog-react/src/components/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +export * from './EntityKindPicker'; export * from './EntityProvider'; export * from './EntityRefLink'; export * from './EntityTable'; diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index 0b17e0e48b..fcb599888e 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -19,6 +19,8 @@ import { act, renderHook } from '@testing-library/react-hooks'; import { ApiProvider, ApiRegistry, + ConfigApi, + configApiRef, IdentityApi, identityApiRef, storageApiRef, @@ -27,13 +29,17 @@ import { MockStorageApi } from '@backstage/test-utils'; import { CatalogApi } from '@backstage/catalog-client'; import { Entity, UserEntity } from '@backstage/catalog-model'; import { - DefaultEntityFilters, EntityListProvider, - EntityListProviderProps, useEntityListProvider, } from './useEntityListProvider'; import { catalogApiRef } from '../api'; -import { EntityKindFilter, EntityTypeFilter, UserListFilter } from '../types'; +import { + EntityKindFilter, + EntityTypeFilter, + UserListFilter, + UserListFilterKind, +} from '../types'; +import { EntityKindPicker, UserListPicker } from '../components'; const mockUser: UserEntity = { apiVersion: 'backstage.io/v1beta1', @@ -73,6 +79,9 @@ const entities: Entity[] = [ }, ]; +const mockConfigApi = { + getOptionalString: () => '', +} as Partial; const mockIdentityApi: Partial = { getUserId: () => 'guest@example.com', }; @@ -83,18 +92,21 @@ const mockCatalogApi: Partial = { getEntityByName: () => Promise.resolve(mockUser), }; const apis = ApiRegistry.from([ + [configApiRef, mockConfigApi], [catalogApiRef, mockCatalogApi], [identityApiRef, mockIdentityApi], [storageApiRef, MockStorageApi.create()], ]); const wrapper = ({ + userFilter, children, - initialFilters, -}: PropsWithChildren>) => { +}: PropsWithChildren<{ userFilter: UserListFilterKind }>) => { return ( - + + @@ -111,9 +123,6 @@ describe('', () => { () => useEntityListProvider(), { wrapper, - initialProps: { - initialFilters: { kind: new EntityKindFilter('component') }, - }, }, ); await waitForValueToChange(() => result.current.backendEntities); @@ -127,9 +136,7 @@ describe('', () => { const { result, waitFor } = renderHook(() => useEntityListProvider(), { wrapper, initialProps: { - initialFilters: { - user: new UserListFilter('owned', mockUser, () => true), - }, + userFilter: 'owned', }, }); await waitFor(() => !!result.current.entities.length); @@ -140,9 +147,6 @@ describe('', () => { it('does not fetch when only frontend filters change', async () => { const { result, waitFor } = renderHook(() => useEntityListProvider(), { wrapper, - initialProps: { - initialFilters: { kind: new EntityKindFilter('component') }, - }, }); await waitFor(() => !!result.current.entities.length); expect(result.current.entities.length).toBe(2); @@ -188,7 +192,7 @@ describe('', () => { mockCatalogApi.getEntities = jest.fn().mockRejectedValue('error'); act(() => { - result.current.updateFilters({ kind: new EntityKindFilter('component') }); + result.current.updateFilters({ kind: new EntityKindFilter('api') }); }); await waitForNextUpdate(); expect(result.current.error).toBeDefined(); diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx index 2cfeeca57c..159c898b74 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx @@ -80,24 +80,12 @@ export const EntityListContext = createContext< EntityListContextProps | undefined >(undefined); -export type EntityListProviderProps< - EntityFilters extends DefaultEntityFilters -> = { - initialFilters?: EntityFilters; -}; - export const EntityListProvider = ({ - initialFilters, children, -}: PropsWithChildren>) => { +}: PropsWithChildren<{}>) => { const catalogApi = useApi(catalogApiRef); - // TODO(timbonicus): is it possible to register initial filters from query params? e.g. if the - // query key matches the generic definition, call a constructor with the value - const [filters, setFilters] = useState( - initialFilters ?? ({} as EntityFilters), - ); - + const [filters, setFilters] = useState({} as EntityFilters); const [entities, setEntities] = useState([]); const [backendEntities, setBackendEntities] = useState([]); diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 97d747f497..358c7f7dc8 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -29,8 +29,12 @@ import { storageApiRef, } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; -import { fireEvent, render, waitFor } from '@testing-library/react'; +import { + MockStorageApi, + renderWithEffects, + wrapInTestApp, +} from '@backstage/test-utils'; +import { fireEvent, waitFor } from '@testing-library/react'; import React from 'react'; import { createComponentRouteRef } from '../../routes'; import { CatalogPage } from './CatalogPage'; @@ -105,7 +109,7 @@ describe('CatalogPage', () => { }; const renderWrapped = (children: React.ReactNode) => - render( + renderWithEffects( wrapInTestApp( { // related to some theme issues in mui-table // https://github.com/mbrn/material-table/issues/1293 it('should render', async () => { - const { findByText, getByTestId } = renderWrapped(); - expect(await findByText(/Owned \(1\)/)).toBeInTheDocument(); + const { getByText, getByTestId } = await renderWrapped(); + expect(getByText(/Owned \(1\)/)).toBeInTheDocument(); fireEvent.click(getByTestId('user-picker-all')); - expect(await findByText(/All \(2\)/)).toBeInTheDocument(); + expect(getByText(/All \(2\)/)).toBeInTheDocument(); }); it('should set initial filter correctly', async () => { - const { findByText } = renderWrapped( + const { getByText } = await renderWrapped( , ); - expect(await findByText(/All \(2\)/)).toBeInTheDocument(); + expect(getByText(/All \(2\)/)).toBeInTheDocument(); }); // this test is for fixing the bug after favoriting an entity, the matching entities defaulting // to "owned" filter and not based on the selected filter it('should render the correct entities filtered on the selectedfilter', async () => { - const { findByText, findAllByTitle, getByTestId } = renderWrapped( + const { getByText, findAllByTitle, getByTestId } = await renderWrapped( , ); - expect(await findByText(/Owned \(1\)/)).toBeInTheDocument(); - expect(await findByText(/Starred/)).toBeInTheDocument(); + expect(getByText(/Owned \(1\)/)).toBeInTheDocument(); + expect(getByText(/Starred/)).toBeInTheDocument(); fireEvent.click(getByTestId('user-picker-starred')); - expect(await findByText(/Starred \(0\)/)).toBeInTheDocument(); + expect(getByText(/Starred \(0\)/)).toBeInTheDocument(); fireEvent.click(getByTestId('user-picker-all')); - expect(await findByText(/All \(2\)/)).toBeInTheDocument(); + expect(getByText(/All \(2\)/)).toBeInTheDocument(); const starredIcons = await findAllByTitle('Add to favorites'); fireEvent.click(starredIcons[0]); - expect(await findByText(/All \(2\)/)).toBeInTheDocument(); + expect(getByText(/All \(2\)/)).toBeInTheDocument(); fireEvent.click(getByTestId('user-picker-starred')); - waitFor(() => expect(findByText(/Starred \(1\)/)).toBeInTheDocument()); + waitFor(() => expect(getByText(/Starred \(1\)/)).toBeInTheDocument()); }); }); diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index abaf1d6cfd..80569f4de6 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -23,7 +23,7 @@ import { TableColumn, } from '@backstage/core'; import { - EntityKindFilter, + EntityKindPicker, EntityListProvider, EntityTagPicker, EntityTypePicker, @@ -58,9 +58,6 @@ export const CatalogPage = ({ columns, }: CatalogPageProps) => { const styles = useStyles(); - const initialFilters = { - kind: new EntityKindFilter('component'), - }; return ( @@ -70,8 +67,9 @@ export const CatalogPage = ({ All your software catalog entities
- +
+