From 4af82967f409c02ef418336a277edf817410631b Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 8 Mar 2022 21:50:02 +0100 Subject: [PATCH 1/2] Decouple EntityTagPicker from backend entities Signed-off-by: Vincenzo Scamporlino --- .changeset/forty-poets-tie.md | 7 + .../EntityTagPicker/EntityTagPicker.test.tsx | 201 +++++++++--------- .../EntityTagPicker/EntityTagPicker.tsx | 31 ++- 3 files changed, 123 insertions(+), 116 deletions(-) create mode 100644 .changeset/forty-poets-tie.md diff --git a/.changeset/forty-poets-tie.md b/.changeset/forty-poets-tie.md new file mode 100644 index 0000000000..9118100934 --- /dev/null +++ b/.changeset/forty-poets-tie.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Decouple tags picker from backend entities + +`EntityTagPicker` fetches all the tags independently and it doesn't require all the entities to be available client side. diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx index c6b985bc8b..2be521203f 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx @@ -14,60 +14,49 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; -import { fireEvent, render } from '@testing-library/react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; import React from 'react'; import { MockEntityListContextProvider } from '../../testUtils/providers'; import { EntityTagFilter } from '../../filters'; import { EntityTagPicker } from './EntityTagPicker'; +import { TestApiProvider } from '@backstage/test-utils'; +import { catalogApiRef } from '../../api'; +import { CatalogApi } from '@backstage/catalog-client'; -const taggedEntities: Entity[] = [ - { - apiVersion: '1', - kind: 'Component', - metadata: { - name: 'component-1', - tags: ['tag4', 'tag1', 'tag2'], - }, - }, - { - apiVersion: '1', - kind: 'Component', - metadata: { - name: 'component-2', - tags: ['tag3', 'tag4'], - }, - }, -]; +const tags = ['tag1', 'tag2', 'tag3', 'tag4']; describe('', () => { - it('renders all tags', () => { + const mockCatalogApiRef = { + getEntityFacets: async () => ({ + facets: { 'metadata.tags': tags.map(value => ({ value })) }, + }), + } as unknown as CatalogApi; + + it('renders all tags', async () => { const rendered = render( - - - , + + + + + , ); - expect(rendered.getByText('Tags')).toBeInTheDocument(); + await waitFor(() => expect(rendered.getByText('Tags')).toBeInTheDocument()); fireEvent.click(rendered.getByTestId('tag-picker-expand')); - taggedEntities - .flatMap(e => e.metadata.tags!) - .forEach(tag => { - expect(rendered.getByText(tag)).toBeInTheDocument(); - }); + tags.forEach(tag => { + expect(rendered.getByText(tag)).toBeInTheDocument(); + }); }); - it('renders unique tags in alphabetical order', () => { + it('renders unique tags in alphabetical order', async () => { const rendered = render( - - - , + + + + + , ); - expect(rendered.getByText('Tags')).toBeInTheDocument(); + await waitFor(() => expect(rendered.getByText('Tags')).toBeInTheDocument()); fireEvent.click(rendered.getByTestId('tag-picker-expand')); @@ -79,43 +68,47 @@ describe('', () => { ]); }); - it('respects the query parameter filter value', () => { + it('respects the query parameter filter value', async () => { const updateFilters = jest.fn(); const queryParameters = { tags: ['tag3'] }; render( - - - , + + + + + , ); - expect(updateFilters).toHaveBeenLastCalledWith({ - tags: new EntityTagFilter(['tag3']), - }); + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + tags: new EntityTagFilter(['tag3']), + }), + ); }); - it('adds tags to filters', () => { + it('adds tags to filters', async () => { const updateFilters = jest.fn(); const rendered = render( - - - , + + + + + , + ); + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + tags: undefined, + }), ); - expect(updateFilters).toHaveBeenLastCalledWith({ - tags: undefined, - }); fireEvent.click(rendered.getByTestId('tag-picker-expand')); fireEvent.click(rendered.getByText('tag1')); @@ -124,23 +117,25 @@ describe('', () => { }); }); - it('removes tags from filters', () => { + it('removes tags from filters', async () => { const updateFilters = jest.fn(); const rendered = render( - - - , + + + + + , + ); + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + tags: new EntityTagFilter(['tag1']), + }), ); - expect(updateFilters).toHaveBeenLastCalledWith({ - tags: new EntityTagFilter(['tag1']), - }); fireEvent.click(rendered.getByTestId('tag-picker-expand')); expect(rendered.getByLabelText('tag1')).toBeChecked(); @@ -150,30 +145,36 @@ describe('', () => { }); }); - it('responds to external queryParameters changes', () => { + it('responds to external queryParameters changes', async () => { const updateFilters = jest.fn(); const rendered = render( - - - , + + + + + , + ); + await waitFor(() => + expect(updateFilters).toHaveBeenLastCalledWith({ + tags: new EntityTagFilter(['tag1']), + }), ); - expect(updateFilters).toHaveBeenLastCalledWith({ - tags: new EntityTagFilter(['tag1']), - }); rendered.rerender( - - - , + + + + + , ); expect(updateFilters).toHaveBeenLastCalledWith({ tags: new EntityTagFilter(['tag2']), diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx index 5b5e0e7ab9..eafa10696c 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; import { Box, Checkbox, @@ -30,6 +29,9 @@ import { Autocomplete } from '@material-ui/lab'; import React, { useEffect, useMemo, useState } from 'react'; import { useEntityList } from '../../hooks/useEntityListProvider'; import { EntityTagFilter } from '../../filters'; +import { useApi } from '@backstage/core-plugin-api'; +import useAsync from 'react-use/lib/useAsync'; +import { catalogApiRef } from '../../api'; /** @public */ export type CatalogReactEntityTagPickerClassKey = 'input'; @@ -49,8 +51,17 @@ const checkedIcon = ; /** @public */ export const EntityTagPicker = () => { const classes = useStyles(); - const { updateFilters, backendEntities, filters, queryParameters } = - useEntityList(); + const { updateFilters, filters, queryParameters } = useEntityList(); + + const catalogApi = useApi(catalogApiRef); + const { value: availableTags } = useAsync(async () => { + const facet = 'metadata.tags'; + const { facets } = await catalogApi.getEntityFacets({ + facets: [facet], + }); + + return facets[facet].map(({ value }) => value); + }, []); const queryParamTags = useMemo( () => [queryParameters.tags].flat().filter(Boolean) as string[], @@ -75,19 +86,7 @@ export const EntityTagPicker = () => { }); }, [selectedTags, updateFilters]); - const availableTags = useMemo( - () => - [ - ...new Set( - backendEntities - .flatMap((e: Entity) => e.metadata.tags) - .filter(Boolean) as string[], - ), - ].sort(), - [backendEntities], - ); - - if (!availableTags.length) return null; + if (!availableTags?.length) return null; return ( From e74b06b35df05e725ece0312b4894d0a2c99baaa Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Wed, 9 Mar 2022 16:25:12 +0100 Subject: [PATCH 2/2] Apply kind filter when fetching tags Signed-off-by: Vincenzo Scamporlino --- .../src/components/EntityTagPicker/EntityTagPicker.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx index eafa10696c..ddb70c6d31 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx @@ -58,10 +58,11 @@ export const EntityTagPicker = () => { const facet = 'metadata.tags'; const { facets } = await catalogApi.getEntityFacets({ facets: [facet], + filter: filters.kind?.getCatalogFilters(), }); return facets[facet].map(({ value }) => value); - }, []); + }, [filters.kind]); const queryParamTags = useMemo( () => [queryParameters.tags].flat().filter(Boolean) as string[],