Decouple EntityTagPicker from backend entities
Signed-off-by: Vincenzo Scamporlino <me@vinzscam.dev>
This commit is contained in:
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user