Merge pull request #27839 from backstage/entity-owner-picker-owners-only-rollback
Catalog: EntityOwnerPicker use getEntityFacets in owners-only mode
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
---
|
||||
|
||||
Fixed an issue where the `EntityOwnerPicker` component failed to load when the `mode` prop was set to `owners-only`. In this mode, the `EntityOwnerPicker` does not load details about the owners, such as `displayName` or `title`. To display these details, use `mode=all` instead.
|
||||
@@ -402,17 +402,16 @@ describe('<EntityOwnerPicker mode="owners-only" />', () => {
|
||||
|
||||
fireEvent.click(screen.getByTestId('owner-picker-expand'));
|
||||
|
||||
// // some-owner, some-owner-2, another-owner, another-owner-2
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Another Owner')).toBeInTheDocument(),
|
||||
expect(screen.getByText('some-owner')).toBeInTheDocument(),
|
||||
);
|
||||
|
||||
[
|
||||
'some-owner',
|
||||
'Some Owner 2',
|
||||
'Another Owner in Another Namespace',
|
||||
].forEach(owner => {
|
||||
expect(screen.getByText(owner)).toBeInTheDocument();
|
||||
});
|
||||
['some-owner-2', 'another-owner', 'test-namespace/another-owner-2'].forEach(
|
||||
owner => {
|
||||
expect(screen.getByText(owner)).toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
|
||||
expect(mockCatalogApi.getEntityFacets).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -423,9 +422,9 @@ describe('<EntityOwnerPicker mode="owners-only" />', () => {
|
||||
);
|
||||
|
||||
[
|
||||
'some-owner-batch-2',
|
||||
'Some Owner Batch 2',
|
||||
'Another Owner in Another Namespace Batch 2',
|
||||
'some-owner-2-batch-2',
|
||||
'another-owner-batch-2',
|
||||
'test-namespace/another-owner-2-batch-2',
|
||||
].forEach(owner => {
|
||||
expect(screen.getByText(owner)).toBeInTheDocument();
|
||||
});
|
||||
@@ -465,10 +464,8 @@ describe('<EntityOwnerPicker mode="owners-only" />', () => {
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
);
|
||||
expect(mockCatalogApi.getEntitiesByRefs).toHaveBeenCalledWith({
|
||||
entityRefs: [...ownerEntitiesBatch1, ...ownerEntitiesBatch2].map(entity =>
|
||||
stringifyEntityRef(entity),
|
||||
),
|
||||
expect(mockCatalogApi.getEntityFacets).toHaveBeenCalledWith({
|
||||
facets: ['relations.ownedBy'],
|
||||
});
|
||||
expect(updateFilters).toHaveBeenLastCalledWith({
|
||||
owners: undefined,
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { useFacetsEntities } from './useFacetsEntities';
|
||||
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import { Entity, parseEntityRef } from '@backstage/catalog-model';
|
||||
|
||||
const mockCatalogApi = catalogApiMock.mock();
|
||||
|
||||
@@ -37,25 +36,6 @@ describe('useFacetsEntities', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const entitiesFromEntityRefs = (
|
||||
entityRefs: string[],
|
||||
enrichedEntities: { [key: string]: Entity } = {},
|
||||
) => ({
|
||||
items: entityRefs.map(ref => {
|
||||
const compoundRef = parseEntityRef(ref);
|
||||
return (
|
||||
enrichedEntities[ref] || {
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: compoundRef.kind,
|
||||
metadata: {
|
||||
name: compoundRef.name,
|
||||
namespace: compoundRef.namespace,
|
||||
},
|
||||
}
|
||||
);
|
||||
}),
|
||||
});
|
||||
|
||||
it(`should return empty items when facets are loading`, () => {
|
||||
mockCatalogApi.getEntityFacets.mockReturnValue(new Promise(() => {}));
|
||||
const { result } = renderHook(() => useFacetsEntities({ enabled: true }));
|
||||
@@ -82,9 +62,6 @@ describe('useFacetsEntities', () => {
|
||||
mockCatalogApi.getEntityFacets.mockResolvedValue(
|
||||
facetsFromEntityRefs(entityRefs),
|
||||
);
|
||||
mockCatalogApi.getEntitiesByRefs.mockResolvedValue(
|
||||
entitiesFromEntityRefs(entityRefs),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useFacetsEntities({ enabled: true }));
|
||||
|
||||
@@ -110,48 +87,20 @@ describe('useFacetsEntities', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it(`should return the owners sorted by namespace, (displayName or title or name) and kind`, async () => {
|
||||
it(`should return the owners sorted by kind, namespace and name`, async () => {
|
||||
const entityRefs = [
|
||||
'group:namespace/team-b',
|
||||
'component:default/c',
|
||||
'user:default/c',
|
||||
'group:default/a',
|
||||
'component:default/a',
|
||||
'component:default/b',
|
||||
'user:default/a',
|
||||
'user:default/b',
|
||||
'group:default/d',
|
||||
'group:default/e',
|
||||
];
|
||||
|
||||
const enrichedEntities: { [key: string]: Entity } = {
|
||||
'group:default/a': {
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'group',
|
||||
metadata: { name: 'a', namespace: 'default', title: 'My title A' },
|
||||
},
|
||||
'component:default/a': {
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'component',
|
||||
metadata: { name: 'a', namespace: 'default', title: 'My title B' },
|
||||
},
|
||||
'group:default/d': {
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'group',
|
||||
metadata: { name: 'd', namespace: 'default' },
|
||||
spec: { profile: { displayName: 'My display name D' } },
|
||||
},
|
||||
'group:default/e': {
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'group',
|
||||
metadata: { name: 'e', namespace: 'default' },
|
||||
spec: { profile: { displayName: 'My display name E' } },
|
||||
},
|
||||
};
|
||||
|
||||
mockCatalogApi.getEntityFacets.mockResolvedValue(
|
||||
facetsFromEntityRefs(entityRefs),
|
||||
);
|
||||
mockCatalogApi.getEntitiesByRefs.mockResolvedValue(
|
||||
entitiesFromEntityRefs(entityRefs, enrichedEntities),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useFacetsEntities({ enabled: true }));
|
||||
|
||||
@@ -162,49 +111,48 @@ describe('useFacetsEntities', () => {
|
||||
items: [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'component',
|
||||
metadata: { name: 'b', namespace: 'default' },
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'component',
|
||||
metadata: { name: 'c', namespace: 'default' },
|
||||
kind: 'group',
|
||||
metadata: { name: 'a', namespace: 'default' },
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'group',
|
||||
metadata: { name: 'd', namespace: 'default' },
|
||||
spec: { profile: { displayName: 'My display name D' } },
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'group',
|
||||
metadata: { name: 'e', namespace: 'default' },
|
||||
spec: { profile: { displayName: 'My display name E' } },
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'group',
|
||||
metadata: {
|
||||
name: 'a',
|
||||
namespace: 'default',
|
||||
title: 'My title A',
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'component',
|
||||
metadata: {
|
||||
name: 'a',
|
||||
namespace: 'default',
|
||||
title: 'My title B',
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'group',
|
||||
metadata: { name: 'team-b', namespace: 'namespace' },
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'user',
|
||||
metadata: {
|
||||
name: 'a',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'user',
|
||||
metadata: {
|
||||
name: 'b',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'user',
|
||||
metadata: {
|
||||
name: 'c',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
loading: false,
|
||||
@@ -215,18 +163,15 @@ describe('useFacetsEntities', () => {
|
||||
it(`should paginate the data accordingly`, async () => {
|
||||
const entityRefs = [
|
||||
'group:namespace/team-b',
|
||||
'component:default/c',
|
||||
'user:default/c',
|
||||
'group:default/a',
|
||||
'component:default/a',
|
||||
'component:default/b',
|
||||
'user:default/a',
|
||||
'user:default/b',
|
||||
];
|
||||
|
||||
mockCatalogApi.getEntityFacets.mockResolvedValue(
|
||||
facetsFromEntityRefs(entityRefs),
|
||||
);
|
||||
mockCatalogApi.getEntitiesByRefs.mockResolvedValue(
|
||||
entitiesFromEntityRefs(entityRefs),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useFacetsEntities({ enabled: true }));
|
||||
|
||||
@@ -237,13 +182,13 @@ describe('useFacetsEntities', () => {
|
||||
items: [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'component',
|
||||
kind: 'group',
|
||||
metadata: { name: 'a', namespace: 'default' },
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'group',
|
||||
metadata: { name: 'a', namespace: 'default' },
|
||||
metadata: { name: 'team-b', namespace: 'namespace' },
|
||||
},
|
||||
],
|
||||
cursor: 'eyJ0ZXh0IjoiIiwic3RhcnQiOjJ9',
|
||||
@@ -257,11 +202,6 @@ describe('useFacetsEntities', () => {
|
||||
expect(result.current[0]).toEqual({
|
||||
value: {
|
||||
items: [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'component',
|
||||
metadata: { name: 'a', namespace: 'default' },
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'group',
|
||||
@@ -269,13 +209,18 @@ describe('useFacetsEntities', () => {
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'component',
|
||||
metadata: { name: 'b', namespace: 'default' },
|
||||
kind: 'group',
|
||||
metadata: { name: 'team-b', namespace: 'namespace' },
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'component',
|
||||
metadata: { name: 'c', namespace: 'default' },
|
||||
kind: 'user',
|
||||
metadata: { name: 'a', namespace: 'default' },
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'user',
|
||||
metadata: { name: 'b', namespace: 'default' },
|
||||
},
|
||||
],
|
||||
cursor: 'eyJ0ZXh0IjoiIiwic3RhcnQiOjR9',
|
||||
@@ -289,31 +234,31 @@ describe('useFacetsEntities', () => {
|
||||
expect(result.current[0]).toEqual({
|
||||
value: {
|
||||
items: [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'component',
|
||||
metadata: { name: 'a', namespace: 'default' },
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'group',
|
||||
metadata: { name: 'a', namespace: 'default' },
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'component',
|
||||
metadata: { name: 'b', namespace: 'default' },
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'component',
|
||||
metadata: { name: 'c', namespace: 'default' },
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'group',
|
||||
metadata: { name: 'team-b', namespace: 'namespace' },
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'user',
|
||||
metadata: { name: 'a', namespace: 'default' },
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'user',
|
||||
metadata: { name: 'b', namespace: 'default' },
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'user',
|
||||
metadata: { name: 'c', namespace: 'default' },
|
||||
},
|
||||
],
|
||||
},
|
||||
loading: false,
|
||||
@@ -337,34 +282,15 @@ describe('useFacetsEntities', () => {
|
||||
mockCatalogApi.getEntityFacets.mockResolvedValue(
|
||||
facetsFromEntityRefs(entityRefs),
|
||||
);
|
||||
const enrichedEntities: { [key: string]: Entity } = {
|
||||
'group:default/go': {
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'group',
|
||||
metadata: { name: 'go', namespace: 'default', title: 'Hidden Spider' },
|
||||
},
|
||||
'component:default/lemon': {
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'component',
|
||||
metadata: { name: 'lemon', namespace: 'default' },
|
||||
spec: {
|
||||
profile: { displayName: 'Lemon Spider' },
|
||||
},
|
||||
},
|
||||
};
|
||||
mockCatalogApi.getEntitiesByRefs.mockResolvedValue(
|
||||
entitiesFromEntityRefs(entityRefs, enrichedEntities),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useFacetsEntities({ enabled: true }));
|
||||
|
||||
result.current[1]({ text: 'der ' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current[0]).toEqual({
|
||||
value: {
|
||||
items: [
|
||||
enrichedEntities['group:default/go'],
|
||||
enrichedEntities['component:default/lemon'],
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'component',
|
||||
@@ -372,13 +298,13 @@ describe('useFacetsEntities', () => {
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'group',
|
||||
metadata: { name: 'spiderman', namespace: 'namespace' },
|
||||
kind: 'component',
|
||||
metadata: { name: 'a-component', namespace: 'spiders' },
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind: 'component',
|
||||
metadata: { name: 'a-component', namespace: 'spiders' },
|
||||
kind: 'group',
|
||||
metadata: { name: 'spiderman', namespace: 'namespace' },
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
|
||||
@@ -17,8 +17,7 @@ import { useApi } from '@backstage/core-plugin-api';
|
||||
import useAsyncFn from 'react-use/esm/useAsyncFn';
|
||||
import { catalogApiRef } from '../../api';
|
||||
import { useState } from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import get from 'lodash/get';
|
||||
import { Entity, parseEntityRef } from '@backstage/catalog-model';
|
||||
|
||||
type FacetsCursor = {
|
||||
start: number;
|
||||
@@ -34,15 +33,13 @@ type FacetsInitialRequest = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
const maybeString = (value: unknown): string | undefined =>
|
||||
typeof value === 'string' ? value : undefined;
|
||||
|
||||
/**
|
||||
* This hook asynchronously loads the entity owners using the facets endpoint.
|
||||
* EntityOwnerPicker uses this hook when mode="owners-only" is passed as prop.
|
||||
* All the owners are kept internally in memory and rendered in batches once requested
|
||||
* by the frontend. The values returned by this hook are compatible with `useQueryEntities`
|
||||
* hook, which is also used by EntityOwnerPicker.
|
||||
* In this mode, the EntityOwnerPicker won't show detailed information of the owners.
|
||||
*/
|
||||
export function useFacetsEntities({ enabled }: { enabled: boolean }) {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
@@ -52,37 +49,30 @@ export function useFacetsEntities({ enabled }: { enabled: boolean }) {
|
||||
return [];
|
||||
}
|
||||
const facet = 'relations.ownedBy';
|
||||
const facetsResponse = await catalogApi.getEntityFacets({
|
||||
facets: [facet],
|
||||
});
|
||||
const entityRefs = facetsResponse.facets[facet]?.map(e => e.value) ?? [];
|
||||
|
||||
return catalogApi
|
||||
.getEntitiesByRefs({ entityRefs })
|
||||
.then(resp =>
|
||||
resp.items
|
||||
.filter(entity => entity !== undefined)
|
||||
.map(entity => entity as Entity)
|
||||
.getEntityFacets({ facets: [facet] })
|
||||
.then(response =>
|
||||
response.facets[facet]
|
||||
.map(e => e.value)
|
||||
.map(ref => {
|
||||
const { kind, name, namespace } = parseEntityRef(ref);
|
||||
return {
|
||||
apiVersion: 'backstage.io/v1beta1',
|
||||
kind,
|
||||
metadata: { name, namespace },
|
||||
};
|
||||
})
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(a.metadata.namespace || '').localeCompare(
|
||||
b.metadata.namespace || '',
|
||||
a.kind.localeCompare(b.kind, 'en-US') ||
|
||||
a.metadata.namespace.localeCompare(
|
||||
b.metadata.namespace,
|
||||
'en-US',
|
||||
) ||
|
||||
(
|
||||
maybeString(get(a, 'spec.profile.displayName')) ||
|
||||
a.metadata.title ||
|
||||
a.metadata.name
|
||||
).localeCompare(
|
||||
maybeString(get(b, 'spec.profile.displayName')) ||
|
||||
b.metadata.title ||
|
||||
b.metadata.name,
|
||||
'en-US',
|
||||
) ||
|
||||
a.kind.localeCompare(b.kind, 'en-US'),
|
||||
a.metadata.name.localeCompare(b.metadata.name, 'en-US'),
|
||||
),
|
||||
)
|
||||
.then(entities => entities)
|
||||
.catch(() => []);
|
||||
});
|
||||
|
||||
@@ -161,10 +151,6 @@ function filterEntity(text: string, entity: Entity) {
|
||||
return (
|
||||
entity.kind.includes(normalizedText) ||
|
||||
entity.metadata.namespace?.includes(normalizedText) ||
|
||||
entity.metadata.name.includes(normalizedText) ||
|
||||
entity.metadata.title?.includes(normalizedText) ||
|
||||
(get(entity, 'spec.profile.displayName') as unknown as string)?.includes(
|
||||
normalizedText,
|
||||
)
|
||||
entity.metadata.name.includes(normalizedText)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user