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 (