Skip to content

Commit

Permalink
Decouple EntityTagPicker from backend entities
Browse files Browse the repository at this point in the history
Signed-off-by: Vincenzo Scamporlino <me@vinzscam.dev>
  • Loading branch information
vinzscam committed Mar 8, 2022
1 parent 1998dfb commit 4af8296
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 116 deletions.
7 changes: 7 additions & 0 deletions .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.
Expand Up @@ -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'));

Expand All @@ -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'));
Expand All @@ -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();

Expand All @@ -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']),
Expand Down
Expand Up @@ -14,7 +14,6 @@
* limitations under the License.
*/

import { Entity } from '@backstage/catalog-model';
import {
Box,
Checkbox,
Expand All @@ -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';
Expand All @@ -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[],
Expand All @@ -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}>
Expand Down

0 comments on commit 4af8296

Please sign in to comment.