diff --git a/.changeset/modern-buses-protect.md b/.changeset/modern-buses-protect.md new file mode 100644 index 0000000000..2aed52eca0 --- /dev/null +++ b/.changeset/modern-buses-protect.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +When a user has zero owned entities when viewing an entity kind in the catalog +page, it will be automatically redirected to see all the entities. Furthermore, +for the kind User and Group there are no longer the owned selector. diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx index f861355762..9aafdf1437 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx @@ -122,26 +122,26 @@ export const UserListPicker = ({ const classes = useStyles(); const configApi = useApi(configApiRef); const orgName = configApi.getOptionalString('organization.name') ?? 'Company'; + const { filters, updateFilters, backendEntities, queryParameters } = + useEntityListProvider(); // Remove group items that aren't in availableFilters and exclude // any now-empty groups. + const userAndGroupFilterIds = ['starred', 'all']; const filterGroups = getFilterGroups(orgName) .map(filterGroup => ({ ...filterGroup, - items: filterGroup.items.filter( - ({ id }) => !availableFilters || availableFilters.includes(id), + items: filterGroup.items.filter(({ id }) => + // TODO: avoid hardcoding kinds here + ['group', 'user'].some(kind => kind === queryParameters.kind) + ? userAndGroupFilterIds.includes(id) + : !availableFilters || availableFilters.includes(id), ), })) .filter(({ items }) => !!items.length); - const { filters, updateFilters, backendEntities, queryParameters } = - useEntityListProvider(); - const { isStarredEntity } = useStarredEntities(); const { isOwnedEntity } = useEntityOwnership(); - const [selectedUserFilter, setSelectedUserFilter] = useState( - [queryParameters.user].flat()[0] ?? initialFilter, - ); // Static filters; used for generating counts of potentially unselected kinds const ownedFilter = useMemo( @@ -153,6 +153,19 @@ export const UserListPicker = ({ [isOwnedEntity, isStarredEntity], ); + // To show proper counts for each section, apply all other frontend filters _except_ the user + // filter that's controlled by this picker. + const [entitiesWithoutUserFilter, setEntitiesWithoutUserFilter] = + useState(backendEntities); + const totalOwnedUserEntities = entitiesWithoutUserFilter.filter(entity => + ownedFilter.filterEntity(entity), + ).length; + const [selectedUserFilter, setSelectedUserFilter] = useState( + totalOwnedUserEntities > 0 + ? [queryParameters.user].flat()[0] ?? initialFilter + : 'all', + ); + useEffect(() => { updateFilters({ user: selectedUserFilter @@ -165,10 +178,6 @@ export const UserListPicker = ({ }); }, [selectedUserFilter, isOwnedEntity, isStarredEntity, updateFilters]); - // To show proper counts for each section, apply all other frontend filters _except_ the user - // filter that's controlled by this picker. - const [entitiesWithoutUserFilter, setEntitiesWithoutUserFilter] = - useState(backendEntities); useEffect(() => { const filterFn = reduceEntityFilters( compact(Object.values({ ...filters, user: undefined })), @@ -179,9 +188,7 @@ export const UserListPicker = ({ function getFilterCount(id: UserListFilterKind) { switch (id) { case 'owned': - return entitiesWithoutUserFilter.filter(entity => - ownedFilter.filterEntity(entity), - ).length; + return totalOwnedUserEntities; case 'starred': return entitiesWithoutUserFilter.filter(entity => starredFilter.filterEntity(entity), diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index b96f63304a..fd5cf3b9dd 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -137,12 +137,27 @@ describe('', () => { const { result, waitFor } = renderHook(() => useEntityListProvider(), { wrapper, initialProps: { - userFilter: 'owned', + userFilter: 'all', }, }); await waitFor(() => !!result.current.entities.length); expect(result.current.backendEntities.length).toBe(2); - expect(result.current.entities.length).toBe(1); + + act(() => + result.current.updateFilters({ + user: new UserListFilter( + 'owned', + entity => entity.metadata.name === 'component-1', + () => true, + ), + }), + ); + + await waitFor(() => { + expect(result.current.backendEntities.length).toBe(2); + expect(result.current.entities.length).toBe(1); + expect(mockCatalogApi.getEntities).toHaveBeenCalledTimes(1); + }); }); it('resolves query param filter values', async () => { diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 0a46580c12..effd9f5bc6 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -189,12 +189,14 @@ describe('CatalogPage', () => { c => c.tagName === 'SPAN', ); const columnHeaderLabels = columnHeader.map(c => c.textContent); - expect(columnHeaderLabels).toEqual(['Foo', 'Bar', 'Baz', 'Actions']); }, 20_000); it('should render the default actions of an item in the grid', async () => { - const { findByTitle, findByText } = await renderWrapped(); + const { getByTestId, findByTitle, findByText } = await renderWrapped( + , + ); + fireEvent.click(getByTestId('user-picker-owned')); expect(await findByText(/Owned \(1\)/)).toBeInTheDocument(); expect(await findByTitle(/View/)).toBeInTheDocument(); expect(await findByTitle(/Edit/)).toBeInTheDocument(); @@ -221,9 +223,10 @@ describe('CatalogPage', () => { }, ]; - const { findByTitle, findByText } = await renderWrapped( + const { getByTestId, findByTitle, findByText } = await renderWrapped( , ); + fireEvent.click(getByTestId('user-picker-owned')); expect(await findByText(/Owned \(1\)/)).toBeInTheDocument(); expect(await findByTitle(/Foo Action/)).toBeInTheDocument(); expect(await findByTitle(/Bar Action/)).toBeInTheDocument(); @@ -235,6 +238,7 @@ describe('CatalogPage', () => { // https://github.com/mbrn/material-table/issues/1293 it('should render', async () => { const { findByText, getByTestId } = await renderWrapped(); + fireEvent.click(getByTestId('user-picker-owned')); await expect(findByText(/Owned \(1\)/)).resolves.toBeInTheDocument(); fireEvent.click(getByTestId('user-picker-all')); await expect(findByText(/All \(2\)/)).resolves.toBeInTheDocument(); @@ -250,7 +254,8 @@ describe('CatalogPage', () => { // this test is for fixing the bug after favoriting an entity, the matching // entities defaulting to "owned" filter and not based on the selected filter it('should render the correct entities filtered on the selected filter', async () => { - await renderWrapped(); + const { getByTestId } = await renderWrapped(); + fireEvent.click(getByTestId('user-picker-owned')); await expect(screen.findByText(/Owned \(1\)/)).resolves.toBeInTheDocument(); fireEvent.click(screen.getByTestId('user-picker-starred')); await expect(