From a1dc5415d7ae4c3a8c8cbc4e2b15c28e23e72784 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Mon, 22 May 2023 10:38:53 +0200 Subject: [PATCH 1/3] catalog-react: decouple EntityLifecyclePicker from backendEntities Signed-off-by: Vincenzo Scamporlino --- .../EntityAutocompletePicker.tsx | 14 +- .../EntityLifecyclePicker.test.tsx | 266 +++++++++--------- .../EntityLifecyclePicker.tsx | 113 +------- 3 files changed, 150 insertions(+), 243 deletions(-) diff --git a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx index 3a9a100357..b6de85937d 100644 --- a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx +++ b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx @@ -54,6 +54,7 @@ export type EntityAutocompletePickerProps< showCounts?: boolean; Filter: ConstructableFilter>; InputProps?: TextFieldProps; + initialSelectedOptions?: string[]; }; /** @public */ @@ -61,7 +62,15 @@ export function EntityAutocompletePicker< T extends DefaultEntityFilters = DefaultEntityFilters, Name extends AllowedEntityFilters = AllowedEntityFilters, >(props: EntityAutocompletePickerProps) { - const { label, name, path, showCounts, Filter, InputProps } = props; + const { + label, + name, + path, + showCounts, + Filter, + InputProps, + initialSelectedOptions = [], + } = props; const { updateFilters, @@ -90,7 +99,8 @@ export function EntityAutocompletePicker< const [selectedOptions, setSelectedOptions] = useState( queryParameters.length ? queryParameters - : (filters[name] as unknown as { values: string[] })?.values ?? [], + : (filters[name] as unknown as { values: string[] })?.values ?? + initialSelectedOptions, ); // Set selected options on query parameter updates; this happens at initial page load and from diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx index 3ee610ddaf..19554f18b7 100644 --- a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx @@ -14,146 +14,116 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; -import { fireEvent, render, screen } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import { MockEntityListContextProvider } from '../../testUtils/providers'; import { EntityLifecycleFilter } from '../../filters'; import { EntityLifecyclePicker } from './EntityLifecyclePicker'; - -const sampleEntities: Entity[] = [ - { - apiVersion: '1', - kind: 'Component', - metadata: { - name: 'component-1', - }, - spec: { - lifecycle: 'production', - }, - }, - { - apiVersion: '1', - kind: 'Component', - metadata: { - name: 'component-2', - }, - spec: { - lifecycle: 'experimental', - }, - }, - { - apiVersion: '1', - kind: 'Component', - metadata: { - name: 'component-3', - }, - spec: { - lifecycle: 'experimental', - }, - }, -]; +import { TestApiProvider } from '@backstage/test-utils'; +import { catalogApiRef } from '../../api'; +import { CatalogApi } from '@backstage/catalog-client'; describe('', () => { - it('renders all lifecycles', () => { - render( - - - , - ); - expect(screen.getByText('Lifecycle')).toBeInTheDocument(); + const catalogApi = { + getEntityFacets: jest.fn(), + } as unknown as jest.Mocked; - fireEvent.click(screen.getByTestId('lifecycle-picker-expand')); - sampleEntities - .map(e => e.spec?.lifecycle!) - .forEach(lifecycle => { - expect(screen.getByText(lifecycle as string)).toBeInTheDocument(); - }); + beforeEach(() => { + catalogApi.getEntityFacets.mockResolvedValue({ + facets: { + 'spec.lifecycle': [ + { count: 1, value: 'experimental' }, + { count: 1, value: 'production' }, + ], + }, + }); }); - it('renders unique lifecycles in alphabetical order', () => { - render( - - - , - ); - expect(screen.getByText('Lifecycle')).toBeInTheDocument(); - - fireEvent.click(screen.getByTestId('lifecycle-picker-expand')); - - expect(screen.getAllByRole('option').map(o => o.textContent)).toEqual([ - 'experimental', - 'production', - ]); + afterEach(() => { + jest.resetAllMocks(); }); - it('respects the query parameter filter value', () => { + it('renders all lifecycles', async () => { + render( + + + + + , + ); + expect(await screen.findByText('Lifecycle')).toBeInTheDocument(); + + fireEvent.click(await screen.findByTestId('lifecycles-picker-expand')); + expect(screen.getByText('experimental')).toBeInTheDocument(); + expect(screen.getByText('production')).toBeInTheDocument(); + }); + + it('respects the query parameter filter value', async () => { const updateFilters = jest.fn(); const queryParameters = { lifecycles: ['experimental'] }; render( - - - , + + + + + , ); + await waitFor(() => expect(catalogApi.getEntityFacets).toHaveBeenCalled()); expect(updateFilters).toHaveBeenLastCalledWith({ lifecycles: new EntityLifecycleFilter(['experimental']), }); }); - it('adds lifecycles to filters', () => { + it('adds lifecycles to filters', async () => { const updateFilters = jest.fn(); render( - - - , + + + + + , ); expect(updateFilters).toHaveBeenLastCalledWith({ lifecycles: undefined, }); - fireEvent.click(screen.getByTestId('lifecycle-picker-expand')); + fireEvent.click(await screen.findByTestId('lifecycles-picker-expand')); fireEvent.click(screen.getByText('production')); expect(updateFilters).toHaveBeenLastCalledWith({ lifecycles: new EntityLifecycleFilter(['production']), }); }); - it('removes lifecycles from filters', () => { + it('removes lifecycles from filters', async () => { const updateFilters = jest.fn(); render( - - - , + + + + + , ); + + await waitFor(() => expect(catalogApi.getEntityFacets).toHaveBeenCalled()); expect(updateFilters).toHaveBeenLastCalledWith({ lifecycles: new EntityLifecycleFilter(['production']), }); - fireEvent.click(screen.getByTestId('lifecycle-picker-expand')); + fireEvent.click(screen.getByTestId('lifecycles-picker-expand')); expect(screen.getByLabelText('production')).toBeChecked(); fireEvent.click(screen.getByLabelText('production')); @@ -162,67 +132,85 @@ describe('', () => { }); }); - it('responds to external queryParameters changes', () => { + it('responds to external queryParameters changes', async () => { const updateFilters = jest.fn(); const rendered = render( - - - , + + + + + , ); + + await waitFor(() => expect(catalogApi.getEntityFacets).toHaveBeenCalled()); expect(updateFilters).toHaveBeenLastCalledWith({ lifecycles: new EntityLifecycleFilter(['experimental']), }); + rendered.rerender( - - - , + + + + + , ); expect(updateFilters).toHaveBeenLastCalledWith({ lifecycles: new EntityLifecycleFilter(['production']), }); }); - it('removes lifecycles from filters if there are no available lifecycles', () => { + + it('removes lifecycles from filters if there are no available lifecycles', async () => { + catalogApi.getEntityFacets.mockResolvedValue({ + facets: { + 'spec.lifecycle': [], + }, + }); + const updateFilters = jest.fn(); render( - - - , + + + + + , ); + + await waitFor(() => expect(catalogApi.getEntityFacets).toHaveBeenCalled()); expect(updateFilters).toHaveBeenLastCalledWith({ lifecycles: undefined, }); }); - it('responds to initialFilter prop', () => { + + it('responds to initialFilter prop', async () => { const updateFilters = jest.fn(); render( - - - , + + + + + , ); + + await waitFor(() => expect(catalogApi.getEntityFacets).toHaveBeenCalled()); expect(updateFilters).toHaveBeenLastCalledWith({ lifecycles: new EntityLifecycleFilter(['production']), }); diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx index dd107b2673..a1bd6ef86b 100644 --- a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx +++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx @@ -14,22 +14,10 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; -import { - Box, - Checkbox, - FormControlLabel, - makeStyles, - TextField, - Typography, -} from '@material-ui/core'; -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, useState } from 'react'; -import { useEntityList } from '../../hooks/useEntityListProvider'; +import { makeStyles } from '@material-ui/core'; +import React from 'react'; import { EntityLifecycleFilter } from '../../filters'; +import { EntityAutocompletePicker } from '../EntityAutocompletePicker'; /** @public */ export type CatalogReactEntityLifecyclePickerClassKey = 'input'; @@ -43,98 +31,19 @@ const useStyles = makeStyles( }, ); -const icon = ; -const checkedIcon = ; - /** @public */ export const EntityLifecyclePicker = (props: { initialFilter?: string[] }) => { const { initialFilter = [] } = props; const classes = useStyles(); - const { - updateFilters, - backendEntities, - filters, - queryParameters: { lifecycles: lifecyclesParameter }, - } = useEntityList(); - - const queryParamLifecycles = useMemo( - () => [lifecyclesParameter].flat().filter(Boolean) as string[], - [lifecyclesParameter], - ); - - const [selectedLifecycles, setSelectedLifecycles] = useState( - queryParamLifecycles.length - ? queryParamLifecycles - : filters.lifecycles?.values ?? initialFilter, - ); - - // Set selected lifecycles on query parameter updates; this happens at initial page load and from - // external updates to the page location. - useEffect(() => { - if (queryParamLifecycles.length) { - setSelectedLifecycles(queryParamLifecycles); - } - }, [queryParamLifecycles]); - - const availableLifecycles = useMemo( - () => - [ - ...new Set( - backendEntities - .map((e: Entity) => e.spec?.lifecycle) - .filter(Boolean) as string[], - ), - ].sort(), - [backendEntities], - ); - - useEffect(() => { - updateFilters({ - lifecycles: - selectedLifecycles.length && availableLifecycles.length - ? new EntityLifecycleFilter(selectedLifecycles) - : undefined, - }); - }, [selectedLifecycles, updateFilters, availableLifecycles]); - - if (!availableLifecycles.length) return null; return ( - - - Lifecycle - - setSelectedLifecycles(value) - } - renderOption={(option, { selected }) => ( - - } - onClick={event => event.preventDefault()} - label={option} - /> - )} - size="small" - popupIcon={} - renderInput={params => ( - - )} - /> - - + ); }; From 429319d080cdb8ce8419ccb59b90344648406e60 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Mon, 22 May 2023 10:50:25 +0200 Subject: [PATCH 2/3] add changesets Signed-off-by: Vincenzo Scamporlino --- .changeset/silver-ducks-drum.md | 5 +++++ .changeset/three-guests-beam.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/silver-ducks-drum.md create mode 100644 .changeset/three-guests-beam.md diff --git a/.changeset/silver-ducks-drum.md b/.changeset/silver-ducks-drum.md new file mode 100644 index 0000000000..6cca57fdd4 --- /dev/null +++ b/.changeset/silver-ducks-drum.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +`EntityAutocompletePicker` add `initialSelectedOptions` prop diff --git a/.changeset/three-guests-beam.md b/.changeset/three-guests-beam.md new file mode 100644 index 0000000000..b46e80a13c --- /dev/null +++ b/.changeset/three-guests-beam.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +`EntityLifecycleFilter` loads data using the facets endpoint From 918f0e2c17d00c88bd589471c593933bb2d2c26a Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Mon, 22 May 2023 14:41:44 +0200 Subject: [PATCH 3/3] catalog-react: api reports Signed-off-by: Vincenzo Scamporlino --- plugins/catalog-react/api-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index f9725e41e8..90bddecd71 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -195,7 +195,7 @@ export class EntityLifecycleFilter implements EntityFilter { // @public (undocumented) export const EntityLifecyclePicker: (props: { initialFilter?: string[]; -}) => JSX.Element | null; +}) => JSX.Element; // @public export const EntityListContext: React_2.Context<