catalog-react: useFacetsEntities use facets endpoint

Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>
This commit is contained in:
Vincenzo Scamporlino
2024-11-26 15:47:39 +01:00
parent 1a03dc6f69
commit f8ca7b8c02
2 changed files with 83 additions and 151 deletions
@@ -82,9 +82,6 @@ describe('useFacetsEntities', () => {
mockCatalogApi.getEntityFacets.mockResolvedValue(
facetsFromEntityRefs(entityRefs),
);
mockCatalogApi.getEntitiesByRefs.mockResolvedValue(
entitiesFromEntityRefs(entityRefs),
);
const { result } = renderHook(() => useFacetsEntities({ enabled: true }));
@@ -110,48 +107,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 +131,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 +183,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 +202,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 +222,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 +229,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 +254,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 +302,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 +318,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)
);
}