Decouple EntityTagPicker from backend entities

Signed-off-by: Vincenzo Scamporlino <me@vinzscam.dev>
This commit is contained in:
Vincenzo Scamporlino
2022-03-08 21:50:02 +01:00
parent 1998dfb2f4
commit 4af82967f4
3 changed files with 123 additions and 116 deletions
+7
View File
@@ -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.
@@ -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('<EntityTagPicker/>', () => {
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(
<MockEntityListContextProvider
value={{ entities: taggedEntities, backendEntities: taggedEntities }}
>
<EntityTagPicker />
</MockEntityListContextProvider>,
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider value={{}}>
<EntityTagPicker />
</MockEntityListContextProvider>
</TestApiProvider>,
);
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(
<MockEntityListContextProvider
value={{ entities: taggedEntities, backendEntities: taggedEntities }}
>
<EntityTagPicker />
</MockEntityListContextProvider>,
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider value={{}}>
<EntityTagPicker />
</MockEntityListContextProvider>
</TestApiProvider>,
);
expect(rendered.getByText('Tags')).toBeInTheDocument();
await waitFor(() => expect(rendered.getByText('Tags')).toBeInTheDocument());
fireEvent.click(rendered.getByTestId('tag-picker-expand'));
@@ -79,43 +68,47 @@ describe('<EntityTagPicker/>', () => {
]);
});
it('respects the query parameter filter value', () => {
it('respects the query parameter filter value', async () => {
const updateFilters = jest.fn();
const queryParameters = { tags: ['tag3'] };
render(
<MockEntityListContextProvider
value={{
entities: taggedEntities,
backendEntities: taggedEntities,
updateFilters,
queryParameters,
}}
>
<EntityTagPicker />
</MockEntityListContextProvider>,
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider
value={{
updateFilters,
queryParameters,
}}
>
<EntityTagPicker />
</MockEntityListContextProvider>
</TestApiProvider>,
);
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(
<MockEntityListContextProvider
value={{
entities: taggedEntities,
backendEntities: taggedEntities,
updateFilters,
}}
>
<EntityTagPicker />
</MockEntityListContextProvider>,
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider
value={{
updateFilters,
}}
>
<EntityTagPicker />
</MockEntityListContextProvider>
</TestApiProvider>,
);
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('<EntityTagPicker/>', () => {
});
});
it('removes tags from filters', () => {
it('removes tags from filters', async () => {
const updateFilters = jest.fn();
const rendered = render(
<MockEntityListContextProvider
value={{
entities: taggedEntities,
backendEntities: taggedEntities,
updateFilters,
filters: { tags: new EntityTagFilter(['tag1']) },
}}
>
<EntityTagPicker />
</MockEntityListContextProvider>,
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider
value={{
updateFilters,
filters: { tags: new EntityTagFilter(['tag1']) },
}}
>
<EntityTagPicker />
</MockEntityListContextProvider>
</TestApiProvider>,
);
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('<EntityTagPicker/>', () => {
});
});
it('responds to external queryParameters changes', () => {
it('responds to external queryParameters changes', async () => {
const updateFilters = jest.fn();
const rendered = render(
<MockEntityListContextProvider
value={{
updateFilters,
queryParameters: { tags: ['tag1'] },
}}
>
<EntityTagPicker />
</MockEntityListContextProvider>,
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider
value={{
updateFilters,
queryParameters: { tags: ['tag1'] },
}}
>
<EntityTagPicker />
</MockEntityListContextProvider>
</TestApiProvider>,
);
await waitFor(() =>
expect(updateFilters).toHaveBeenLastCalledWith({
tags: new EntityTagFilter(['tag1']),
}),
);
expect(updateFilters).toHaveBeenLastCalledWith({
tags: new EntityTagFilter(['tag1']),
});
rendered.rerender(
<MockEntityListContextProvider
value={{
updateFilters,
queryParameters: { tags: ['tag2'] },
}}
>
<EntityTagPicker />
</MockEntityListContextProvider>,
<TestApiProvider apis={[[catalogApiRef, mockCatalogApiRef]]}>
<MockEntityListContextProvider
value={{
updateFilters,
queryParameters: { tags: ['tag2'] },
}}
>
<EntityTagPicker />
</MockEntityListContextProvider>
</TestApiProvider>,
);
expect(updateFilters).toHaveBeenLastCalledWith({
tags: new EntityTagFilter(['tag2']),
@@ -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 = <CheckBoxIcon fontSize="small" />;
/** @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 (
<Box pb={1} pt={1}>